Current section

Files

Jump to
ids src ids@ulid.erl
Raw

src/ids@ulid.erl

-module(ids@ulid).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]).
-export([monotonic_generate/1, monotonic_from_timestamp/2, from_parts/2, from_timestamp/1, generate/0, decode/1, start/0]).
-export_type([message/0, state/0]).
-opaque message() :: {generate,
gleam@erlang@process:subject({ok, binary()} | {error, binary()})} |
{generate_from_timestamp,
integer(),
gleam@erlang@process:subject({ok, binary()} | {error, binary()})}.
-opaque state() :: {state, integer(), bitstring()}.
-spec monotonic_generate(gleam@erlang@process:subject(message())) -> {ok,
binary()} |
{error, binary()}.
monotonic_generate(Channel) ->
gleam@otp@actor:call(Channel, fun(Field@0) -> {generate, Field@0} end, 1000).
-spec monotonic_from_timestamp(
gleam@erlang@process:subject(message()),
integer()
) -> {ok, binary()} | {error, binary()}.
monotonic_from_timestamp(Channel, Timestamp) ->
gleam@otp@actor:call(
Channel,
fun(Subject) -> {generate_from_timestamp, Timestamp, Subject} end,
1000
).
-spec encode_bytes(bitstring()) -> binary().
encode_bytes(Binary) ->
case Binary of
<<Index:5/unsigned, Rest/bitstring>> ->
_pipe = <<"0123456789ABCDEFGHJKMNPQRSTVWXYZ"/utf8>>,
_pipe@1 = gleam@string:to_graphemes(_pipe),
_pipe@2 = gleam@list:at(_pipe@1, Index),
_pipe@3 = gleam@result:unwrap(_pipe@2, <<"0"/utf8>>),
gleam@string:append(_pipe@3, encode_bytes(Rest));
<<>> ->
<<""/utf8>>
end.
-spec encode_base32(bitstring()) -> binary().
encode_base32(Bytes) ->
To_pad = begin
_pipe = Bytes,
_pipe@1 = erlang:bit_size(_pipe),
_pipe@2 = gleam@int:modulo(_pipe@1, 5),
_pipe@3 = gleam@result:unwrap(_pipe@2, 5),
_pipe@4 = gleam@int:subtract(_pipe@3, 5),
_pipe@5 = gleam@int:absolute_value(_pipe@4),
_pipe@6 = gleam@int:modulo(_pipe@5, 5),
gleam@result:unwrap(_pipe@6, 0)
end,
encode_bytes(<<0:(lists:max([(To_pad), 0])), Bytes/bitstring>>).
-spec from_parts(integer(), bitstring()) -> {ok, binary()} | {error, binary()}.
from_parts(Timestamp, Randomness) ->
case {Timestamp, Randomness} of
{Time, <<Rand:80/bitstring>>} when Time =< 281474976710655 ->
_pipe = <<Timestamp:48, Rand/bitstring>>,
_pipe@1 = encode_base32(_pipe),
{ok, _pipe@1};
_ ->
Error = gleam@string:concat(
[<<"Error: The timestamp is too large or randomness isn't 80 bits. Please use an Unix timestamp smaller than "/utf8>>,
gleam@int:to_string(281474976710655),
<<"."/utf8>>]
),
{error, Error}
end.
-spec from_timestamp(integer()) -> {ok, binary()} | {error, binary()}.
from_timestamp(Timestamp) ->
from_parts(Timestamp, crypto:strong_rand_bytes(10)).
-spec generate() -> binary().
generate() ->
Timestamp = os:system_time(millisecond),
case from_timestamp(Timestamp) of
{ok, Ulid} ->
Ulid;
_ ->
erlang:error(#{gleam_error => panic,
message => <<"Error: Couldn't generate ULID."/utf8>>,
module => <<"ids/ulid"/utf8>>,
function => <<"generate"/utf8>>,
line => 89})
end.
-spec decode_base32(binary()) -> {ok, bitstring()} | {error, nil}.
decode_base32(Binary) ->
Crockford_with_index = begin
_pipe = <<"0123456789ABCDEFGHJKMNPQRSTVWXYZ"/utf8>>,
_pipe@1 = gleam@string:to_graphemes(_pipe),
gleam@list:index_map(_pipe@1, fun(I, X) -> {X, I} end)
end,
Bits = begin
_pipe@2 = Binary,
_pipe@3 = gleam@string:to_graphemes(_pipe@2),
gleam@list:fold(
_pipe@3,
<<>>,
fun(Acc, C) ->
Index = begin
_pipe@4 = Crockford_with_index,
_pipe@5 = gleam@list:key_find(_pipe@4, C),
gleam@result:unwrap(_pipe@5, 0)
end,
<<Acc/bitstring, Index:5>>
end
)
end,
Padding = begin
_pipe@6 = Bits,
_pipe@7 = erlang:bit_size(_pipe@6),
_pipe@8 = gleam@int:modulo(_pipe@7, 8),
gleam@result:unwrap(_pipe@8, 0)
end,
case Bits of
<<0:Padding, Res/bitstring>> ->
{ok, Res};
_ ->
{error, nil}
end.
-spec decode(binary()) -> {ok, {integer(), bitstring()}} | {error, binary()}.
decode(Ulid) ->
case decode_base32(Ulid) of
{ok, <<Timestamp:48/unsigned, Randomness:80/bitstring>>} ->
{ok, {Timestamp, Randomness}};
_ ->
{error,
<<"Error: Decoding failed. Is a valid ULID being supplied?"/utf8>>}
end.
-spec generate_response_ulid(
integer(),
gleam@erlang@process:subject({ok, binary()} | {error, binary()}),
state()
) -> gleam@otp@actor:next(message(), state()).
generate_response_ulid(Timestamp, Reply, State) ->
case erlang:element(2, State) =:= Timestamp of
true ->
Randomness = begin
_pipe = erlang:element(3, State),
_pipe@1 = binary:decode_unsigned(_pipe),
_pipe@2 = gleam@int:add(_pipe@1, 1),
binary:encode_unsigned(_pipe@2)
end,
case from_parts(Timestamp, Randomness) of
{ok, Ulid} ->
gleam@otp@actor:send(Reply, {ok, Ulid});
_ ->
gleam@otp@actor:send(
Reply,
{error, <<"Error: Couldn't generate ULID."/utf8>>}
)
end,
gleam@otp@actor:continue({state, Timestamp, Randomness});
false ->
Randomness@1 = crypto:strong_rand_bytes(10),
case from_parts(Timestamp, Randomness@1) of
{ok, Ulid@1} ->
gleam@otp@actor:send(Reply, {ok, Ulid@1});
_ ->
gleam@otp@actor:send(
Reply,
{error, <<"Error: Couldn't generate ULID."/utf8>>}
)
end,
gleam@otp@actor:continue({state, Timestamp, Randomness@1})
end.
-spec handle_msg(message(), state()) -> gleam@otp@actor:next(message(), state()).
handle_msg(Msg, State) ->
case Msg of
{generate, Reply} ->
_pipe = os:system_time(millisecond),
generate_response_ulid(_pipe, Reply, State);
{generate_from_timestamp, Timestamp, Reply@1} ->
_pipe@1 = Timestamp,
generate_response_ulid(_pipe@1, Reply@1, State)
end.
-spec start() -> {ok, gleam@erlang@process:subject(message())} |
{error, gleam@otp@actor:start_error()}.
start() ->
gleam@otp@actor:start({state, 0, <<>>}, fun handle_msg/2).