Packages

An Ash extension to use object IDs as primary and foreign keys.

Current section

Files

Jump to
ash_object_ids lib type.ex
Raw

lib/type.ex

defmodule AshObjectIds.Type do
@moduledoc """
Helper to define the object ID as `Ash.Type`.
"""
defmacro __using__(opts) do
prefix = Keyword.fetch!(opts, :prefix)
uuid_type = Keyword.get(opts, :uuid_type, :uuid) |> Ash.Type.get_type()
quote do
use Ash.Type
def __object_id_info__, do: [prefix: unquote(prefix), uuid_type: unquote(uuid_type)]
@impl Ash.Type
defdelegate storage_type(constraints), to: unquote(uuid_type)
@impl Ash.Type
def cast_input(input, constraints) do
AshObjectIds.Type.cast_input(unquote(uuid_type), unquote(prefix), input, constraints)
end
@impl Ash.Type
def cast_stored(input, constraints) do
AshObjectIds.Type.cast_stored(
unquote(uuid_type),
unquote(prefix),
input,
constraints
)
end
@impl Ash.Type
def dump_to_native(input, constraints) do
AshObjectIds.Type.dump_to_native(
unquote(uuid_type),
unquote(prefix),
input,
constraints
)
end
@impl Ash.Type
def dump_to_embedded(value, constraints) do
cast_input(value, constraints)
end
@impl Ash.Type
def matches_type?(value, constraints) do
case cast_input(value, constraints) do
{:ok, _} -> true
_ -> false
end
end
@impl Ash.Type
def cast_atomic(new_value, constraints) do
unquote(uuid_type).cast_atomic(new_value, constraints)
end
@impl Ash.Type
def generator(constraints) do
AshObjectIds.Type.generator(unquote(uuid_type), unquote(prefix), constraints)
end
def generate(constraints \\ []) do
AshObjectIds.Type.generate(unquote(uuid_type), unquote(prefix), constraints)
end
end
end
def cast_input(_uuid_type, _prefix, nil, _constraints), do: {:ok, nil}
def cast_input(uuid_type, prefix, input, constraints) do
with {:ok, uuid_bin} <- decode_object_id(input, prefix),
{:ok, _uuid} <- uuid_type.cast_input(uuid_bin, constraints) do
# Keep the value in object id form
{:ok, input}
end
end
def cast_stored(_uuid_type, _prefix, nil, _constraints), do: {:ok, nil}
def cast_stored(uuid_type, prefix, input, constraints) do
with {:ok, uuid} when is_binary(uuid) <- uuid_type.cast_stored(input, constraints) do
{:ok, encode_uuid(input, prefix)}
end
end
def dump_to_native(_uuid_type, _prefix, nil, _constraints), do: {:ok, nil}
def dump_to_native(uuid_type, prefix, input, constraints) do
case decode_object_id(input, prefix) do
{:ok, uuid} ->
# This doesn't just call `uuid_type.dump_to_native` because
# Ecto.UUID.dump doesn't support the 128bit uuid, so it would
# succeed for Ash.Type.UUIDv7, but fail for Ash.Type.UUID.
# So only deal with it in case of custom UUID types.
case uuid_type do
Ash.Type.UUID -> uuid
Ash.Type.UUIDv7 -> uuid
_ -> uuid_type.dump_to_native(uuid, constraints)
end
_ ->
:error
end
with {:error, _} <- decode_object_id(input, prefix) do
:error
end
end
def encode_uuid(binary_uuid, prefix) do
"#{prefix}_#{:base58.binary_to_base58(binary_uuid)}"
end
def decode_object_id(input, prefix) do
case decode_object_id(input) do
{:ok, ^prefix, uuid} -> {:ok, uuid}
{:ok, _other, _uuid} -> {:error, "incorrect object prefix"}
_ -> :error
end
end
def decode_object_id(input) when is_binary(input) do
case String.split(input, "_") do
[prefix, slug] ->
case :base58.base58_to_binary(to_charlist(slug)) do
uuid when is_binary(uuid) and byte_size(uuid) == 16 -> {:ok, prefix, uuid}
_ -> :error
end
_ ->
:error
end
end
def decode_object_id(_), do: :error
def generator(uuid_type, prefix, constraints) do
StreamData.repeatedly(fn ->
generate(uuid_type, prefix, constraints)
end)
end
def generate(uuid_type, prefix, constraints) do
case uuid_type do
Ash.Type.UUID ->
Ecto.UUID.bingenerate()
Ash.Type.UUIDv7 ->
Ash.UUIDv7.bingenerate()
_ ->
# Generic implementation (for possibly custom UUID types)
{:ok, bin} = uuid_type.generator(constraints) |> Enum.take(1) |> hd() |> Ecto.UUID.dump()
bin
end
|> encode_uuid(prefix)
end
end