lib/ash_uuid/uuid.ex

defmodule AshUUID.UUID do
  @constraints [
    prefix: [type: :string, doc: "Prefix", default: "id"],
    version: [type: :integer, doc: "Version", default: 7],
    encoded?: [type: :boolean, doc: "Encoded?", default: true],
    prefixed?: [type: :boolean, doc: "Prefixed?", default: true],
    migration_default?: [type: :boolean, doc: "Migration default", default: false],
    strict?: [type: :boolean, doc: "Strict?", default: true]
  ]

  @moduledoc """
  UUID

  ### Constraints

  #{Spark.OptionsHelpers.docs(@constraints)}
  """

  use Ash.Type

  @impl true
  def storage_type, do: :uuid

  @impl true
  def generator(constraints) do
    {:ok, term} =
      generate(constraints[:version])
      |> process(
        constraints[:prefix],
        constraints[:strict?],
        :raw,
        requested_format(constraints[:encoded?], constraints[:prefixed?])
      )

    term
  end

  @impl true
  def cast_input(term, constraints) do
    requested_format = requested_format(constraints[:encoded?], constraints[:prefixed?])
    process(term, constraints[:prefix], constraints[:strict?], initial_format(term), requested_format)
  end

  @impl true
  def cast_stored(term, constraints) do
    requested_format = requested_format(constraints[:encoded?], constraints[:prefixed?])
    process(term, constraints[:prefix], constraints[:strict?], initial_format(term), requested_format)
  end

  @impl true
  def dump_to_native(term, constraints),
    do: process(term, constraints[:prefix], constraints[:strict?], initial_format(term), :integer)

  @impl true
  def dump_to_embedded(term, constraints),
    do: process(term, constraints[:prefix], constraints[:strict?], initial_format(term), :raw)

  @impl true
  def equal?(term1, term2),
    do:
      process(term1, nil, false, initial_format(term1), :raw) == process(term2, nil, false, initial_format(term2), :raw)

  @impl true
  def constraints do
    computed_opts =
      AshUUID.Config.get_config()
      |> Map.put(:prefix, @constraints[:prefix][:default])

    @constraints
    |> Enum.map(fn {key, opts} ->
      default = Map.get(computed_opts, key)
      {key, Keyword.replace(opts, :default, default)}
    end)
  end

  @impl true
  def apply_constraints(_term, _constraints), do: :ok

  ###

  defp process(term, prefix, strict, initial_format, requested_format) do
    with {:ok, term} <- validate(term, initial_format),
         {:ok, term} <- restore(term, initial_format, requested_format),
         {:ok, term} <- strip(term, prefix, strict, initial_format),
         {:ok, term} <- decode(term, initial_format),
         {:ok, term} <- encode(term, requested_format),
         {:ok, term} <- prefix(term, prefix, requested_format),
         {:ok, term} <- dump(term, initial_format, requested_format) do
      {:ok, term}
    else
      err -> err
    end
  end

  ###

  defp initial_format(term), do: AshUUID.identify_format(term)

  ###

  defp requested_format(encoded, prefixed)
  defp requested_format(nil, nil), do: :raw
  defp requested_format(false, _prefixed), do: :raw
  defp requested_format(true, false), do: :encoded
  defp requested_format(true, true), do: :prefixed

  ###

  defp generate(version)
  defp generate(4), do: Uniq.UUID.uuid4()
  defp generate(7), do: Uniq.UUID.uuid7()

  ###

  defp validate(term, initial_format)

  defp validate(term, initial_format) when initial_format in [:integer, :raw] do
    if Uniq.UUID.valid?(term), do: {:ok, term}, else: {:error, "got invalid term"}
  end

  defp validate(term, initial_format) when initial_format in [:encoded, :prefixed], do: {:ok, term}
  defp validate(nil, _initial_format), do: {:ok, nil}
  defp validate(_term, _initial_format), do: {:error, "got invalid term"}

  ###

  defp restore(result, initial_format, requested_format)

  defp restore(term, :integer, requested_format) when requested_format in [:raw, :encoded, :prefixed],
    do: {:ok, Uniq.UUID.to_string(term)}

  defp restore(term, _initial_format, _requested_format), do: {:ok, term}

  ###

  defp strip(result, prefix, strict, initial_format)

  defp strip(term, nil, _strict, :prefixed) do
    case String.split(term, "_") do
      [_prefix, encoded_uuid] -> {:ok, encoded_uuid}
      _ -> {:error, "got invalid prefixed term"}
    end
  end

  defp strip(term, prefix, strict, :prefixed) do
    case {strict, String.split(term, "_")} do
      {true, [^prefix, encoded_uuid]} -> {:ok, encoded_uuid}
      {false, [_prefix, encoded_uuid]} -> {:ok, encoded_uuid}
      _ -> {:error, "got invalid prefixed term"}
    end
  end

  defp strip(term, _prefix, _strict, _initial_format), do: {:ok, term}

  ###

  defp decode(result, initial_format)

  defp decode(nil, nil), do: {:ok, nil}

  defp decode(term, initial_format) when initial_format in [:encoded, :prefixed],
    do: AshUUID.Encoder.decode(term)

  defp decode(term, _initial_format), do: {:ok, term}

  ###

  defp encode(result, requested_format)

  defp encode(nil, _requested_format), do: {:ok, nil}

  defp encode(term, requested_format) when requested_format in [:encoded, :prefixed],
    do: AshUUID.Encoder.encode(term)

  defp encode(term, _requested_format), do: {:ok, term}

  ###

  defp prefix(result, prefix, requested_format)
  defp prefix(nil, _prefix, _requested_format), do: {:ok, nil}
  defp prefix(term, prefix, :prefixed), do: {:ok, "#{prefix}_#{term}"}
  defp prefix(term, _prefix, _requested_format), do: {:ok, term}

  ###

  defp dump(result, initial_format, requested_format)

  defp dump(term, initial_format, :integer) when initial_format in [:raw, :encoded, :prefixed] do
    {:ok, Uniq.UUID.string_to_binary!(term)}
  rescue
    _error in ArgumentError -> {:error, "can not dump term"}
  end

  defp dump(term, _initial_format, _requested_format), do: {:ok, term}
end