# 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 (Universally Unique Lexicographically Sortable Identifier) bitstring. This format is binary compatible with UUID, but also provides lexicographic sorting capability. The spec for ULID can be found [here](https://github.com/ulid/spec) """ use Ecto.Type @typedoc """ The binary type definition for ULID """ @type t() :: <<_::128>> @typedoc """ ASCII type definition for ULID """ @type pretty() :: <<_::208>> @typedoc """ A combination of binary and ASCII types """ @type ulid() :: t() | pretty() @crockford_base32 '0123456789ABCDEFGHJKMNPQRSTVWXYZ' @doc """ `c:Ecto.Type.type/0` callback implementation ## Examples iex> type() :uuid """ @impl Ecto.Type @spec type :: :uuid def type, do: :uuid @doc """ `c:Ecto.Type.embed_as/1` callback implementation ## Examples iex> embed_as(:any) :self """ @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(ulid()) :: :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 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() {: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(ulid()) :: {:ok, t()} | :error def cast(value), do: dump(value) @doc """ Validate the ULID and return true or false. An ULID is valid when it can be converted to a 128 bit string and the significant 48 bits when converted to integer are less then the current unix time in milliseconds """ @spec valid?(ulid() | :error) :: boolean() def valid?(<<_::208>> = value), do: cast(value) |> valid?() def valid?({:ok, raw}), do: valid?(raw) def valid?(<>), do: ts <= System.os_time(:millisecond) def valid?(_), do: false fun_lowercase = fn {value, index} when value > '9' -> defp unquote(:d)(unquote(value + 32)), do: unquote(index) _ -> :noop end fun = fn {value, index} -> defp unquote(:d)(unquote(value)), do: unquote(index) defp unquote(:e)(unquote(index)), do: unquote(List.to_charlist([value])) fun_lowercase.({value, index}) end @crockford_base32 |> Enum.with_index() |> Enum.each(&fun.(&1)) defp d(_), do: throw(:error) end