defmodule TUID.ParameterizedType do @moduledoc """ Documentation for `TUID.ParameterizedType`. ParameterizedType implemention for use with Ecto to use TUIDs (tagged, unique ids) as Ecto Types. """ use Ecto.ParameterizedType alias TUID.Base58 @doc """ Callback to convert the options specified in the field macro into parameters to be used in other callbacks. This function is called at compile time, and should raise if invalid values are specified. It is idiomatic that the parameters returned from this are a map. `field` and `schema` will be injected into the options automatically. For example, this schema specification schema "my_table" do field :my_field, MyParameterizedType, opt1: :foo, opt2: nil end will result in the call: MyParameterizedType.init([schema: "my_table", field: :my_field, opt1: :foo, opt2: nil]) """ @impl true def init(opts) do schema = Keyword.fetch!(opts, :schema) field = Keyword.fetch!(opts, :field) uniq = Uniq.UUID.init(schema: schema, field: field, version: 7, default: :raw, dump: :raw) case opts[:primary_key] do true -> prefix = Keyword.get(opts, :prefix) || raise "`:prefix` option is required" %{ primary_key: true, schema: schema, prefix: prefix, uniq: uniq } _any -> %{ schema: schema, field: field, uniq: uniq } end end @impl true def type(_params), do: :uuid @doc """ Casts the given input to the ParameterizedType with the given parameters. Specifically, convert a TUID, such as `user_C19xa4ANGXSz72USEyc2m` to a binary UUID for storage into the DB. For more information on casting, see `c:Ecto.Type.cast/1`. """ @impl true def cast(nil, _params), do: {:ok, nil} def cast(data, params) do with {:ok, prefix, _uuid} <- slug_to_uuid(data, params), {prefix, prefix} <- {prefix, prefix(params)} do {:ok, data} else _ -> :error end end defp slug_to_uuid(string, _params) do with [prefix, slug] <- String.split(string, "_"), {:ok, uuid} <- Base58.decode_uuid(slug) do {:ok, prefix, uuid} else _ -> :error end end defp prefix(%{primary_key: true, prefix: prefix}), do: prefix # If we deal with a belongs_to assocation we need to fetch the prefix from # the associations schema module defp prefix(%{schema: schema, field: field}) do %{related: schema, related_key: field} = schema.__schema__(:association, field) {:parameterized, __MODULE__, %{prefix: prefix}} = schema.__schema__(:type, field) prefix end @doc """ Loads the given term into a ParameterizedType. It receives a `loader` function in case the parameterized type is also a composite type. In order to load the inner type, the `loader` must be called with the inner type and the inner value as argument. For more information on loading, see `c:Ecto.Type.load/1`. Note that this callback *will* be called when loading a `nil` value, unlike `c:Ecto.Type.load/1`. """ @impl true def load(data, loader, params) do case Uniq.UUID.load(data, loader, params.uniq) do {:ok, nil} -> {:ok, nil} {:ok, uuid} -> {:ok, uuid_to_slug(uuid, params)} :error -> :error end end defp uuid_to_slug(uuid, params) do "#{prefix(params)}_#{Base58.encode_uuid(uuid)}" end @doc """ Dumps the given term into an Ecto native type. It receives a `dumper` function in case the parameterized type is also a composite type. In order to dump the inner type, the `dumper` must be called with the inner type and the inner value as argument. For more information on dumping, see `c:Ecto.Type.dump/1`. Note that this callback *will* be called when dumping a `nil` value, unlike `c:Ecto.Type.dump/1`. """ @impl true def dump(nil, _, _), do: {:ok, nil} def dump(slug, dumper, params) do case slug_to_uuid(slug, params) do {:ok, _prefix, uuid} -> Uniq.UUID.dump(uuid, dumper, params.uniq) :error -> :error end end @doc """ Generates a loaded version of the data. This callback is invoked when a parameterized type is given to `field` with the `:autogenerate` flag. """ @impl true def autogenerate(params) do uuid_to_slug(Uniq.UUID.autogenerate(params.uniq), params) end @doc """ Dictates how the type should be treated inside embeds. For more information on embedding, see `c:Ecto.Type.embed_as/1` """ @impl true def embed_as(format, params), do: Uniq.UUID.embed_as(format, params.uniq) @doc """ Returns the underlying schema type for the ParameterizedType. For more information on schema types, see `c:Ecto.Type.type/0` """ @impl true def equal?(a, b, params), do: Uniq.UUID.equal?(a, b, params.uniq) end