# Copyright (c) 2021 Anand Panchapakesan # # This software is released under the MIT License. # https://opensource.org/licenses/MIT defmodule Ecto.ULID do @moduledoc """ An Ecto type for ULID string: * ULID -> Universally Unique Lexicographically Sortable Identifier * The spec for ULID can be found [here](https://github.com/ulid/spec) """ use Ecto.Type @typedoc """ The type definition for ULID """ @type t() :: <<_::128>> @typedoc """ Type definition for human readable form """ @type pretty() :: <<_::208>> @type ulid() :: t() | pretty() @crockford_base32 '0123456789ABCDEFGHJKMNPQRSTVWXYZ' @doc """ `c:Ecto.Type.type/0` callback implementation """ @impl Ecto.Type @spec type :: :uuid def type, do: :uuid @doc """ `c:Ecto.Type.embed_as/1` callback implementation """ @impl Ecto.Type @spec embed_as(atom()) :: :self def embed_as(_), do: :self @doc """ `c:Ecto.Type.equal?/2` callback implementation """ @impl Ecto.Type @spec equal?(t1, t2) :: boolean() when t1: ulid(), t2: t1 def equal?(<<_::128>> = t1, <<_::128>> = t2), do: t1 === t2 def equal?(<<_::208>> = t1, <<_::208>> = t2), do: t1 === t2 def equal?(_, _), do: false @doc """ `c:Ecto.Type.autogenerate/0` callback implementation """ @impl Ecto.Type @spec autogenerate :: t() def autogenerate, do: <> <> :crypto.strong_rand_bytes(10) @doc """ `c:Ecto.Type.dump/1` callback implementation """ @impl Ecto.Type @spec dump(ulid()) :: {:ok, t()} | :error def dump(<<_::128>> = value), do: if(valid?(value), do: {:ok, value}, else: :error) def dump( << a1, a2, a3, a4, a5, b1, b2, b3, b4, b5, b6, c1, c2, c3, c4, c5, c6, d1, d2, d3, d4, d5, e1, e2, e3, e4 >> = _value ) do << d(a1)::3, d(a2)::5, d(a3)::5, d(a4)::5, d(a5)::5, d(b1)::5, d(b2)::5, d(b3)::5, d(b4)::5, d(b5)::5, d(b6)::5, d(c1)::5, d(c2)::5, d(c3)::5, d(c4)::5, d(c5)::5, d(c6)::5, d(d1)::5, d(d2)::5, d(d3)::5, d(d4)::5, d(d5)::5, d(e1)::5, d(e2)::5, d(e3)::5, d(e4)::5 >> catch :error -> :error else value -> if valid?(value), do: {:ok, value}, else: :error end def dump(_), do: :error @doc """ `c:Ecto.Type.load/1` callback implementation """ @impl Ecto.Type @spec load(t()) :: :error | {:ok, pretty()} def load(<<_::128>> = value) do unless valid?(value), do: throw(:error) << a1::3, a2::5, a3::5, a4::5, a5::5, b1::5, b2::5, b3::5, b4::5, b5::5, b6::5, c1::5, c2::5, c3::5, c4::5, c5::5, c6::5, d1::5, d2::5, d3::5, d4::5, d5::5, e1::5, e2::5, e3::5, e4::5 >> = value [ e(a1), e(a2), e(a3), e(a4), e(a5), e(b1), e(b2), e(b3), e(b4), e(b5), e(b6), e(c1), e(c2), e(c3), e(c4), e(c5), e(c6), e(d1), e(d2), e(d3), e(d4), e(d5), e(e1), e(e2), e(e3), e(e4) ] |> List.to_string() catch :error -> :error else value -> {:ok, value} end def load(<<_::208>> = value), do: if(valid?(value), do: {:ok, value}, else: :error) def load(_), do: :error @doc """ `c:Ecto.Type.cast/1` callback implementation """ @impl Ecto.Type @spec cast(pretty()) :: {:ok, t()} | :error def cast(value), do: dump(value) @doc """ Check whether passed ULID (either raw or pretty format) is valid """ @spec valid?(ulid()) :: boolean() def valid?(<>), do: ts <= System.os_time(:millisecond) def valid?(<<_::208>> = value) do with {:ok, raw} <- cast(value) do valid?(raw) else :error -> false end end def valid?(_), do: false @doc false @crockford_base32 |> Enum.with_index() |> Enum.each(fn {value, index} -> defp unquote(:d)(unquote(value)), do: unquote(index) if value > '9' do defp unquote(:d)(unquote(value + 32)), do: unquote(index) end end) defp d(_), do: throw(:error) @doc false @crockford_base32 |> Enum.with_index() |> Enum.each(fn {value, index} -> defp unquote(:e)(unquote(index)), do: unquote(List.to_charlist([value])) end) end