src/ehashids.erl

-module(ehashids).

-export([
    new/0,
    new/1,
    new/2,
    new/3,
    default_alphabet/0,
    default_salt/0,
    default_separators/0,
    min_hash_length/0,
    min_alphabet_length/0,
    estimate_encoded_size/2,
    encode/2,
    encode_one/2,
    decode/2,
    decode_safe/2,
    compile/1,
    from_compiled/1
]).
-on_load(init/0).

-define(APPNAME, ehashids).
-define(LIBNAME, ehashids).


-opaque hashids_ref() :: binary().
-export_type([hashids_ref/0]).
-opaque compiled_hashids_ref() :: tuple().
-export_type([compiled_hashids_ref/0]).

%% @doc Returns the default hashids alphabet.
%% @end
-spec default_alphabet() -> binary().
default_alphabet() -> <<"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890">>.

%% @doc Returns the default hashids salt.
%% @end
-spec default_salt() -> binary().
default_salt() -> <<"">>.

%% @doc Returns the default hashids separators.
%% @end
-spec default_separators() -> binary().
default_separators() -> <<"cfhistuCFHISTU">>.

%% @doc Returns the default hashids minimum alphabet length.
%% @end
-spec min_alphabet_length() -> pos_integer().
min_alphabet_length() -> 16.

%% @doc Returns the default hashids minimum hash length.
%% @end
-spec min_hash_length() -> non_neg_integer().
min_hash_length() -> 0.

%% @doc Creates a new hashids context. With the default parameters.
%% <p>IMPORTANT do not share these references between processes.
%% This can cause stability issues. Avoiding the shuffle overhead
%% at initialization can be accomplished by caching the compiled
%% output of the reference using the `compile/1' function and then
%% recreating the reference in each process.
%% </p>
%% @end
-spec new() -> hashids_ref().
new() ->
    not_loaded(?LINE).

%% @doc Creates a new hashids context. With the default parameters
%% but specify a custom salt.
%% <p>IMPORTANT do not share these references between processes.
%% This can cause stability issues. Avoiding the shuffle overhead
%% at initialization can be accomplished by caching the compiled
%% output of the reference using the `compile/1' function and then
%% recreating the reference in each process.
%% </p>
%% @end
-spec new(Salt :: binary()) -> hashids_ref() | {error, Reason :: atom()}.
new(_Salt) ->
    not_loaded(?LINE).

%% @doc Creates a new hashids context. With the default parameters
%% but specify a custom salt and minimum hash length.
%% <p>IMPORTANT do not share these references between processes.
%% This can cause stability issues. Avoiding the shuffle overhead
%% at initialization can be accomplished by caching the compiled
%% output of the reference using the `compile/1' function and then
%% recreating the reference in each process.
%% </p>
%% @end
-spec new(Salt :: binary(), MinHashLen :: non_neg_integer()) -> hashids_ref() | {error, Reason :: atom()}.
new(_Salt, _MinHashLen) ->
    not_loaded(?LINE).

%% @doc Creates a new hashids context. With the default parameters
%% but specify a custom salt and minimum hash length as well as
%% a custom alphabet.
%% <p>Keep in mind that the alphabet only supports the ASCII
%% character set. If this a problem consider adding UTF-8 support
%% to the underlying library.
%% </p>
%% <p>IMPORTANT do not share these references between processes.
%% This can cause stability issues. Avoiding the shuffle overhead
%% at initialization can be accomplished by caching the compiled
%% output of the reference using the `compile/1' function and then
%% recreating the reference in each process.
%% </p>
%% @end
-spec new(Salt :: binary(), MinHashLen :: non_neg_integer(), Alphabet :: binary()) ->
    hashids_ref() | {error, Reason :: atom()}.
new(_Salt, _MinHashLen, _Alphabet) ->
    not_loaded(?LINE).

%% @doc Estimate the size of the encoding for an input (for REPL use only).
%% @end
-spec estimate_encoded_size(Ref :: hashids_ref(), list(non_neg_integer())) ->
    {ok, Size :: non_neg_integer()} | {error, atom()}.
estimate_encoded_size(_Ref, _Numbers) ->
    not_loaded(?LINE).

%% @doc Encode a list of numbers into a hash id.
%% <p>`Numbers' here are 64 bit unsigned integers internally larger
%% integers will cause an error. For larger values multiple numbers
%% can be used instead.
%% </p>
%% @end
-spec encode(Ref :: hashids_ref(), Numbers :: list(non_neg_integer())) -> {ok, Id :: binary()} | {error, atom()}.
encode(_Ref, _Numbers) ->
    not_loaded(?LINE).

%% @doc Encode a number into a hash id.
%% <p>`Number' here is a 64 bit unsigned integer internally larger
%% values will cause an error. For larger values multiple numbers
%% can be used instead using the `encode/2' function.
%% </p>
%% @end
-spec encode_one(Ref :: hashids_ref(), Number :: non_neg_integer()) -> {ok, Id :: binary()} | {error, atom()}.
encode_one(Ref, Number) -> encode(Ref, [Number]).

%% @doc Decode a hash id into a list of numbers.
%% <p>Note that decode here is not safe the Id can contain additional
%% invalid data. If this is a problem use the equivalent `decode_safe/2'
%% function.
%% </p>
%% @end
-spec decode(Ref :: hashids_ref(), Id :: binary()) -> {ok, Numbers :: list(non_neg_integer())} | {error, atom()}.
decode(_Ref, _Id) ->
    not_loaded(?LINE).

%% @doc Decode a hash id into a list of numbers in a safe way.
%% <p>This function decodes and also checks if the result is
%% commutative with `encode/2'.
%% </p>
%% @end
-spec decode_safe(Ref :: hashids_ref(), Id :: binary()) ->
    {ok, Numbers :: list(non_neg_integer())} | {error, unsafe} | {error, atom()}.
decode_safe(Ref, Id0) ->
    case decode(Ref, Id0) of
        {ok, Numbers} ->
            {ok, Id1} = encode(Ref, Numbers),
            case Id0 =:= Id1 of
                true -> {ok, Numbers};
                false -> {error, unsafe}
            end;
        Error -> Error
    end.

%% @doc Encode the `hashids_ref()' data in to a tuple to be cached.
%% <p>This data needs to be treated as opaque changing the values
%% can be dangerous!
%% </p>
%% @end
-spec compile(Ref :: hashids_ref()) -> {ok, compiled_hashids_ref()} | {error, atom()}.
compile(_Ref) ->
    not_loaded(?LINE).

%% @doc Create a new `hashids_ref()' from cached data avoiding shuffling.
%% <p>The compiled data needs to be treated as opaque changing the values
%% can be dangerous! Only use an input produced by the `compile/1' function.
%% </p>
%% @end
-spec from_compiled(CompiledData :: compiled_hashids_ref()) -> hashids_ref() | {error, atom()}.
from_compiled(_CompiledData) ->
    not_loaded(?LINE).

%% internal - loads the NIF .so file
init() ->
    SoName = case code:priv_dir(?APPNAME) of
        {error, bad_name} ->
            case filelib:is_dir(filename:join(["..", priv])) of
                true ->
                    filename:join(["..", priv, ?LIBNAME]);
                _ ->
                    filename:join([priv, ?LIBNAME])
            end;
        Dir ->
            filename:join(Dir, ?LIBNAME)
    end,
    erlang:load_nif(SoName, 0).

%% internal - returns an error if the implementation is not loaded
not_loaded(Line) ->
    erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, Line}]}).