├── rebar.lock ├── .gitignore ├── rebar.config ├── .github └── workflows │ └── main.yaml ├── CHANGELOG.md ├── LICENSE.txt ├── src ├── ksuid.app.src ├── ksuid_base62.erl └── ksuid.erl ├── GNUmakefile ├── README.md ├── test ├── ksuid_base62_tests.erl └── ksuid_tests.erl └── doc └── handbook.md /rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | *.beam 3 | *.crashdump 4 | *.plt 5 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {cover_enabled, true}. 2 | {dialyzer, [{plt_extra_apps, []}, 3 | {warnings, [unknown]}]}. 4 | {erl_opts, [debug_info]}. 5 | {deps, []}. 6 | {shell, [{apps, [ksuid]}]}. 7 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: "Tests" 2 | on: 3 | push: 4 | branches: 5 | - "master" 6 | pull_request: 7 | branches: 8 | - "master" 9 | jobs: 10 | linux: 11 | name: "Test on OTP ${{ matrix.otp_version }} and ${{ matrix.os }}" 12 | runs-on: "${{ matrix.os }}" 13 | strategy: 14 | matrix: 15 | otp_version: [23, 24, 25] 16 | os: ["ubuntu-latest"] 17 | container: 18 | image: "erlang:${{ matrix.otp_version }}" 19 | steps: 20 | - uses: "actions/checkout@v2" 21 | - name: "make dialyzer" 22 | run: | 23 | make dialyzer 24 | - name: "make test" 25 | run: | 26 | make test 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a 6 | Changelog](https://keepachangelog.com/en/1.0.0/), and this project 7 | adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 8 | 9 | ## [Unreleased] 10 | 11 | ## 1.1.3 12 | 13 | No change. 14 | 15 | ## 1.1.2 16 | 17 | ## Fixed 18 | 19 | - Add missing dependency on `crypto`. 20 | 21 | ## 1.1.1 22 | 23 | ## Fixed 24 | 25 | - Validate the string content in `ksuid:is_valid/1`. 26 | 27 | ## 1.1.0 28 | 29 | ## Added 30 | 31 | - Use the string representation by default. 32 | 33 | ## 1.0.1 34 | 35 | ## Changed 36 | 37 | - Reject strings which are not precisely 27 characters long. 38 | 39 | ## 1.0.0 40 | 41 | First public version. 42 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Bryan Frimin . 2 | Copyright (c) 2020-2021 Exograd SAS. 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any 5 | purpose with or without fee is hereby granted, provided that the above 6 | copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /src/ksuid.app.src: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2022 Bryan Frimin . 2 | %% Copyright (c) 2020-2021 Exograd SAS. 3 | %% 4 | %% Permission to use, copy, modify, and/or distribute this software for any 5 | %% purpose with or without fee is hereby granted, provided that the above 6 | %% copyright notice and this permission notice appear in all copies. 7 | %% 8 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 11 | %% SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 14 | %% IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | 16 | {application, ksuid, 17 | [{description, "K-sortable unique identifier handling."}, 18 | {vsn, "git"}, 19 | {registered, []}, 20 | {applications, 21 | [kernel, 22 | stdlib, 23 | crypto]}, 24 | {env, []}, 25 | {modules, []}, 26 | 27 | {licenses, ["ISC"]}]}. 28 | -------------------------------------------------------------------------------- /GNUmakefile: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Bryan Frimin . 2 | # 3 | # Permission to use, copy, modify, and/or distribute this software for 4 | # any purpose with or without fee is hereby granted, provided that the 5 | # above copyright notice and this permission notice appear in all 6 | # copies. 7 | # 8 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL 9 | # WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED 10 | # WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE 11 | # AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL 12 | # DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR 13 | # PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 14 | # TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | # PERFORMANCE OF THIS SOFTWARE. 16 | 17 | all: dialyzer test 18 | 19 | dialyzer: 20 | QUIET=1 rebar3 dialyzer 21 | 22 | build: 23 | QUIET=1 rebar3 compile 24 | 25 | shell: 26 | QUIET=1 rebar3 shell 27 | 28 | test: 29 | QUIET=1 rebar3 eunit 30 | 31 | cover: 32 | QUIET=1 rebar3 eunit --cover 33 | QUIET=1 rebar3 cover 34 | 35 | clean: 36 | $(RM) -r _build 37 | 38 | .PHONY: all dialyzer build shell test cover clean 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | This repository contains an Erlang library providing functions to 4 | generate and manipulate KSUID as described in the [reference 5 | implementation](https://github.com/segmentio/ksuid). 6 | 7 | # Build 8 | You can build the library with: 9 | 10 | make build 11 | 12 | # Test 13 | You can execute the test suite with: 14 | 15 | make dialyzer test 16 | 17 | You can generate the test coverage with: 18 | 19 | make cover 20 | 21 | # Documentation 22 | 23 | A handbook is available [in the `doc` 24 | directory](https://github.com/exograd/erl-ksuid/blob/master/doc/handbook.md). 25 | 26 | # Contact 27 | 28 | If you find a bug or have any question, feel free to open a GitHub 29 | issue. 30 | 31 | Please not that we do not currently review or accept any contribution. 32 | 33 | # Licence 34 | 35 | Released under the ISC license. 36 | 37 | Copyright (c) 2022 Bryan Frimin . 38 | 39 | Permission to use, copy, modify, and/or distribute this software for any 40 | purpose with or without fee is hereby granted, provided that the above 41 | copyright notice and this permission notice appear in all copies. 42 | 43 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 44 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 45 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 46 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 47 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 48 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 49 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 50 | -------------------------------------------------------------------------------- /test/ksuid_base62_tests.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2022 Bryan Frimin . 2 | %% Copyright (c) 2020-2021 Exograd SAS. 3 | %% 4 | %% Permission to use, copy, modify, and/or distribute this software for any 5 | %% purpose with or without fee is hereby granted, provided that the above 6 | %% copyright notice and this permission notice appear in all copies. 7 | %% 8 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 11 | %% SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 14 | %% IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | 16 | -module(ksuid_base62_tests). 17 | 18 | -include_lib("eunit/include/eunit.hrl"). 19 | 20 | encode_test_() -> 21 | [?_assertEqual(<<"0">>, ksuid_base62:encode(0)), 22 | ?_assertEqual(<<"1">>, ksuid_base62:encode(1)), 23 | ?_assertEqual(<<"K">>, ksuid_base62:encode(20)), 24 | ?_assertEqual(<<"10">>, ksuid_base62:encode(62)), 25 | ?_assertEqual(<<"100">>, ksuid_base62:encode(3844)), 26 | ?_assertEqual(<<"3D7">>, ksuid_base62:encode(12345))]. 27 | 28 | decode_test_() -> 29 | [?_assertEqual({ok, 0}, ksuid_base62:decode(<<"0">>)), 30 | ?_assertEqual({ok, 1}, ksuid_base62:decode(<<"1">>)), 31 | ?_assertEqual({ok, 20}, ksuid_base62:decode(<<"K">>)), 32 | ?_assertEqual({ok, 62}, ksuid_base62:decode(<<"10">>)), 33 | ?_assertEqual({ok, 3844}, ksuid_base62:decode(<<"100">>)), 34 | ?_assertEqual({ok, 12345}, ksuid_base62:decode(<<"3D7">>)), 35 | ?_assertEqual({error, {invalid_character, $.}}, 36 | ksuid_base62:decode(<<"a.b">>))]. 37 | 38 | encode_decode_test_() -> 39 | Numbers = [rand:uniform((2 bsl 160) - 1) || _ <- lists:seq(1, 100)], 40 | [?_assertEqual({ok, N}, 41 | ksuid_base62:decode(ksuid_base62:encode(N))) || N <- Numbers]. 42 | -------------------------------------------------------------------------------- /test/ksuid_tests.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2022 Bryan Frimin . 2 | %% Copyright (c) 2020-2021 Exograd SAS. 3 | %% 4 | %% Permission to use, copy, modify, and/or distribute this software for any 5 | %% purpose with or without fee is hereby granted, provided that the above 6 | %% copyright notice and this permission notice appear in all copies. 7 | %% 8 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 11 | %% SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 14 | %% IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | 16 | -module(ksuid_tests). 17 | 18 | -include_lib("eunit/include/eunit.hrl"). 19 | 20 | parse_test_() -> 21 | [?_assertEqual({ok, <<12,82,194,192,81,235,30,45,167,101, 22 | 37,49,142,237,93,150,108,158,91,122>>}, 23 | ksuid:parse(<<"1l12i5euax5i7oGDn5DFULPYdCM">>)), 24 | ?_assertEqual({error, invalid_format}, 25 | ksuid:parse(<<"">>)), 26 | ?_assertEqual({error, invalid_format}, 27 | ksuid:parse(<<"1l12i5euax5i7oGDn5DFULPYdCM2">>)), 28 | ?_assertEqual({error, {invalid_character, $=}}, 29 | ksuid:parse(<<"1l12i5euax5i7oGDn5DFULPYdC=">>))]. 30 | 31 | is_valid_test_() -> 32 | Ids = [ksuid:generate() || _ <- lists:seq(1, 100)], 33 | [?_assert(ksuid:is_valid(Id)) || Id <- Ids]. 34 | 35 | is_valid_binary_test_() -> 36 | Ids = [ksuid:generate_binary() || _ <- lists:seq(1, 100)], 37 | [?_assert(ksuid:is_valid_binary(Id)) || Id <- Ids]. 38 | 39 | format_test_() -> 40 | Ids = [ksuid:generate_binary() || _ <- lists:seq(1, 100)], 41 | [?_assertEqual(27, byte_size(ksuid:format(Id))) || Id <- Ids]. 42 | 43 | format_parse_test_() -> 44 | Ids = [ksuid:generate_binary() || _ <- lists:seq(1, 100)], 45 | [?_assertEqual({ok, Id}, ksuid:parse(ksuid:format(Id))) || Id <- Ids]. 46 | -------------------------------------------------------------------------------- /doc/handbook.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | The erl-ksuid project provides functions to manipulate KSUID as 4 | described in the [reference 5 | implementation](https://github.com/segmentio/ksuid). 6 | 7 | KSUID, for "K-Sortable Unique IDentifier", are 20 byte globally unique 8 | identifiers with various interesting properties. 9 | 10 | Compared to incremental integers, KSUID provide (reasonable) unicity 11 | without coordination around a single counter and protect against 12 | identifier discovery. 13 | 14 | Compared to [UUID v4](https://www.ietf.org/rfc/rfc4122.txt), KSUID are 15 | loosely time ordered (which is useful for resource pagination), and both 16 | their textual and binary representations are lexicographically sortable. 17 | 18 | Compared to [ULID](https://github.com/ulid/spec), KSUID do not require a 19 | state. 20 | 21 | # Implementation 22 | 23 | KSUID generation uses 24 | [`erlang:system_time/1`](https://erlang.org/doc/man/erlang.html#system_time-1) 25 | to obtain the timestamp part, and 26 | [`crypto:strong_rand_bytes/1`](https://erlang.org/doc/man/crypto.html#strong_rand_bytes-1) 27 | for the high quality random data required by the random part. 28 | 29 | # API 30 | 31 | ## KSUID values 32 | 33 | The `ksuid/0` and `ksuid_binary/0` types are used to represent the 34 | textual representation and binary representation of KSUID values. Both 35 | are based on Erlang binaries. 36 | 37 | ## Generation 38 | 39 | The `ksuid:generate/0` and `ksuid:generate_binary/0` functions generate 40 | a random KSUID. 41 | 42 | # Validation 43 | 44 | The `ksuid:is_valid/1` function is used to check the validity of a 45 | textual KSUID. The textual representation of a KSUID is valid if it a 46 | string of 27 characters, each one being either an ASCII letter or digit. 47 | 48 | The `ksuid:is_valid_binary/1` function is used to check the validity of 49 | a binary KSUID. The binary representation of a KSUID is valid if it is a 50 | 20 byte long binary. 51 | 52 | # Parsing 53 | 54 | The `ksuid:parse/1` function is used to obtain the binary representation 55 | of a textual KSUID. It returns `{ok, KSUID}` if the parameter is a valid 56 | textual KSUID or `{error, Reason}` if it is not. 57 | 58 | # Formatting 59 | 60 | The `ksuid:format/1` function returns the textual representation of a 61 | binary KSUID. 62 | -------------------------------------------------------------------------------- /src/ksuid_base62.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2022 Bryan Frimin . 2 | %% Copyright (c) 2020-2021 Exograd SAS. 3 | %% 4 | %% Permission to use, copy, modify, and/or distribute this software for any 5 | %% purpose with or without fee is hereby granted, provided that the above 6 | %% copyright notice and this permission notice appear in all copies. 7 | %% 8 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 11 | %% SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 14 | %% IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | 16 | -module(ksuid_base62). 17 | 18 | -export([encode/1, decode/1, pow62/1]). 19 | 20 | -spec encode(non_neg_integer()) -> binary(). 21 | encode(N) -> 22 | list_to_binary(encode(N, [])). 23 | 24 | -spec encode(non_neg_integer(), string()) -> string(). 25 | encode(0, []) -> 26 | "0"; 27 | encode(0, Acc) -> 28 | Acc; 29 | encode(N, Acc) -> 30 | encode(N div 62, [number_to_character(N rem 62) | Acc]). 31 | 32 | -spec decode(binary()) -> {ok, non_neg_integer()} | {error, term()}. 33 | decode(Bin) -> 34 | String = binary_to_list(Bin), 35 | decode(String, length(String)-1, 0). 36 | 37 | -spec decode(string(), non_neg_integer(), non_neg_integer()) -> 38 | {ok, non_neg_integer()} | {error, {invalid_character, byte()}}. 39 | decode([], _, Acc) -> 40 | {ok, Acc}; 41 | decode([C | Rest], I, Acc) -> 42 | case character_to_number(C) of 43 | {ok, N} -> 44 | decode(Rest, I-1, Acc + N*pow62(I)); 45 | error -> 46 | {error, {invalid_character, C}} 47 | end. 48 | 49 | -spec pow62(non_neg_integer()) -> non_neg_integer(). 50 | pow62(0) -> 51 | 1; 52 | pow62(N) -> 53 | 62 * pow62(N-1). 54 | 55 | -spec number_to_character(0..61) -> byte(). 56 | number_to_character(N) when N < 10 -> 57 | $0 + N; 58 | number_to_character(N) when N < 36 -> 59 | $A + N - 10; 60 | number_to_character(N) -> 61 | $a + N - 36. 62 | 63 | -spec character_to_number(byte()) -> {ok, 0..61} | error. 64 | character_to_number(C) when C >= $0, C =< $9 -> 65 | {ok, C - $0}; 66 | character_to_number(C) when C >= $A, C =< $Z -> 67 | {ok, C - $A + 10}; 68 | character_to_number(C) when C >= $a, C =< $z -> 69 | {ok, C - $a + 36}; 70 | character_to_number(_) -> 71 | error. 72 | -------------------------------------------------------------------------------- /src/ksuid.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2022 Bryan Frimin . 2 | %% Copyright (c) 2020-2021 Exograd SAS. 3 | %% 4 | %% Permission to use, copy, modify, and/or distribute this software for any 5 | %% purpose with or without fee is hereby granted, provided that the above 6 | %% copyright notice and this permission notice appear in all copies. 7 | %% 8 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 11 | %% SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 14 | %% IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | 16 | -module(ksuid). 17 | 18 | -export([generate/0, generate_binary/0, 19 | is_valid/1, is_valid_binary/1, system_time/1, format/1, parse/1, 20 | random_data/0, current_timestamp/0, 21 | system_time_to_timestamp/1, timestamp_to_system_time/1]). 22 | 23 | -export_type([ksuid/0, ksuid_binary/0]). 24 | 25 | -type ksuid() :: <<_:216>>. 26 | -type ksuid_binary() :: <<_:160>>. 27 | -type ksuid_timestamp() :: 0..4_294_967_295. 28 | 29 | -spec generate() -> ksuid(). 30 | generate() -> 31 | format(generate_binary()). 32 | 33 | -spec generate_binary() -> ksuid_binary(). 34 | generate_binary() -> 35 | Timestamp = current_timestamp(), 36 | RandomData = random_data(), 37 | <>. 38 | 39 | -spec is_valid(ksuid()) -> boolean(). 40 | is_valid(Id) when is_binary(Id) -> 41 | case re:run(Id, "^[0-9A-Za-z]{27}$") of 42 | {match, _} -> 43 | true; 44 | nomatch -> 45 | false 46 | end; 47 | is_valid(_) -> 48 | false. 49 | 50 | -spec is_valid_binary(ksuid_binary()) -> boolean(). 51 | is_valid_binary(<<_:160>>) -> 52 | true; 53 | is_valid_binary(_) -> 54 | false. 55 | 56 | -spec system_time(ksuid() | ksuid_binary()) -> integer(). 57 | system_time(Id) when byte_size(Id) == 27 -> 58 | case parse(Id) of 59 | {ok, Bin} -> 60 | system_time(Bin); 61 | {error, Reason} -> 62 | error({invalid_ksuid, Reason}) 63 | end; 64 | system_time(<>) -> 65 | timestamp_to_system_time(Id). 66 | 67 | -spec format(ksuid_binary()) -> ksuid(). 68 | format(<>) -> 69 | ksuid_base62:encode(Id). 70 | 71 | -spec parse(binary()) -> {ok, ksuid_binary()} | {error, term()}. 72 | parse(Data) when byte_size(Data) =:= 27 -> 73 | case ksuid_base62:decode(Data) of 74 | {ok, N} -> 75 | {ok, <>}; 76 | {error, Reason} -> 77 | {error, Reason} 78 | end; 79 | parse(_Data) -> 80 | {error, invalid_format}. 81 | 82 | -spec current_timestamp() -> ksuid_timestamp(). 83 | current_timestamp() -> 84 | system_time_to_timestamp(erlang:system_time(second)). 85 | 86 | -spec system_time_to_timestamp(integer()) -> ksuid_timestamp(). 87 | system_time_to_timestamp(SysTime) -> 88 | SysTime - epoch(). 89 | 90 | -spec timestamp_to_system_time(ksuid_timestamp()) -> integer(). 91 | timestamp_to_system_time(Timestamp) -> 92 | Timestamp + epoch(). 93 | 94 | -spec epoch() -> ksuid_timestamp(). 95 | epoch() -> 96 | 1_400_000_000. 97 | 98 | -spec random_data() -> <<_:128>>. 99 | random_data() -> 100 | crypto:strong_rand_bytes(16). 101 | --------------------------------------------------------------------------------