-module(sqid).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/sqid.gleam").
-export([alphabet/1, new/1, set_minimum_length/2, set_blocklist/2, encode/2, decode/2]).
-export_type([alphabet/0, options/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-opaque alphabet() :: {alphabet, gleam@dict:dict(integer(), binary())}.
-opaque options() :: {options, alphabet(), integer(), list(binary())}.
-file("src/sqid.gleam", 406).
?DOC(
" Given a dictionary of letters this returns the codepoint value of the letter\n"
" at the given index.\n"
" This would fail if:\n"
" - the index is not included in the dictionary\n"
" - the dictionary value is an empty string\n"
"\n"
" We only ever use this internally after validating an alphabet and making\n"
" sure that all the entries are single letters, so under this assumption it\n"
" should never fail.\n"
).
-spec letter_value(gleam@dict:dict(integer(), binary()), integer()) -> {ok,
integer()} |
{error, nil}.
letter_value(Letters, Index) ->
_pipe = gleam_stdlib:map_get(Letters, Index),
_pipe@1 = gleam@result:map(_pipe, fun gleam@string:to_utf_codepoints/1),
_pipe@2 = gleam@result:'try'(_pipe@1, fun gleam@list:first/1),
gleam@result:map(_pipe@2, fun gleam_stdlib:identity/1).
-file("src/sqid.gleam", 427).
-spec shuffle_alphabet_loop(
gleam@dict:dict(integer(), binary()),
integer(),
integer()
) -> gleam@dict:dict(integer(), binary()).
shuffle_alphabet_loop(Letters, I, J) ->
case J =< 0 of
true ->
Letters;
false ->
I_value@1 = case letter_value(Letters, I) of
{ok, I_value} -> I_value;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"i always in range"/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"sqid"/utf8>>,
function => <<"shuffle_alphabet_loop"/utf8>>,
line => 435,
value => _assert_fail,
start => 14846,
'end' => 14895,
pattern_start => 14857,
pattern_end => 14868})
end,
J_value@1 = case letter_value(Letters, J) of
{ok, J_value} -> J_value;
_assert_fail@1 ->
erlang:error(#{gleam_error => let_assert,
message => <<"j always in range"/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"sqid"/utf8>>,
function => <<"shuffle_alphabet_loop"/utf8>>,
line => 436,
value => _assert_fail@1,
start => 14925,
'end' => 14974,
pattern_start => 14936,
pattern_end => 14947})
end,
R = case maps:size(Letters) of
0 -> 0;
Gleam@denominator -> (((I * J) + I_value@1) + J_value@1) rem Gleam@denominator
end,
Old_i@1 = case gleam_stdlib:map_get(Letters, I) of
{ok, Old_i} -> Old_i;
_assert_fail@2 ->
erlang:error(#{gleam_error => let_assert,
message => <<"i always in range"/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"sqid"/utf8>>,
function => <<"shuffle_alphabet_loop"/utf8>>,
line => 439,
value => _assert_fail@2,
start => 15070,
'end' => 15113,
pattern_start => 15081,
pattern_end => 15090})
end,
Old_r@1 = case gleam_stdlib:map_get(Letters, R) of
{ok, Old_r} -> Old_r;
_assert_fail@3 ->
erlang:error(#{gleam_error => let_assert,
message => <<"r always in range"/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"sqid"/utf8>>,
function => <<"shuffle_alphabet_loop"/utf8>>,
line => 440,
value => _assert_fail@3,
start => 15143,
'end' => 15186,
pattern_start => 15154,
pattern_end => 15163})
end,
Letters@1 = begin
_pipe = gleam@dict:insert(Letters, I, Old_r@1),
gleam@dict:insert(_pipe, R, Old_i@1)
end,
shuffle_alphabet_loop(Letters@1, I + 1, J - 1)
end.
-file("src/sqid.gleam", 421).
?DOC(
" Shuffles an alphabet.\n"
" Don't be fooled by the name: the shuffle might look random, but it's\n"
" actually deterministic. Given an alphabet its shuffled version is always\n"
" going to be the same.\n"
).
-spec shuffle_alphabet(alphabet()) -> alphabet().
shuffle_alphabet(Alphabet) ->
Letters = shuffle_alphabet_loop(
erlang:element(2, Alphabet),
0,
maps:size(erlang:element(2, Alphabet)) - 1
),
{alphabet, Letters}.
-file("src/sqid.gleam", 96).
-spec is_multi_byte(binary()) -> boolean().
is_multi_byte(String) ->
erlang:byte_size(String) > 1.
-file("src/sqid.gleam", 104).
-spec has_duplicates_loop(list(DKX), gleam@set:set(DKX)) -> boolean().
has_duplicates_loop(List, Seen_so_far) ->
case List of
[] ->
false;
[First | Rest] ->
case gleam@set:contains(Seen_so_far, First) of
true ->
true;
false ->
has_duplicates_loop(
Rest,
gleam@set:insert(Seen_so_far, First)
)
end
end.
-file("src/sqid.gleam", 100).
-spec has_duplicates(list(any())) -> boolean().
has_duplicates(List) ->
has_duplicates_loop(List, gleam@set:new()).
-file("src/sqid.gleam", 79).
?DOC(
" Creates a new alphabet from the letters making up the given string.\n"
" An alphabet is what defines what characters can end up in a generated Sqid.\n"
"\n"
" This function will fail if:\n"
" - The alphabet has less than 3 characters\n"
" - The alphabet contains repeated characters\n"
" - The alphabet has multi-byte characters\n"
"\n"
" ## Examples\n"
"\n"
" If you want Sqids that only contains lowercase letters you can define an\n"
" alphabet like this:\n"
"\n"
" ```gleam\n"
" let assert Ok(alphabet) =\n"
" sqid.alphabet(\"abcdefghijklmnopqrstuvwxyz\")\n"
" ```\n"
"\n"
" ```gleam\n"
" assert Error(Nil) == sqid.alphabet(\"wibble\")\n"
" as \"wrong alphabet: it contains duplicates\"\n"
"\n"
" assert Error(Nil) == sqid.alphabet(\"dziękuję\")\n"
" as \"wrong alphabet: it contains non ascii characters\"\n"
"\n"
" assert Error(Nil) == sqid.alphabet(\"01\")\n"
" as \"wrong alphabet: it contains less than 3 characters\"\n"
" ```\n"
).
-spec alphabet(binary()) -> {ok, alphabet()} | {error, nil}.
alphabet(String) ->
Letters = gleam@string:to_graphemes(String),
gleam@bool:guard(
has_duplicates(Letters),
{error, nil},
fun() ->
gleam@bool:guard(
gleam@list:any(Letters, fun is_multi_byte/1),
{error, nil},
fun() -> case Letters of
[] ->
{error, nil};
[_] ->
{error, nil};
[_, _] ->
{error, nil};
_ ->
Letters@1 = gleam@list:index_fold(
Letters,
maps:new(),
fun(Acc, Letter, Index) ->
gleam@dict:insert(Acc, Index, Letter)
end
),
{ok, shuffle_alphabet({alphabet, Letters@1})}
end end
)
end
).
-file("src/sqid.gleam", 124).
?DOC(
" Creates an `Options` object from a valid alphabet.\n"
" `Options` determine how Sqids are encoded and decoded.\n"
"\n"
" If you want to change the generated Sqids you can also use:\n"
" - [`set_minimum_length`](#set_minimum_length): to pick a minimum length for\n"
" the generated Sqids.\n"
" - [`set_blocklist`](#set_blocklist): to specify a blocklist of words that\n"
" will not end up in any of the generated Sqids.\n"
).
-spec new(alphabet()) -> options().
new(Alphabet) ->
{options, Alphabet, 0, []}.
-file("src/sqid.gleam", 132).
?DOC(
" Sets the minimum length of the generated Sqids.\n"
" The value must be between 0 and 255. Any value lower than 0 is considered\n"
" as 0, while any value higher than 255 is considered as 255.\n"
).
-spec set_minimum_length(options(), integer()) -> options().
set_minimum_length(Options, Minimum_length) ->
{options,
erlang:element(2, Options),
gleam@int:clamp(Minimum_length, 0, 255),
erlang:element(4, Options)}.
-file("src/sqid.gleam", 459).
-spec alphabet_to_string(alphabet()) -> binary().
alphabet_to_string(Alphabet) ->
_pipe = maps:to_list(erlang:element(2, Alphabet)),
_pipe@1 = gleam@list:sort(
_pipe,
fun(One, Other) ->
gleam@int:compare(erlang:element(1, One), erlang:element(1, Other))
end
),
_pipe@2 = gleam@list:map(_pipe@1, fun(Pair) -> erlang:element(2, Pair) end),
gleam@string:join(_pipe@2, <<""/utf8>>).
-file("src/sqid.gleam", 151).
?DOC(
" Sets the list of blocked words that cannot appear inside any of the\n"
" generated Sqids.\n"
" This is useful if you want to make sure a generated Sqid cannot contain\n"
" profanities.\n"
"\n"
" There's a couple of things to note:\n"
" - The blocklist is case insensitive. If `\"gleam\"` is a blocked word,\n"
" generated Sqids will not contain any of `\"gLeaM\"`, `\"GLEAM\"`, `\"GLeam\"`,\n"
" ... and so on.\n"
" - All words in the blocklist must be more than three characters long, any\n"
" word shorter than that will be ignored.\n"
).
-spec set_blocklist(options(), list(binary())) -> options().
set_blocklist(Options, Blocklist) ->
Alphabet_string = alphabet_to_string(erlang:element(2, Options)),
Blocklist@2 = gleam@list:fold(
Blocklist,
[],
fun(Blocklist@1, Word) ->
Word@1 = string:lowercase(Word),
case gleam@string:to_graphemes(Word@1) of
[] ->
Blocklist@1;
[_] ->
Blocklist@1;
[_, _] ->
Blocklist@1;
Letters ->
case gleam@list:all(
Letters,
fun(_capture) ->
gleam_stdlib:contains_string(
Alphabet_string,
_capture
)
end
) of
true ->
[Word@1 | Blocklist@1];
false ->
Blocklist@1
end
end
end
),
{options,
erlang:element(2, Options),
erlang:element(3, Options),
Blocklist@2}.
-file("src/sqid.gleam", 228).
?DOC(
" Returns true if the id contains any of the words that are in the options'\n"
" block list.\n"
" Checking is case insensitive: if `\"gleam\"` is not allowed then `\"GLeaM\"`\n"
" would be rejected as well.\n"
).
-spec is_blocked_id(binary(), options()) -> boolean().
is_blocked_id(Id, Options) ->
Id@1 = string:lowercase(Id),
Id_size = erlang:byte_size(Id@1),
gleam@list:any(
erlang:element(4, Options),
fun(Blocked_word) ->
Blocked_word_size = erlang:byte_size(Blocked_word),
case Blocked_word_size > Id_size of
true ->
false;
false when (Id_size =< 3) orelse (Blocked_word_size =< 3) ->
Id@1 =:= Blocked_word;
false ->
gleam_stdlib:contains_string(Id@1, Blocked_word)
end
end
).
-file("src/sqid.gleam", 310).
-spec pad_to_length_loop(binary(), integer(), alphabet()) -> binary().
pad_to_length_loop(Id, Missing, Alphabet) ->
case Missing > 0 of
false ->
Id;
true ->
Alphabet@1 = shuffle_alphabet(Alphabet),
Alphabet_size = maps:size(erlang:element(2, Alphabet@1)),
Slice_size = gleam@int:min(Missing, Alphabet_size),
Slice = gleam@string:slice(
alphabet_to_string(Alphabet@1),
0,
Slice_size
),
Id@1 = <<Id/binary, Slice/binary>>,
Missing@1 = Missing - Slice_size,
pad_to_length_loop(Id@1, Missing@1, Alphabet@1)
end.
-file("src/sqid.gleam", 327).
?DOC(
" Returns the separator of the alphabet, the separator changes based on how\n"
" the\n"
).
-spec alphabet_separator(alphabet()) -> binary().
alphabet_separator(Alphabet) ->
Separator@1 = case gleam_stdlib:map_get(erlang:element(2, Alphabet), 0) of
{ok, Separator} -> Separator;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"empty alphabet"/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"sqid"/utf8>>,
function => <<"alphabet_separator"/utf8>>,
line => 328,
value => _assert_fail,
start => 11196,
'end' => 11252,
pattern_start => 11207,
pattern_end => 11220})
end,
Separator@1.
-file("src/sqid.gleam", 298).
-spec pad_to_length(binary(), integer(), alphabet()) -> binary().
pad_to_length(Id, Length, Alphabet) ->
Id_length = erlang:byte_size(Id),
case Id_length < Length of
false ->
Id;
true ->
Id@1 = <<Id/binary, (alphabet_separator(Alphabet))/binary>>,
Missing = (Length - Id_length) - 1,
pad_to_length_loop(Id@1, Missing, Alphabet)
end.
-file("src/sqid.gleam", 271).
-spec number_to_id_loop(integer(), alphabet(), list(binary())) -> binary().
number_to_id_loop(Number, Alphabet, Acc) ->
Alphabet_size = maps:size(erlang:element(2, Alphabet)) - 1,
Letter@1 = case gleam_stdlib:map_get(
erlang:element(2, Alphabet),
(case Alphabet_size of
0 -> 0;
Gleam@denominator -> Number rem Gleam@denominator
end) + 1
) of
{ok, Letter} -> Letter;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"sqid"/utf8>>,
function => <<"number_to_id_loop"/utf8>>,
line => 281,
value => _assert_fail,
start => 9723,
'end' => 9809,
pattern_start => 9734,
pattern_end => 9744})
end,
Acc@1 = [Letter@1 | Acc],
Number@1 = begin
_pipe = math:floor(case erlang:float(Alphabet_size) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator@1 -> erlang:float(Number) / Gleam@denominator@1
end),
erlang:round(_pipe)
end,
case Number@1 > 0 of
true ->
number_to_id_loop(Number@1, Alphabet, Acc@1);
false ->
gleam@string:join(Acc@1, <<""/utf8>>)
end.
-file("src/sqid.gleam", 267).
-spec number_to_id(integer(), alphabet()) -> binary().
number_to_id(Number, Alphabet) ->
number_to_id_loop(Number, Alphabet, []).
-file("src/sqid.gleam", 247).
-spec encode_numbers(list(integer()), alphabet(), binary()) -> {binary(),
alphabet()}.
encode_numbers(Numbers, Alphabet, Id) ->
case Numbers of
[] ->
{Id, Alphabet};
[Last] ->
{<<Id/binary, (number_to_id(Last, Alphabet))/binary>>, Alphabet};
[First | Rest] ->
Id@1 = <<Id/binary, (number_to_id(First, Alphabet))/binary>>,
Id@2 = <<Id@1/binary, (alphabet_separator(Alphabet))/binary>>,
Alphabet@1 = shuffle_alphabet(Alphabet),
encode_numbers(Rest, Alphabet@1, Id@2)
end.
-file("src/sqid.gleam", 449).
?DOC(
" This reverses an alphabet: if the initial alphabet is \"abcd\", the reversed\n"
" alphabet is going to be \"dcba\".\n"
).
-spec reverse_alphabet(alphabet()) -> alphabet().
reverse_alphabet(Alphabet) ->
Size = maps:size(erlang:element(2, Alphabet)),
Letters@1 = gleam@dict:fold(
erlang:element(2, Alphabet),
maps:new(),
fun(Letters, Index, Letter) ->
gleam@dict:insert(Letters, (Size - 1) - Index, Letter)
end
),
{alphabet, Letters@1}.
-file("src/sqid.gleam", 336).
?DOC(
" Shifts the alphabet by the given offset.\n"
" For example if alphabet is `\"abcde\"` and I end up shifting it by 2 to the\n"
" left it ends up being `\"cdeab\"`.\n"
).
-spec shift_alphabet(alphabet(), integer()) -> alphabet().
shift_alphabet(Alphabet, Offset) ->
Alphabet_size = maps:size(erlang:element(2, Alphabet)),
Letters@1 = gleam@dict:fold(
erlang:element(2, Alphabet),
maps:new(),
fun(Letters, Index, Letter) ->
New_index = case Index < Offset of
true ->
(Alphabet_size - Offset) + Index;
false ->
Index - Offset
end,
gleam@dict:insert(Letters, New_index, Letter)
end
),
{alphabet, Letters@1}.
-file("src/sqid.gleam", 376).
?DOC(
" This computes an offset from a list of numbers, the Sqid spec calls this\n"
" \"semi random\": it's random looking but it's actually deterministically\n"
" decided by the numbers and alphabet we pass as arguments.\n"
" This returns an error if any of the numbers is negative.\n"
).
-spec semi_random_offset(alphabet(), list(integer())) -> {ok, integer()} |
{error, nil}.
semi_random_offset(Alphabet, Numbers) ->
Alphabet_size = maps:size(erlang:element(2, Alphabet)),
_pipe = gleam@list:try_fold(
Numbers,
{erlang:length(Numbers), 0},
fun(Pair, Number) ->
{Acc, Index} = Pair,
case Number < 0 of
true ->
{error, nil};
false ->
Letter_value@1 = case letter_value(
erlang:element(2, Alphabet),
case Alphabet_size of
0 -> 0;
Gleam@denominator -> Number rem Gleam@denominator
end
) of
{ok, Letter_value} -> Letter_value;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"index outside of dictionary"/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"sqid"/utf8>>,
function => <<"semi_random_offset"/utf8>>,
line => 386,
value => _assert_fail,
start => 13405,
'end' => 13499,
pattern_start => 13416,
pattern_end => 13432})
end,
{ok, {(Letter_value@1 + Index) + Acc, Index + 1}}
end
end
),
gleam@result:map(_pipe, fun(Pair@1) -> erlang:element(1, Pair@1) end).
-file("src/sqid.gleam", 185).
-spec do_encode(options(), list(integer()), integer()) -> {ok, binary()} |
{error, nil}.
do_encode(Options, Numbers, Attempts) ->
case Attempts >= maps:size(erlang:element(2, erlang:element(2, Options))) of
true ->
{error, nil};
false ->
Alphabet_size = maps:size(
erlang:element(2, erlang:element(2, Options))
),
gleam@result:'try'(
semi_random_offset(erlang:element(2, Options), Numbers),
fun(Offset) ->
Offset@1 = case Alphabet_size of
0 -> 0;
Gleam@denominator -> Offset rem Gleam@denominator
end,
Offset@2 = case Alphabet_size of
0 -> 0;
Gleam@denominator@1 -> (Offset@1 + Attempts) rem Gleam@denominator@1
end,
Alphabet = shift_alphabet(
erlang:element(2, Options),
Offset@2
),
Id = alphabet_separator(Alphabet),
Alphabet@1 = reverse_alphabet(Alphabet),
{Id@1, Alphabet@2} = encode_numbers(Numbers, Alphabet@1, Id),
Id@2 = pad_to_length(
Id@1,
erlang:element(3, Options),
Alphabet@2
),
case is_blocked_id(Id@2, Options) of
true ->
do_encode(Options, Numbers, Attempts + 1);
false ->
{ok, Id@2}
end
end
)
end.
-file("src/sqid.gleam", 178).
?DOC(
" Encodes a list of numbers into a readable string using the given options.\n"
" This function might fail if it's not possible to generate a string that is\n"
" not excluded by the options' blocklist.\n"
).
-spec encode(options(), list(integer())) -> {ok, binary()} | {error, nil}.
encode(Options, Numbers) ->
case Numbers of
[] ->
{ok, <<""/utf8>>};
_ ->
do_encode(Options, Numbers, 0)
end.
-file("src/sqid.gleam", 528).
-spec index(binary(), binary()) -> {ok, integer()} | {error, nil}.
index(String, Separator) ->
gleam@result:'try'(
gleam@string:split_once(String, Separator),
fun(_use0) ->
{Before, _} = _use0,
{ok, erlang:byte_size(Before)}
end
).
-file("src/sqid.gleam", 518).
-spec id_to_number(binary(), alphabet()) -> {ok, integer()} | {error, nil}.
id_to_number(Id, Alphabet) ->
Alphabet_size = maps:size(erlang:element(2, Alphabet)) - 1,
Alphabet_string = alphabet_to_string(Alphabet),
gleam@list:try_fold(
gleam@string:to_graphemes(Id),
0,
fun(Acc, Letter) ->
gleam@result:'try'(
index(Alphabet_string, Letter),
fun(Index) -> {ok, ((Acc * Alphabet_size) + Index) - 1} end
)
end
).
-file("src/sqid.gleam", 494).
-spec decode_loop(alphabet(), binary(), list(integer())) -> {ok,
list(integer())} |
{error, nil}.
decode_loop(Alphabet, Sqid, Numbers) ->
Separator = alphabet_separator(Alphabet),
case gleam@string:split(Sqid, Separator) of
[] ->
{ok, lists:reverse(Numbers)};
[<<""/utf8>> | _] ->
{ok, lists:reverse(Numbers)};
[Id | Rest] ->
case id_to_number(Id, Alphabet) of
{error, _} ->
{error, nil};
{ok, Number} ->
Numbers@1 = [Number | Numbers],
Alphabet@1 = shuffle_alphabet(Alphabet),
Sqid@1 = gleam@string:join(Rest, Separator),
decode_loop(Alphabet@1, Sqid@1, Numbers@1)
end
end.
-file("src/sqid.gleam", 475).
?DOC(
" Decodes a Sqid into the list of integers that was used to generate it.\n"
" To decode a Sqid, always use the same options that were used to encode it,\n"
" otherwise you will get nonsense results!\n"
"\n"
" This function will fail if the Sqid has carachters that are not allowed by\n"
" these options, meaning that the Sqid was encoded with a different alphabet.\n"
).
-spec decode(options(), binary()) -> {ok, list(integer())} | {error, nil}.
decode(Options, Sqid) ->
case gleam_stdlib:string_pop_grapheme(Sqid) of
{error, _} ->
{ok, []};
{ok, {Separator, Sqid@1}} ->
case index(
alphabet_to_string(erlang:element(2, Options)),
Separator
) of
{error, _} ->
{error, nil};
{ok, Offset} ->
_pipe = shift_alphabet(erlang:element(2, Options), Offset),
_pipe@1 = reverse_alphabet(_pipe),
decode_loop(_pipe@1, Sqid@1, [])
end
end.