Skip to content

Instantly share code, notes, and snippets.

@sourdoughdev
Last active January 13, 2022 21:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sourdoughdev/8f809409e5a3c5a77b9c64f81a1b3102 to your computer and use it in GitHub Desktop.
Save sourdoughdev/8f809409e5a3c5a77b9c64f81a1b3102 to your computer and use it in GitHub Desktop.
Ecto ULID support with downcasing + prefixing
org_01fsab1yp4zh97q2pnj7p6x73x
@primary_key {:id, Ecto.ULID, prefix: "org", autogenerate: true}
defmodule Ecto.ULID do
@moduledoc """
An Ecto type for ULID strings.
Forked from: https://github.com/woylie/ecto-ulid/blob/03f80432d5aba9b04d4476e4e67129fe2935fa53/lib/ecto/ulid.ex#L1
Supports `prefix` + auto downcasing
"""
use Ecto.ParameterizedType
@prefix_separator "_"
@type t :: binary
@typedoc "A hex-encoded UUID string."
@type uuid :: <<_::288>>
@typedoc "A raw binary representation of a UUID."
@type raw :: <<_::128>>
@spec type(any) :: :uuid
def type(_), do: :uuid
@spec init(keyword) :: map
def init(opts), do: Enum.into(opts, %{})
@doc """
Casts a string to ULID.
"""
def cast(value, %{ prefix: prefix } = _params) when is_binary(value) and byte_size(value) >= 26 do
prefix_with_separator = prefix <> @prefix_separator
value
|> String.split_at(String.length(prefix_with_separator))
|> case do
{^prefix_with_separator, data} -> if valid?(String.upcase(data)) do
{:ok, String.upcase(data)}
else
:error
end
_ -> :error
end
end
def cast(value, params) when is_binary(value) and byte_size(value) >= 26 do
if valid?(String.upcase(value)) do
{:ok, String.upcase(value)}
else
:error
end
end
def cast(_, _), do: :error
@doc """
Same as `cast/1` but raises `Ecto.CastError` on invalid arguments.
"""
def cast!(value, params) do
case cast(value, params) do
{:ok, ulid} -> ulid
:error -> raise Ecto.CastError, type: __MODULE__, value: value
end
end
@doc """
Converts a Crockford Base32 encoded ULID into a binary.
"""
def dump(<<_::bytes-size(26)>> = encoded, _dumper, _params) do
encoded
|> String.upcase()
|> decode()
end
def dump(encoded, _dumper, params) when is_binary(encoded) and byte_size(encoded) > 0 do
prefix = Map.get(params, :prefix)
prefix_with_separator = prefix <> @prefix_separator
encoded
|> String.split_at(String.length(prefix_with_separator))
|> case do
{^prefix_with_separator, data} -> if valid?(String.upcase(data)) do
decode(String.upcase(data))
else
:error
end
_ -> :error
end
end
def dump(nil, _, _), do: {:ok, nil}
def dump(_, _, _), do: :error
@doc """
Converts a binary ULID into a Crockford Base32 encoded string.
"""
def load(<<_::unsigned-size(128)>> = bytes, _, params) do
prefix = Map.get(params, :prefix)
case encode(bytes) do
{:ok, ulid_string} -> {:ok, format_id(ulid_string, prefix)}
:error -> :error
end
end
def load(_, _, _), do: :error
@doc false
def autogenerate(params) do
prefix = Map.get(params, :prefix)
generate()
|> format_id(prefix)
end
@doc """
Generates a Crockford Base32 encoded ULID.
If a value is provided for `timestamp`, the generated ULID will be for the provided timestamp.
Otherwise, a ULID will be generated for the current time.
Arguments:
* `timestamp`: A Unix timestamp with millisecond precision.
"""
def generate(timestamp \\ System.system_time(:millisecond)) do
{:ok, ulid} = encode(bingenerate(timestamp))
ulid
end
@doc """
Generates a binary ULID.
If a value is provided for `timestamp`, the generated ULID will be for the provided timestamp.
Otherwise, a ULID will be generated for the current time.
Arguments:
* `timestamp`: A Unix timestamp with millisecond precision.
"""
def bingenerate(timestamp \\ System.system_time(:millisecond)) do
<<timestamp::unsigned-size(48), :crypto.strong_rand_bytes(10)::binary>>
end
defp format_id(id, nil) do
String.downcase(id)
end
defp format_id(id, prefix) do
String.downcase("#{prefix}#{@prefix_separator}#{id}")
end
defp encode(
<<b1::3, b2::5, b3::5, b4::5, b5::5, b6::5, b7::5, b8::5, b9::5,
b10::5, b11::5, b12::5, b13::5, b14::5, b15::5, b16::5, b17::5,
b18::5, b19::5, b20::5, b21::5, b22::5, b23::5, b24::5, b25::5,
b26::5>>
) do
<<e(b1), e(b2), e(b3), e(b4), e(b5), e(b6), e(b7), e(b8), e(b9), e(b10),
e(b11), e(b12), e(b13), e(b14), e(b15), e(b16), e(b17), e(b18), e(b19),
e(b20), e(b21), e(b22), e(b23), e(b24), e(b25), e(b26)>>
catch
:error -> :error
else
encoded -> {:ok, encoded}
end
defp encode(_), do: :error
@compile {:inline, e: 1}
defp e(0), do: ?0
defp e(1), do: ?1
defp e(2), do: ?2
defp e(3), do: ?3
defp e(4), do: ?4
defp e(5), do: ?5
defp e(6), do: ?6
defp e(7), do: ?7
defp e(8), do: ?8
defp e(9), do: ?9
defp e(10), do: ?A
defp e(11), do: ?B
defp e(12), do: ?C
defp e(13), do: ?D
defp e(14), do: ?E
defp e(15), do: ?F
defp e(16), do: ?G
defp e(17), do: ?H
defp e(18), do: ?J
defp e(19), do: ?K
defp e(20), do: ?M
defp e(21), do: ?N
defp e(22), do: ?P
defp e(23), do: ?Q
defp e(24), do: ?R
defp e(25), do: ?S
defp e(26), do: ?T
defp e(27), do: ?V
defp e(28), do: ?W
defp e(29), do: ?X
defp e(30), do: ?Y
defp e(31), do: ?Z
defp decode(
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8, c9::8,
c10::8, c11::8, c12::8, c13::8, c14::8, c15::8, c16::8, c17::8,
c18::8, c19::8, c20::8, c21::8, c22::8, c23::8, c24::8, c25::8,
c26::8>>
) do
<<d(c1)::3, d(c2)::5, d(c3)::5, d(c4)::5, d(c5)::5, d(c6)::5, d(c7)::5,
d(c8)::5, d(c9)::5, d(c10)::5, d(c11)::5, d(c12)::5, d(c13)::5, d(c14)::5,
d(c15)::5, d(c16)::5, d(c17)::5, d(c18)::5, d(c19)::5, d(c20)::5,
d(c21)::5, d(c22)::5, d(c23)::5, d(c24)::5, d(c25)::5, d(c26)::5>>
catch
:error -> :error
else
decoded -> {:ok, decoded}
end
defp decode(_), do: :error
@compile {:inline, d: 1}
defp d(?0), do: 0
defp d(?1), do: 1
defp d(?2), do: 2
defp d(?3), do: 3
defp d(?4), do: 4
defp d(?5), do: 5
defp d(?6), do: 6
defp d(?7), do: 7
defp d(?8), do: 8
defp d(?9), do: 9
defp d(?A), do: 10
defp d(?B), do: 11
defp d(?C), do: 12
defp d(?D), do: 13
defp d(?E), do: 14
defp d(?F), do: 15
defp d(?G), do: 16
defp d(?H), do: 17
defp d(?J), do: 18
defp d(?K), do: 19
defp d(?M), do: 20
defp d(?N), do: 21
defp d(?P), do: 22
defp d(?Q), do: 23
defp d(?R), do: 24
defp d(?S), do: 25
defp d(?T), do: 26
defp d(?V), do: 27
defp d(?W), do: 28
defp d(?X), do: 29
defp d(?Y), do: 30
defp d(?Z), do: 31
defp d(_), do: throw(:error)
defp valid?(
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8, c9::8,
c10::8, c11::8, c12::8, c13::8, c14::8, c15::8, c16::8, c17::8,
c18::8, c19::8, c20::8, c21::8, c22::8, c23::8, c24::8, c25::8,
c26::8>>
) do
v(c1) && v(c2) && v(c3) && v(c4) && v(c5) && v(c6) && v(c7) && v(c8) &&
v(c9) && v(c10) &&
v(c11) && v(c12) && v(c13) &&
v(c14) && v(c15) && v(c16) && v(c17) && v(c18) && v(c19) && v(c20) &&
v(c21) && v(c22) &&
v(c23) && v(c24) && v(c25) && v(c26)
end
defp valid?(_), do: false
@compile {:inline, v: 1}
defp v(?0), do: true
defp v(?1), do: true
defp v(?2), do: true
defp v(?3), do: true
defp v(?4), do: true
defp v(?5), do: true
defp v(?6), do: true
defp v(?7), do: true
defp v(?8), do: true
defp v(?9), do: true
defp v(?A), do: true
defp v(?B), do: true
defp v(?C), do: true
defp v(?D), do: true
defp v(?E), do: true
defp v(?F), do: true
defp v(?G), do: true
defp v(?H), do: true
defp v(?J), do: true
defp v(?K), do: true
defp v(?M), do: true
defp v(?N), do: true
defp v(?P), do: true
defp v(?Q), do: true
defp v(?R), do: true
defp v(?S), do: true
defp v(?T), do: true
defp v(?V), do: true
defp v(?W), do: true
defp v(?X), do: true
defp v(?Y), do: true
defp v(?Z), do: true
defp v(_), do: false
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment