# 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