defmodule Needle.UID do
@moduledoc "./README.md" |> File.stream!() |> Enum.drop(1) |> Enum.join()
use Ecto.ParameterizedType
import Untangle, except: [dump: 3]
@pride_enabled Code.ensure_loaded?(Pride) and Application.compile_env(:needle_uid, :pride_enabled, false)
@ulid_enabled Application.compile_env(:needle_uid, :ulid_enabled, true)
@doc "translates alphanumerics into a sentinel ID value"
def synthesise!(x) when is_binary(x) do
# TODO with UUID
Needle.ULID.synthesise!(x)
end
@doc """
The underlying schema type.
"""
@impl true
def type(_params), do: :uuid
@impl true
def init(opts) do
if @pride_enabled do
if Keyword.get(opts, :prefix) do
IO.warn("use prefixed UUIDv7 for #{inspect(opts)}")
Pride.init(opts)
else
IO.warn("fallback to ULID for #{inspect(opts)}")
Pride.init(opts ++ [allow_unprefixed: true])
end
else
Map.new(opts)
end
end
@impl true
def equal?(a, b, params \\ nil)
if @pride_enabled do
def equal?(a, b, %{prefix: _} = params), do: Pride.equal?(a, b, params)
end
if @ulid_enabled do
def equal?(a, b, _), do: Needle.ULID.equal?(a, b)
end
@impl true
def embed_as(format, params \\ nil)
if @pride_enabled do
def embed_as(format, %{prefix: _} = params), do: Pride.embed_as(format, params)
end
if @ulid_enabled do
def embed_as(format, _), do: Needle.ULID.embed_as(format)
end
@impl true
def autogenerate(params), do: generate(params)
def generate(params_or_timestamp \\ nil)
if @pride_enabled do
def generate(%{prefix: _} = params), do: Pride.autogenerate(params)
end
if @ulid_enabled do
def generate(timestamp) when is_integer(timestamp), do: Needle.ULID.generate(timestamp)
# def generate(%Date{} =timestamp), do: Needle.ULID.generate(timestamp) # TODO
def generate(%DateTime{} =timestamp), do: Needle.ULID.generate(timestamp)
end
if @pride_enabled do
def generate(schema) when is_atom(schema) and not is_nil(schema) do
debug(schema, "gen schema")
if function_exported?(schema, :__schema__, 1) do
# hopefully ok to just take the first?
case schema.__schema__(:primary_key) |> List.first() |> debug("gen primary_key first") do
nil ->
if @ulid_enabled do
Needle.ULID.generate()
end
field ->
generate(schema, field)
end
else
if @ulid_enabled do
Needle.ULID.generate()
end
end
end
end
if @ulid_enabled do
def generate(_), do: Needle.ULID.generate()
end
if @pride_enabled do
def generate(schema, field) when is_atom(schema) do
case Pride.params(schema, field) |> debug("gen prefix") do
%{prefix: _} = params ->
Pride.autogenerate(params)
_ ->
if @ulid_enabled do
Needle.ULID.generate()
end
end
end
end
@doc "Returns the timestamp of an encoded or unencoded UID"
if @ulid_enabled do
def timestamp(<<_::bytes-size(26)>> = encoded) do
Needle.ULID.timestamp(encoded)
end
end
def timestamp(encoded) do
debug(encoded, "TODO")
raise "#TODO for UUID"
end
@doc """
Casts an encoded string to ID. Transforms outside data into runtime data.
Used to (potentially) convert your data into an internal normalized representation which will be part of your changesets. It also used for verifying that something is actually valid input data for your type.
"""
@impl true
def cast(term, params \\ nil)
def cast(nil, _params), do: {:ok, nil}
if @pride_enabled do
def cast(term, %{prefix: _} = params) do
with {:error, _} <- Pride.cast(term, params) do
# for old ULIDs in a prefixed table
if @ulid_enabled and Needle.ULID.valid?(term) do
{:ok, term}
else
{:error, message: "Not recognised as valid Prefixed UUIDv7 or ULID"}
end
end
end
end
def cast(<<_::bytes-size(16)>> = value, _), do: {:ok, value}
def cast(<<_::bytes-size(26)>> = value, params) do
if @ulid_enabled and Needle.ULID.valid?(value) do
{:ok, value}
else
if @pride_enabled do
Pride.cast(value, params)
else
{:error, message: "Invalid ULID"}
end
end
end
if @pride_enabled do
def cast(term, %{} = params), do: Pride.cast(term, params)
end
def cast(value, params) do
debug(value, "Could not recognise value (with params: #{inspect(params)}) ")
{:error, message: "Not recognised as valid ULID or Prefixed UUIDv7"}
end
@doc """
Same as `cast/1` but raises `Ecto.CastError` on invalid arguments.
"""
def cast!(value, params \\ nil) do
case cast(value, params) do
{:ok, uid} ->
uid
{:error, term} ->
raise Ecto.CastError, type: __MODULE__, value: value, message: term[:message]
:error ->
raise Ecto.CastError, type: __MODULE__, value: value
end
end
@doc """
Converts an encoded ID into a binary. Used to get your data ready to be written to the database. Transforms anything (outside data or runtime data) into database column data
"""
@impl true
def dump(value, dumper \\ nil, params \\ nil)
def dump(nil, _, _), do: {:ok, nil}
if @pride_enabled do
def dump(value, dumper, %{prefix: _} = params), do: Pride.dump(value, dumper, params)
end
if @ulid_enabled do
def dump(<<_::bytes-size(26)>> = encoded, _, _), do: Needle.ULID.decode(encoded)
end
if @pride_enabled do
def dump(value, dumper, %{} = params), do: Pride.dump(value, dumper, params)
end
def dump(_, _, _), do: :error
def dump!(encoded, dumper \\ nil, params \\ nil) do
case dump(encoded, dumper, params) do
{:ok, uid} -> uid
_ -> raise Ecto.CastError, type: __MODULE__, value: encoded
end
end
@doc """
Converts a binary ID into an encoded string. Transforms database column data into runtime data.
"""
@impl true
def load(value, loader \\ nil, params \\ nil)
def load(nil, _, _), do: {:ok, nil}
if @pride_enabled do
def load(value, loader, %{prefix: _} = params), do: Pride.load(value, loader, params)
end
if @pride_enabled do
def load(value, loader, %{} = params) do
with :error <- Pride.load(value, loader, params) do
if @ulid_enabled do
Needle.ULID.encode(value)
else
:error
end
end
end
end
if @ulid_enabled do
def load(bytes, _, _) when is_binary(bytes) and byte_size(bytes) == 16,
do: Needle.ULID.encode(bytes)
end
def load(_, _, _), do: :error
@doc """
Takes a string and returns true if it is a valid UUID or ULID.
## Examples
iex> valid?("01J3MQ2Q4RVB1WTE3KT1D8ZNX1")
true
iex> valid?("550e8400-e29b-41d4-a716-446655440000")
true
> is_pride?("test_3TUIKuXX5mNO2jSA41bsDx") and is_uuid?("test_3TUIKuXX5mNO2jSA41bsDx")
true
iex> valid?("invalid_id")
false
"""
def valid?(str, params \\ nil) do
is_ulid?(str) || is_uuid?(str, params)
end
@doc """
Takes a string and returns true if it is a valid ULID (Universally Unique Lexicographically Sortable Identifier).
## Examples
iex> is_ulid?("01J3MQ2Q4RVB1WTE3KT1D8ZNX1")
true
iex> is_ulid?("invalid_ulid")
false
"""
def is_ulid?(str) when is_binary(str) and byte_size(str) == 26 do
Needle.ULID.valid?(str)
end
def is_ulid?(_), do: false
@doc """
Takes a string and returns true if it is a valid Object ID or Prefixed UUID (Universally Unique Identifier).
## Examples
iex> is_uuid?("550e8400-e29b-41d4-a716-446655440000")
true
> is_pride?("test_3TUIKuXX5mNO2jSA41bsDx") and is_uuid?("test_3TUIKuXX5mNO2jSA41bsDx")
true
iex> is_uuid?("invalid_uuid")
false
"""
def is_uuid?(str, params \\ nil)
def is_uuid?(str, params) when is_binary(str) and byte_size(str) == 36 do
with {:ok, _} <- Ecto.UUID.cast(str) do
true
else
_ ->
is_pride?(str, params)
end
end
def is_uuid?(str, params), do: is_pride?(str, params)
def is_pride?(str, params \\ nil)
def is_pride?(str, params) do
@pride_enabled && Pride.valid?(str, params)
end
end