initial implementation

This commit is contained in:
Sloane Perrault 2023-06-29 17:23:05 -04:00
parent e4e25935b7
commit cdd8624662
No known key found for this signature in database
5 changed files with 337 additions and 13 deletions

View file

@ -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
View 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

View file

@ -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
View file

@ -0,0 +1,3 @@
%{
"uniq": {:hex, :uniq, "0.5.4", "0602d0c75682f14863c1edea48920bd3d278b29ff7801d56e040e5f9f51ae0e2", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "856ec690d713f97373a3746afada340143c7e37a6488fca5afa720bfb08f0fe5"},
}

View file

@ -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