lib/ash/resource/transformers/cache_primary_key.ex

defmodule Ash.Resource.Transformers.CachePrimaryKey do
  @moduledoc "Validates and caches the primary key of a resource"
  use Spark.Dsl.Transformer

  alias Spark.Dsl.Transformer
  alias Spark.Error.DslError

  def transform(dsl_state) do
    attributes =
      dsl_state
      |> Transformer.get_entities([:attributes])
      |> Enum.filter(& &1.primary_key?)

    with :ok <- validate_attributes_not_nil(attributes, dsl_state),
         :ok <- maybe_validate_pk_present(attributes, dsl_state) do
      dsl_state =
        dsl_state
        |> Transformer.persist(:primary_key, Enum.map(attributes, & &1.name))
        |> Transformer.persist(
          :primary_key_simple_equality?,
          Enum.all?(attributes, &Ash.Type.simple_equality?(&1.type))
        )

      {:ok, dsl_state}
    end
  end

  defp validate_attributes_not_nil(attributes, dsl_state) do
    Enum.reduce_while(attributes, :ok, fn attribute, :ok ->
      case validate_attribute_not_nil(attribute, dsl_state) do
        :ok -> {:cont, :ok}
        {:error, reason} -> {:halt, {:error, reason}}
      end
    end)
  end

  defp validate_attribute_not_nil(attribute, dsl_state) when attribute.allow_nil? do
    message = """
    Primary keys must not be allowed to be nil.

    The attribute `#{attribute.name}` is set as a primary key and allows nil values.
    """

    {:error,
     DslError.exception(
       module: Transformer.get_persisted(dsl_state, :module),
       path: [:attributes],
       message: message
     )}
  end

  defp validate_attribute_not_nil(_, _), do: :ok

  defp maybe_validate_pk_present([], dsl_state) do
    if Transformer.get_option(dsl_state, [:resource], :require_primary_key?, true) do
      message = """
      By default, resources must have a primary key.

      See `d:Ash.Resource.Dsl.resource.require_primary_key?` for more information.
      """

      {:error,
       DslError.exception(
         module: Transformer.get_persisted(dsl_state, :module),
         path: [:attributes],
         message: message
       )}
    else
      :ok
    end
  end

  defp maybe_validate_pk_present(_, _), do: :ok

  def after?(Ash.Resource.Transformers.BelongsToAttribute), do: true
  def after?(Ash.Resource.Transformers.DefaultPrimaryKey), do: true

  def after?(_), do: false
end