-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 => <>, 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 => <>, 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 => <>, 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 => <>, 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 = <>, 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 => <>, 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 = <>, 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 => <>, 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] -> {<>, Alphabet}; [First | Rest] -> Id@1 = <>, Id@2 = <>, 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 => <>, 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.