Packages

A helper library for Ecto, Nebulex and other utilties

Current section

Files

Jump to
helper lib ecto ulid.ex
Raw

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 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: <<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(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?(<<ts::48, _random::80>>), 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