/home/runner/work/klsn/klsn/_build/test/cover/aggregate/klsn_db.html

1 -module(klsn_db).
2
3 -export([
4 create_db/1
5 , create_db/2
6 , create_doc/2
7 , create_doc/3
8 , bulk_create_doc/2
9 , bulk_create_doc/3
10 , exists/2
11 , exists/3
12 , get/2
13 , get/3
14 , lookup/2
15 , lookup/3
16 , bulk_lookup/2
17 , bulk_lookup/3
18 , mango_find/2
19 , mango_find/3
20 , mango_index/2
21 , mango_index/3
22 , mango_explain/2
23 , mango_explain/3
24 , update/3
25 , update/4
26 , upsert/3
27 , upsert/4
28 , bulk_upsert/3
29 , bulk_upsert/4
30 , time_now/0
31 , new_id/0
32 , db_info/0
33 ]).
34 -export_type([
35 info/0
36 , db/0
37 , key/0
38 , payload/0
39 , value/0
40 , id/0
41 , rev/0
42 , update_function/0
43 , upsert_function/0
44 ]).
45
46 %% ------------------------------------------------------------------
47 %% Exported types
48 %% ------------------------------------------------------------------
49
50 %% Connection information used by the helper functions when talking to a
51 %% CouchDB-compatible server. Currently only the base URL is recorded.
52 -type info() :: #{
53 url := unicode:unicode_binary()
54 }.
55
56 %% Name of the database (will be url-encoded when used in a request).
57 -type db() :: unicode:unicode_binary().
58
59 %% Document key (i.e. the _id field) inside the database.
60 -type key() :: unicode:unicode_binary().
61
62 %% Document identifier returned by CouchDB after a create / update.
63 -type id() :: unicode:unicode_binary().
64
65 %% Revision string returned by CouchDB (the _rev field).
66 -type rev() :: unicode:unicode_binary().
67
68 %% JSON-serialisable map that becomes the body of a CouchDB document.
69 -type payload() :: maps:map(atom() | unicode:unicode_binary(), value()).
70
71 %% Allowed JSON values used inside a payload().
72 -type value() :: atom()
73 | unicode:unicode_binary()
74 | lists:list(value())
75 | maps:map(atom() | unicode:unicode_binary(), value())
76 .
77
78 %% Callback used by update/3,4. Receives the existing payload() and
79 %% must return the updated one.
80 -type update_function() :: fun((payload())->payload()).
81
82 %% Callback used by upsert/3,4. Receives none when the document is
83 %% missing, or {value, Payload} when it exists, and must return the new
84 %% version that will be stored.
85 -type upsert_function() :: fun((klsn:'maybe'(payload()))->payload()).
86
87 %% @doc
88 %% Create a new database named *Db* on the configured CouchDB server. If
89 %% the database already exists the call is idempotent and still returns
90 %% ok.
91 -spec create_db(db()) -> ok.
92 create_db(Db) ->
93 2 create_db(Db, db_info()).
94
95 %% @doc
96 %% Same as create_db/1 but allows passing a custom connection Info
97 %% record (usually produced by db_info/0).
98 -spec create_db(db(), info()) -> ok.
99 create_db(Db, Info) when is_atom(Db) ->
100 2 create_db(atom_to_binary(Db), Info);
101 create_db(Db0, #{url:=Url0}) ->
102 2 Db1 = klsn_binstr:urlencode(Db0),
103 2 Db = <<"/", Db1/binary>>,
104 2 Url = <<Url0/binary, Db/binary>>,
105 2 Res = httpc:request(put, {Url, []}, [], [{body_format, binary}]),
106 2 case Res of
107 {ok, {{_, Stat, _}, _, _}} when 200=<Stat,Stat=<299 ->
108 1 ok;
109 {ok, {{_, 412, _}, _, _}} ->
110 1 error(exists)
111 end.
112
113
114 %% @doc
115 %% Run a Mango query against Db using the provided Body.
116 %%
117 %% Body must be a JSON-serialisable map compatible with CouchDB's
118 %% /_find endpoint. Returns the list of matching documents from the
119 %% "docs" field.
120 -spec mango_find(db(), map()) -> [payload()].
121 mango_find(Db, Body) ->
122 2 mango_find(Db, Body, db_info()).
123
124 -spec mango_find(db(), map(), info()) -> [payload()].
125 mango_find(Db, Body, Info) when is_atom(Db) ->
126 2 mango_find(atom_to_binary(Db), Body, Info);
127 mango_find(Db0, Body0, #{url := Url0}) ->
128 2 Db1 = klsn_binstr:urlencode(Db0),
129 2 Path = <<"/", Db1/binary, "/_find">>,
130 2 Url = <<Url0/binary, Path/binary>>,
131 2 Payload = jsone:encode(Body0),
132 2 Res = httpc:request(post, {Url, [], "application/json", Payload}, [], [{body_format, binary}]),
133 2 case Res of
134 {ok, {{_, Stat, _}, _, Data}} when 200 =< Stat, Stat =< 299 ->
135 1 #{<<"docs">> := Docs} = jsone:decode(Data),
136 1 Docs;
137 {ok, {{_, 404, _}, _, _}} ->
138 1 error(not_found)
139 end.
140
141
142 %% @doc
143 %% Create a Mango index on Db using the provided Body.
144 %%
145 %% Body must be a JSON-serialisable map for the /_index endpoint.
146 %% Returns the decoded response (e.g. #{"result" := "created"|"exists", ...}).
147 -spec mango_index(db(), map()) -> map().
148 mango_index(Db, Body) ->
149 2 mango_index(Db, Body, db_info()).
150
151 -spec mango_index(db(), map(), info()) -> map().
152 mango_index(Db, Body, Info) when is_atom(Db) ->
153 2 mango_index(atom_to_binary(Db), Body, Info);
154 mango_index(Db0, Body0, #{url := Url0}) ->
155 2 Db1 = klsn_binstr:urlencode(Db0),
156 2 Path = <<"/", Db1/binary, "/_index">>,
157 2 Url = <<Url0/binary, Path/binary>>,
158 2 Payload = jsone:encode(Body0),
159 2 Res = httpc:request(post, {Url, [], "application/json", Payload}, [], [{body_format, binary}]),
160 2 case Res of
161 {ok, {{_, Stat, _}, _, Data}} when 200 =< Stat, Stat =< 299 ->
162 1 jsone:decode(Data);
163 {ok, {{_, 404, _}, _, _}} ->
164 1 error(not_found)
165 end.
166
167
168 %% @doc
169 %% Explain a Mango query plan for Db using the provided Body.
170 %%
171 %% Body matches the /_find request body. Returns the decoded explanation map.
172 -spec mango_explain(db(), map()) -> map().
173 mango_explain(Db, Body) ->
174 2 mango_explain(Db, Body, db_info()).
175
176 -spec mango_explain(db(), map(), info()) -> map().
177 mango_explain(Db, Body, Info) when is_atom(Db) ->
178 2 mango_explain(atom_to_binary(Db), Body, Info);
179 mango_explain(Db0, Body0, #{url := Url0}) ->
180 2 Db1 = klsn_binstr:urlencode(Db0),
181 2 Path = <<"/", Db1/binary, "/_explain">>,
182 2 Url = <<Url0/binary, Path/binary>>,
183 2 Payload = jsone:encode(Body0),
184 2 Res = httpc:request(post, {Url, [], "application/json", Payload}, [], [{body_format, binary}]),
185 2 case Res of
186 {ok, {{_, Stat, _}, _, Data}} when 200 =< Stat, Stat =< 299 ->
187 1 jsone:decode(Data);
188 {ok, {{_, 404, _}, _, _}} ->
189 1 error(not_found)
190 end.
191
192
193 %% @doc
194 %% Insert a new document Data into Db and return the {Id, Rev} pair
195 %% assigned by the server. Convenience wrapper that uses default *Info*.
196 -spec create_doc(db(), payload()) -> {id(), rev()}.
197 create_doc(Db, Data0) ->
198 5 create_doc(Db, Data0, db_info()).
199
200 %% @doc
201 %% Same as create_doc/2 but with explicit Info.
202 -spec create_doc(db(), payload(), info()) -> {id(), rev()}.
203 create_doc(Db, Data0, Info) ->
204 5 Data2 = remove_keys(['_rev', 'C', 'U'], Data0),
205 5 TimeNow = time_now(),
206 5 Data = Data2#{<<"U">>=>TimeNow, <<"C">>=>TimeNow},
207 5 post(Db, Data, Info).
208
209 -spec bulk_create_doc(db(), [payload()]) -> [klsn:'maybe'({id(), rev()})].
210 bulk_create_doc(Db, Docs) ->
211
:-(
bulk_create_doc(Db, Docs, db_info()).
212 -spec bulk_create_doc(db(), [payload()], info()) -> [klsn:'maybe'({id(), rev()})].
213
:-(
bulk_create_doc(_Db, [], _Info) -> [];
214 bulk_create_doc(Db, Docs0, #{url := Url0}) when is_list(Docs0) ->
215
:-(
TimeNow = time_now(),
216
:-(
Docs1 = lists:map(
217 fun(D0) ->
218
:-(
D1 = remove_keys(['_rev', 'C', 'U'], D0),
219
:-(
D1#{<<"U">> => TimeNow, <<"C">> => TimeNow}
220 end,
221 Docs0),
222
:-(
DbBin = klsn_binstr:urlencode(klsn_binstr:from_any(Db)),
223
:-(
Path = <<"/", DbBin/binary, "/_bulk_docs">>,
224
:-(
Url = <<Url0/binary, Path/binary>>,
225
:-(
Body = jsone:encode(#{<<"docs">> => Docs1}),
226
:-(
Res = httpc:request(post, {Url, [], "application/json", Body}, [], [{body_format, binary}]),
227
:-(
case Res of
228 {ok, {{_, Stat, _}, _, Data}} when 200 =< Stat, Stat =< 299 ->
229
:-(
Results = jsone:decode(Data),
230
:-(
lists:map(
231
:-(
fun(#{<<"ok">> := true, <<"id">> := Id, <<"rev">> := Rev}) -> {value, {Id, Rev}};
232
:-(
(_) -> none
233 end,
234 Results);
235 _ ->
236
:-(
lists:duplicate(length(Docs0), none)
237 end.
238
239 %% @doc
240 %% Fetch the document identified by Key from Db or raise error:not_found.
241 -spec get(db(), key()) -> payload().
242 get(Db, Key) ->
243 2 get(Db, Key, db_info()).
244
245 %% @doc
246 %% Return true when the document identified by Key exists in Db, false otherwise.
247 %% Uses HTTP HEAD against CouchDB to avoid transferring the document body.
248 -spec exists(db(), key()) -> boolean().
249 exists(Db, Key) ->
250 4 exists(Db, Key, db_info()).
251
252 %% @doc
253 %% Same as exists/2 but with explicit Info.
254 -spec exists(db(), key(), info()) -> boolean().
255 exists(Db, Key, Info) when is_atom(Db) ->
256 4 exists(atom_to_binary(Db), Key, Info);
257 exists(_, <<>>, _) ->
258 1 false;
259 exists(Db0, {raw, Key0}, #{url := Url0}) ->
260 1 Db1 = klsn_binstr:urlencode(Db0),
261 1 Db = <<"/", Db1/binary>>,
262 1 Key = <<"/", Key0/binary>>,
263 1 Url = <<Url0/binary, Db/binary, Key/binary>>,
264 1 Res = httpc:request(head, {Url, []}, [], [{body_format, binary}]),
265 1 case Res of
266 {ok, {{_, Stat, _}, _, _}} when 200 =< Stat, Stat =< 299 ->
267 1 true;
268 {ok, {{_, 404, _}, _, _}} ->
269
:-(
false;
270 {ok, {{_, Stat, _}, _, _}} ->
271
:-(
error({http_error, Stat})
272 end;
273 exists(Db0, Key0, #{url := Url0}) ->
274 2 Db1 = klsn_binstr:urlencode(Db0),
275 2 Db = <<"/", Db1/binary>>,
276 2 Key1 = klsn_binstr:urlencode(Key0),
277 2 Key = <<"/", Key1/binary>>,
278 2 Url = <<Url0/binary, Db/binary, Key/binary>>,
279 2 Res = httpc:request(head, {Url, []}, [], [{body_format, binary}]),
280 2 case Res of
281 {ok, {{_, Stat, _}, _, _}} when 200 =< Stat, Stat =< 299 ->
282 1 true;
283 {ok, {{_, 404, _}, _, _}} ->
284 1 false;
285 {ok, {{_, Stat, _}, _, _}} ->
286
:-(
error({http_error, Stat})
287 end.
288
289 %% @doc
290 %% Same as get/2 but with explicit Info.
291 -spec get(db(), key(), info()) -> payload().
292 get(Db, Key, Info) ->
293 2 case lookup(Db, Key, Info) of
294 1 {value, Value} -> Value;
295 1 none -> error(not_found)
296 end.
297
298 %% @doc
299 %% Safe variant of get/2. Returns {value, Payload} when the document
300 %% exists or none when it is missing.
301 -spec lookup(db(), key()) -> klsn:'maybe'(payload()).
302 lookup(Db, Key) ->
303 9 lookup(Db, Key, db_info()).
304
305 %% @doc
306 %% Same as lookup/2 but with explicit Info.
307 -spec lookup(db(), key(), info()) -> klsn:'maybe'(payload()).
308 lookup(Db, Key, Info) when is_atom(Db) ->
309 21 lookup(atom_to_binary(Db), Key, Info);
310 lookup(_, <<>>, _) ->
311 1 none;
312 lookup(Db0, {raw, Key0}, #{url:=Url0}) -> % for _design view
313 2 Db1 = klsn_binstr:urlencode(Db0),
314 2 Db = <<"/", Db1/binary>>,
315 2 Key = <<"/", Key0/binary>>,
316 2 Url = <<Url0/binary, Db/binary, Key/binary>>,
317 2 Res = httpc:request(get, {Url, []}, [], [{body_format, binary}]),
318 2 case Res of
319 {ok, {{_, Stat, _}, _, Data}} when 200=<Stat,Stat=<299->
320 1 {value, jsone:decode(Data)};
321 {ok, {{_, 404, _}, _, _}} ->
322 1 none
323 end;
324 lookup(Db0, Key0, #{url:=Url0}) ->
325 18 Db1 = klsn_binstr:urlencode(Db0),
326 18 Db = <<"/", Db1/binary>>,
327 18 Key1 = klsn_binstr:urlencode(Key0),
328 18 Key = <<"/", Key1/binary>>,
329 18 Url = <<Url0/binary, Db/binary, Key/binary>>,
330 18 Res = httpc:request(get, {Url, []}, [], [{body_format, binary}]),
331 18 case Res of
332 {ok, {{_, Stat, _}, _, Data}} when 200=<Stat,Stat=<299->
333 10 {value, jsone:decode(Data)};
334 {ok, {{_, 404, _}, _, _}} ->
335 8 none
336 end.
337
338
339 -spec bulk_lookup(db(), [key()]) -> [klsn:'maybe'(payload())].
340 bulk_lookup(Db, Keys) ->
341 1 bulk_lookup(Db, Keys, db_info()).
342 -spec bulk_lookup(db(), [key()], info()) -> [klsn:'maybe'(payload())].
343
:-(
bulk_lookup(_Db, [], _Info) -> [];
344 bulk_lookup(Db, Keys0, #{url := Url0}) when is_list(Keys0) ->
345 3 Keys = lists:map(fun klsn_binstr:from_any/1, Keys0),
346 3 DbBin = klsn_binstr:urlencode(klsn_binstr:from_any(Db)),
347 3 Path = <<"/", DbBin/binary, "/_all_docs?include_docs=true">>,
348 3 Url = <<Url0/binary, Path/binary>>,
349 3 Body = jsone:encode(#{<<"keys">> => Keys}),
350 3 Res = httpc:request(post, {Url, [], "application/json", Body}, [], [{body_format, binary}]),
351 3 case Res of
352 {ok, {{_, Stat, _}, _, Data}} when 200 =< Stat, Stat =< 299 ->
353 3 #{<<"rows">> := Rows} = jsone:decode(Data),
354 3 lists:map(
355 fun(Row) ->
356 12 case Row of
357 8 #{<<"error">> := _} -> none;
358 4 #{<<"doc">> := Doc} -> {value, Doc}
359 end
360 end,
361 Rows);
362 _ ->
363
:-(
lists:duplicate(length(Keys0), none)
364 end.
365 -spec bulk_upsert(db(), [key()], upsert_function()) -> [payload()].
366 bulk_upsert(Db, Keys, Fun) ->
367 2 bulk_upsert(Db, Keys, Fun, db_info()).
368 -spec bulk_upsert(db(), [key()], upsert_function(), info()) -> [payload()].
369
:-(
bulk_upsert(_Db, [], _Fun, _Info) -> [];
370 bulk_upsert(Db, Keys0, Fun, #{url := Url0} = Info) when is_list(Keys0) ->
371 %% Fetch current documents once
372 2 MaybeDocs = bulk_lookup(Db, Keys0, Info),
373
374 %% Prepare updated/new documents using the callback
375 2 TimeNow = time_now(),
376 2 DocsPrepared = lists:map(
377 fun({Key, MaybeDoc}) ->
378 7 New0 = Fun(MaybeDoc),
379 7 New1 = remove_keys(['_id', 'C', 'U'], New0),
380 7 New2 = New1#{<<"_id">> => klsn_binstr:from_any(Key)},
381 7 New3 = New2#{<<"U">> => TimeNow},
382 7 case MaybeDoc of
383 2 {value, #{<<"C">> := C}} -> New3#{<<"C">> => C};
384 5 _ -> New3#{<<"C">> => TimeNow}
385 end
386 end,
387 lists:zip(Keys0, MaybeDocs)
388 ),
389
390 %% Submit via _bulk_docs
391 2 DbBin = klsn_binstr:urlencode(klsn_binstr:from_any(Db)),
392 2 Path = <<"/", DbBin/binary, "/_bulk_docs">>,
393 2 Url = <<Url0/binary, Path/binary>>,
394 2 Body = jsone:encode(#{<<"docs">> => DocsPrepared}),
395 2 Res = httpc:request(post, {Url, [], "application/json", Body}, [], [{body_format, binary}]),
396
397 2 case Res of
398 {ok, {{_, Stat, _}, _, Data}} when 200 =< Stat, Stat =< 299 ->
399 2 Results = jsone:decode(Data),
400 2 lists:map(
401 fun({Doc0, ResRow}) ->
402 7 Doc = jsone:decode(jsone:encode(Doc0)),
403 7 case ResRow of
404 #{<<"ok">> := true, <<"id">> := Id, <<"rev">> := Rev} ->
405 7 Doc#{<<"_id">> => Id, <<"_rev">> => Rev};
406 #{<<"error">> := _} ->
407 %% Retry single-document upsert that already
408 %% has conflict–handling logic.
409
:-(
KeyBin = maps:get(<<"_id">>, Doc),
410
:-(
upsert(Db, KeyBin, Fun, Info)
411 end
412 end,
413 lists:zip(DocsPrepared, Results)
414 )
415 end.
416
417
418 -spec post(db(), payload(), info()) -> {id(), rev()}.
419 post(Db, Payload, Info) when is_atom(Db) ->
420 13 post(atom_to_binary(Db), Payload, Info);
421 post(Db0, Payload0, #{url:=Url0}) ->
422 13 Db1 = klsn_binstr:urlencode(Db0),
423 13 Db = <<"/", Db1/binary>>,
424 13 Payload = jsone:encode(Payload0),
425 13 Url = <<Url0/binary, Db/binary>>,
426 13 Res = httpc:request(post, {Url, [], "application/json", Payload}, [], [{body_format, binary}]),
427 13 case Res of
428 {ok, {{_, Stat, _}, _, Data}} when 200=<Stat,Stat=<299 ->
429 10 #{<<"ok">>:=true,<<"id">>:=Id,<<"rev">>:=Rev} = jsone:decode(Data),
430 10 {Id, Rev};
431 {ok, {{_, 404, _}, _, _}} ->
432 2 error(not_found);
433 {ok, {{_, 409, _}, _, _}} ->
434 1 error(conflict)
435 end.
436
437 %% @doc
438 %% Read-modify-write helper. Applies *Fun* to the current document and
439 %% stores the result. Fails with error:not_found when the key is absent.
440 -spec update(db(), key(), update_function()) -> payload().
441 update(Db, Key, Fun) ->
442 3 update(Db, Key, Fun, db_info()).
443
444 %% @doc
445 %% Same as update/3 but with explicit Info.
446 -spec update(db(), key(), update_function(), info()) -> payload().
447 update(Db, Key, Fun0, Info) ->
448 3 Fun = fun
449 (none) ->
450 2 error(not_found);
451 ({value, Data}) ->
452 1 Fun0(Data)
453 end,
454 3 upsert_(Db, Key, Fun, Info, 1).
455
456 %% @doc
457 %% Insert or update the document located at Key using Fun. Fun will
458 %% receive none on insert or {value, Old} on update and must return
459 %% the new payload.
460 -spec upsert(db(), key(), upsert_function()) -> payload().
461 upsert(Db, Key, Fun) ->
462 7 upsert(Db, Key, Fun, db_info()).
463
464 %% @doc
465 %% Same as upsert/3 but with explicit Info.
466 -spec upsert(db(), key(), upsert_function(), info()
467 ) -> payload().
468 upsert(_, <<>>, Fun, _) ->
469 1 Fun(none);
470 upsert(Db, Key, Fun, Info) ->
471 6 upsert_(Db, Key, Fun, Info, 1).
472
473 upsert_(_Db, _Key, _Fun, _Info, ReTry) when ReTry >= 10 ->
474
:-(
error(too_many_retry);
475 upsert_(Db, {raw, Key}, Fun, Info, Retry) ->
476 2 upsert_(Db, Key, Fun, Info, Retry);
477 upsert_(Db, Key, Fun, Info, Retry) ->
478 10 MaybeData = lookup(Db, Key, Info),
479 10 Data0 = Fun(MaybeData),
480 8 Data1 = remove_keys(['_id', 'C', 'U'], Data0),
481 8 Data2 = Data1#{<<"_id">>=>Key},
482 8 TimeNow = time_now(),
483 8 Data3 = Data2#{<<"U">>=>TimeNow},
484 8 Data = case MaybeData of
485 3 {value, #{<<"C">>:=C}} -> Data3#{<<"C">>=>C};
486 5 _ -> Data3#{<<"C">>=>TimeNow}
487 end,
488 8 try
489 8 post(Db, Data, Info)
490 of
491 {Id, Rev} ->
492 6 Data#{
493 <<"_id">> => Id
494 , <<"_rev">> => Rev
495 }
496 catch
497 error:conflict ->
498 1 sleep(Retry),
499 1 upsert_(Db, Key, Fun, Info, Retry+1);
500 error:not_found ->
501 1 error(not_found);
502 throw:Error ->
503
:-(
throw(Error);
504 Class:Error:Stack ->
505
:-(
spawn(fun()-> erlang:raise(Class,Error,Stack) end),
506
:-(
sleep(Retry),
507
:-(
upsert_(Db, Key, Fun, Info, Retry+5)
508 end.
509
510
511 %% @doc
512 %% ISO-8601/RFC-3339 timestamp with millisecond precision and a fixed
513 %% +09:00 offset. Used in audit fields C (created) and U (updated).
514 -spec time_now() -> unicode:unicode_binary().
515 time_now() ->
516 16 list_to_binary(calendar:system_time_to_rfc3339(erlang:system_time(millisecond), [{unit, millisecond}, {offset, "+09:00"}])).
517
518 -spec remove_keys([atom()], map()) -> map().
519 remove_keys(Keys, Map) when is_list(Keys), is_map(Map) ->
520 20 lists:foldl(fun(Key, Data0) ->
521 60 Data1 = maps:remove(Key, Data0),
522 60 maps:remove(atom_to_binary(Key), Data1)
523 end, Map, Keys).
524
525
526 %% @doc
527 %% Generate a monotonic-ish unique identifier suitable for use as a CouchDB
528 %% _id. Combines the current Unix time (seconds) with parts of an Erlang
529 %% reference encoded in base-36 so that the resulting IDs sort roughly in
530 %% creation order and stay URL-safe.
531
532 new_id() ->
533 8 Ref = make_ref(),
534 8 Time = erlang:system_time(second),
535 8 Str0 = ref_to_list(Ref),
536 8 [_|Str1] = lists:reverse(Str0),
537 8 Str2 = lists:reverse(Str1),
538 8 [_,A1,A2,A3] = string:split(Str2, ".", all),
539 8 N1 = list_to_integer(A1),
540 8 N2 = list_to_integer(A2),
541 8 N3 = list_to_integer(A3),
542 8 List = lists:flatten([
543 string:casefold(integer_to_list(Time, 36)),
544 "-",
545 string:casefold(integer_to_list(N1, 36)),
546 "-",
547 string:casefold(integer_to_list(N2, 36)),
548 "-",
549 string:casefold(integer_to_list(N3, 36))
550 ]),
551 8 list_to_binary(List).
552
553 db_info() ->
554 41 Url = case os:getenv("COUCHDB_URL") of
555 false ->
556
:-(
<<"http://localhost:5984">>;
557 Str when is_list(Str) ->
558 41 list_to_binary(Str)
559 end,
560 41 #{url=>Url}.
561
562 sleep(Stage) ->
563 1 timer:sleep(round(1000 * rand:uniform() + 100 * math:exp(Stage))).
Line Hits Source