lib/ecto/ulid.ex

# 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: <<System.os_time(:millisecond)::48>> <> :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?(<<ts::48, _random::80>>), 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