mirror of
https://github.com/sloanelybutsurely/typeid-elixir.git
synced 2024-11-28 01:42:54 -05:00
initial implementation
This commit is contained in:
parent
e4e25935b7
commit
cdd8624662
5 changed files with 337 additions and 13 deletions
125
lib/type_id.ex
125
lib/type_id.ex
|
@ -3,16 +3,125 @@ defmodule TypeID do
|
|||
Documentation for `TypeID`.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Hello world.
|
||||
alias TypeID.Base32
|
||||
|
||||
## Examples
|
||||
@enforce_keys [:prefix, :suffix]
|
||||
defstruct @enforce_keys
|
||||
|
||||
iex> TypeID.hello()
|
||||
:world
|
||||
@opaque t() :: %__MODULE__{
|
||||
prefix: String.t(),
|
||||
suffix: String.t()
|
||||
}
|
||||
|
||||
"""
|
||||
def hello do
|
||||
:world
|
||||
@spec new(prefix :: String.t()) :: t()
|
||||
def new(prefix) do
|
||||
suffix =
|
||||
Uniq.UUID.uuid7(:raw)
|
||||
|> Base32.encode()
|
||||
|
||||
%__MODULE__{prefix: prefix, suffix: suffix}
|
||||
end
|
||||
|
||||
@spec type(tid :: t()) :: String.t()
|
||||
def type(%__MODULE__{prefix: prefix}) do
|
||||
prefix
|
||||
end
|
||||
|
||||
@spec suffix(tid :: t()) :: String.t()
|
||||
def suffix(%__MODULE__{suffix: suffix}) do
|
||||
suffix
|
||||
end
|
||||
|
||||
@spec to_string(tid :: t()) :: String.t()
|
||||
def to_string(%__MODULE__{prefix: prefix, suffix: suffix}) do
|
||||
prefix <> "_" <> suffix
|
||||
end
|
||||
|
||||
@spec uuid_bytes(tid :: t()) :: binary()
|
||||
def uuid_bytes(%__MODULE__{suffix: suffix}) do
|
||||
Base32.decode!(suffix)
|
||||
end
|
||||
|
||||
@spec uuid(tid :: t()) :: String.t()
|
||||
def uuid(%__MODULE__{suffix: suffix}) do
|
||||
suffix
|
||||
|> Base32.decode!()
|
||||
|> Uniq.UUID.to_string(:default)
|
||||
end
|
||||
|
||||
@spec from_string!(String.t()) :: t() | no_return()
|
||||
def from_string!(str) do
|
||||
[prefix, suffix] = String.split(str, "_")
|
||||
from!(prefix, suffix)
|
||||
end
|
||||
|
||||
@spec from!(prefix :: String.t(), suffix :: String.t()) :: t() | no_return()
|
||||
def from!(prefix, suffix) do
|
||||
validate_prefix!(prefix)
|
||||
validate_suffix!(suffix)
|
||||
|
||||
%__MODULE__{prefix: prefix, suffix: suffix}
|
||||
end
|
||||
|
||||
@spec from(prefix :: String.t(), suffix :: String.t()) :: {:ok, t()} | :error
|
||||
def from(prefix, suffix) do
|
||||
{:ok, from!(prefix, suffix)}
|
||||
rescue
|
||||
ArgumentError -> :error
|
||||
end
|
||||
|
||||
@spec from_string!(String.t()) :: {:ok, t()} | :error
|
||||
def from_string(str) do
|
||||
{:ok, from_string!(str)}
|
||||
rescue
|
||||
ArgumentError -> :error
|
||||
end
|
||||
|
||||
@spec from_uuid!(prefix :: String.t(), uuid :: String.t()) :: t() | no_return()
|
||||
def from_uuid!(prefix, uuid) do
|
||||
{:ok, %Uniq.UUID{bytes: uuid_bytes, version: 7}} = Uniq.UUID.parse(uuid)
|
||||
from_uuid_bytes!(prefix, uuid_bytes)
|
||||
end
|
||||
|
||||
@spec from_uuid_bytes!(prefix :: String.t(), uuid_bytes :: binary()) :: t() | no_return()
|
||||
def from_uuid_bytes!(prefix, <<uuid_bytes::binary-size(16)>>) do
|
||||
suffix = Base32.encode(uuid_bytes)
|
||||
from!(prefix, suffix)
|
||||
end
|
||||
|
||||
@spec from_uuid(prefix :: String.t(), uuid :: String.t()) :: {:ok, t()} | :error
|
||||
def from_uuid(prefix, uuid) do
|
||||
{:ok, from_uuid!(prefix, uuid)}
|
||||
rescue
|
||||
ArgumentError -> :error
|
||||
end
|
||||
|
||||
@spec from_uuid_bytes(prefix :: String.t(), uuid_bytes :: binary()) :: {:ok, t()} | :error
|
||||
def from_uuid_bytes(prefix, uuid_bytes) do
|
||||
{:ok, from_uuid_bytes!(prefix, uuid_bytes)}
|
||||
rescue
|
||||
ArgumentError -> :error
|
||||
end
|
||||
|
||||
defp validate_prefix!(prefix) do
|
||||
unless prefix =~ ~r/[a-z]{0,63}/ do
|
||||
raise ArgumentError, "invalid prefix: #{prefix}. prefix should match [a-z]{0,63}"
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp validate_suffix!(suffix) do
|
||||
Base32.decode!(suffix)
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: TypeID do
|
||||
import Inspect.Algebra
|
||||
|
||||
def inspect(tid, _opts) do
|
||||
concat(["TypeID.from_string!(\"", tid.prefix, "_", tid.suffix, "\")"])
|
||||
end
|
||||
end
|
||||
|
|
210
lib/type_id/base32.ex
Normal file
210
lib/type_id/base32.ex
Normal file
|
@ -0,0 +1,210 @@
|
|||
defmodule TypeID.Base32 do
|
||||
import Bitwise
|
||||
|
||||
crockford_alphabet = ~c"0123456789abcdefghjkmnpqrstvwxyz"
|
||||
|
||||
encoded = for e1 <- crockford_alphabet, e2 <- crockford_alphabet, do: bsl(e1, 8) + e2
|
||||
|
||||
to_decode_list = fn alphabet ->
|
||||
alphabet = Enum.sort(alphabet)
|
||||
map = Map.new(alphabet)
|
||||
{min, _} = List.first(alphabet)
|
||||
{max, _} = List.last(alphabet)
|
||||
{min, Enum.map(min..max, &map[&1])}
|
||||
end
|
||||
|
||||
{min, decoded} =
|
||||
crockford_alphabet
|
||||
|> Enum.with_index()
|
||||
|> to_decode_list.()
|
||||
|
||||
@spec encode(binary()) :: binary()
|
||||
def encode(data) when is_binary(data) do
|
||||
do_encode(data, "")
|
||||
end
|
||||
|
||||
@spec decode(binary()) :: {:ok, binary()} | :error
|
||||
def decode(string) when is_binary(string) do
|
||||
{:ok, decode!(string)}
|
||||
rescue
|
||||
ArgumentError -> :error
|
||||
end
|
||||
|
||||
@spec decode!(binary()) :: binary() | no_return()
|
||||
def decode!(string) when is_binary(string) do
|
||||
do_decode!(string)
|
||||
end
|
||||
|
||||
@compile {:inline, [do_encode: 1]}
|
||||
defp do_encode(byte) do
|
||||
elem({unquote_splicing(encoded)}, byte)
|
||||
end
|
||||
|
||||
defp do_encode(<<c1::10, c2::10, c3::10, c4::10, rest::binary>>, acc) do
|
||||
do_encode(
|
||||
rest,
|
||||
<<
|
||||
acc::binary,
|
||||
do_encode(c1)::16,
|
||||
do_encode(c2)::16,
|
||||
do_encode(c3)::16,
|
||||
do_encode(c4)::16
|
||||
>>
|
||||
)
|
||||
end
|
||||
|
||||
defp do_encode(<<c1::10, c2::10, c3::10, c4::2>>, acc) do
|
||||
<<
|
||||
acc::binary,
|
||||
do_encode(c1)::16,
|
||||
do_encode(c2)::16,
|
||||
do_encode(c3)::16,
|
||||
c4 |> bsl(3) |> do_encode() |> band(0x00FF)::8
|
||||
>>
|
||||
end
|
||||
|
||||
defp do_encode(<<c1::10, c2::10, c3::4>>, acc) do
|
||||
<<
|
||||
acc::binary,
|
||||
do_encode(c1)::16,
|
||||
do_encode(c2)::16,
|
||||
c3 |> bsl(1) |> do_encode() |> band(0x00FF)::8
|
||||
>>
|
||||
end
|
||||
|
||||
defp do_encode(<<c1::10, c2::6>>, acc) do
|
||||
<<
|
||||
acc::binary,
|
||||
do_encode(c1)::16,
|
||||
c2 |> bsl(4) |> do_encode()::16
|
||||
>>
|
||||
end
|
||||
|
||||
defp do_encode(<<c1::8>>, acc) do
|
||||
<<acc::binary, c1 |> bsl(2) |> do_encode()::16>>
|
||||
end
|
||||
|
||||
defp do_encode(<<>>, acc) do
|
||||
acc
|
||||
end
|
||||
|
||||
defp do_decode!(<<>>), do: <<>>
|
||||
|
||||
defp do_decode!(string) when is_binary(string) do
|
||||
segs = div(byte_size(string) + 7, 8) - 1
|
||||
<<main::size(segs)-binary-unit(64), rest::binary>> = string
|
||||
|
||||
main =
|
||||
for <<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8 <- main>>, into: <<>> do
|
||||
<<
|
||||
do_decode!(c1)::5,
|
||||
do_decode!(c2)::5,
|
||||
do_decode!(c3)::5,
|
||||
do_decode!(c4)::5,
|
||||
do_decode!(c5)::5,
|
||||
do_decode!(c6)::5,
|
||||
do_decode!(c7)::5,
|
||||
do_decode!(c8)::5
|
||||
>>
|
||||
end
|
||||
|
||||
case rest do
|
||||
<<c1::8, c2::8, ?=, ?=, ?=, ?=, ?=, ?=>> ->
|
||||
<<main::bits, do_decode!(c1)::5, bsr(do_decode!(c2), 2)::3>>
|
||||
|
||||
<<c1::8, c2::8, c3::8, c4::8, ?=, ?=, ?=, ?=>> ->
|
||||
<<
|
||||
main::bits,
|
||||
do_decode!(c1)::5,
|
||||
do_decode!(c2)::5,
|
||||
do_decode!(c3)::5,
|
||||
bsr(do_decode!(c4), 4)::1
|
||||
>>
|
||||
|
||||
<<c1::8, c2::8, c3::8, c4::8, c5::8, ?=, ?=, ?=>> ->
|
||||
<<
|
||||
main::bits,
|
||||
do_decode!(c1)::5,
|
||||
do_decode!(c2)::5,
|
||||
do_decode!(c3)::5,
|
||||
do_decode!(c4)::5,
|
||||
bsr(do_decode!(c5), 1)::4
|
||||
>>
|
||||
|
||||
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, ?=>> ->
|
||||
<<
|
||||
main::bits,
|
||||
do_decode!(c1)::5,
|
||||
do_decode!(c2)::5,
|
||||
do_decode!(c3)::5,
|
||||
do_decode!(c4)::5,
|
||||
do_decode!(c5)::5,
|
||||
do_decode!(c6)::5,
|
||||
bsr(do_decode!(c7), 3)::2
|
||||
>>
|
||||
|
||||
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8>> ->
|
||||
<<
|
||||
main::bits,
|
||||
do_decode!(c1)::5,
|
||||
do_decode!(c2)::5,
|
||||
do_decode!(c3)::5,
|
||||
do_decode!(c4)::5,
|
||||
do_decode!(c5)::5,
|
||||
do_decode!(c6)::5,
|
||||
do_decode!(c7)::5,
|
||||
do_decode!(c8)::5
|
||||
>>
|
||||
|
||||
<<c1::8, c2::8>> ->
|
||||
<<main::bits, do_decode!(c1)::5, bsr(do_decode!(c2), 2)::3>>
|
||||
|
||||
<<c1::8, c2::8, c3::8, c4::8>> ->
|
||||
<<
|
||||
main::bits,
|
||||
do_decode!(c1)::5,
|
||||
do_decode!(c2)::5,
|
||||
do_decode!(c3)::5,
|
||||
bsr(do_decode!(c4), 4)::1
|
||||
>>
|
||||
|
||||
<<c1::8, c2::8, c3::8, c4::8, c5::8>> ->
|
||||
<<
|
||||
main::bits,
|
||||
do_decode!(c1)::5,
|
||||
do_decode!(c2)::5,
|
||||
do_decode!(c3)::5,
|
||||
do_decode!(c4)::5,
|
||||
bsr(do_decode!(c5), 1)::4
|
||||
>>
|
||||
|
||||
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8>> ->
|
||||
<<
|
||||
main::bits,
|
||||
do_decode!(c1)::5,
|
||||
do_decode!(c2)::5,
|
||||
do_decode!(c3)::5,
|
||||
do_decode!(c4)::5,
|
||||
do_decode!(c5)::5,
|
||||
do_decode!(c6)::5,
|
||||
bsr(do_decode!(c7), 3)::2
|
||||
>>
|
||||
end
|
||||
end
|
||||
|
||||
defp do_decode!(char) do
|
||||
try do
|
||||
elem({unquote_splicing(decoded)}, char - unquote(min))
|
||||
rescue
|
||||
_ -> bad_character!(char)
|
||||
else
|
||||
nil -> bad_character!(char)
|
||||
char -> char
|
||||
end
|
||||
end
|
||||
|
||||
defp bad_character!(byte) do
|
||||
raise ArgumentError,
|
||||
"non-alphabet character found: #{inspect(<<byte>>, binaries: :as_strings)} (byte #{byte})"
|
||||
end
|
||||
end
|
3
mix.exs
3
mix.exs
|
@ -21,8 +21,7 @@ defmodule TypeID.MixProject do
|
|||
# Run "mix help deps" to learn about dependencies.
|
||||
defp deps do
|
||||
[
|
||||
# {:dep_from_hexpm, "~> 0.3.0"},
|
||||
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
|
||||
{:uniq, "~> 0.5.4"}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
3
mix.lock
Normal file
3
mix.lock
Normal file
|
@ -0,0 +1,3 @@
|
|||
%{
|
||||
"uniq": {:hex, :uniq, "0.5.4", "0602d0c75682f14863c1edea48920bd3d278b29ff7801d56e040e5f9f51ae0e2", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "856ec690d713f97373a3746afada340143c7e37a6488fca5afa720bfb08f0fe5"},
|
||||
}
|
|
@ -1,8 +1,11 @@
|
|||
defmodule TypeIDTest do
|
||||
use ExUnit.Case
|
||||
doctest TypeID
|
||||
|
||||
test "greets the world" do
|
||||
assert TypeID.hello() == :world
|
||||
describe "new/1" do
|
||||
test "returns a new TypeID struct" do
|
||||
tid = TypeID.new("test")
|
||||
assert is_struct(tid, TypeID)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue