lib/ash/resource/transformers/validate_accept.ex

defmodule Ash.Resource.Transformers.ValidateAccept do
  @moduledoc "Validates that accept and reject lists only contain valid attributes"
  use Spark.Dsl.Transformer

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

  @impl true
  def after_compile?, do: true

  @impl true
  def transform(dsl_state) do
    {private_attributes, public_attributes} =
      dsl_state
      |> Transformer.get_entities([:attributes])
      |> Enum.split_with(& &1.private?)

    public_attribute_names = MapSet.new(public_attributes, & &1.name)
    private_attribute_names = MapSet.new(private_attributes, & &1.name)

    initial_errors = %{private: [], not_attribute: []}

    result =
      Transformer.get_entities(dsl_state, [:actions])
      |> Enum.reduce(%{}, fn
        %{name: action_name, accept: accept, reject: reject}, acc ->
          validate_attribute_name = fn attribute_name ->
            cond do
              MapSet.member?(private_attribute_names, attribute_name) ->
                :private

              MapSet.member?(public_attribute_names, attribute_name) ->
                :ok

              true ->
                :not_attribute
            end
          end

          accept_errors =
            Enum.reduce(
              accept,
              initial_errors,
              fn attribute, %{private: private, not_attribute: not_attribute} = acc ->
                case validate_attribute_name.(attribute) do
                  :ok ->
                    acc

                  :private ->
                    %{private: [attribute | private], not_attribute: not_attribute}

                  :not_attribute ->
                    %{private: private, not_attribute: [attribute | not_attribute]}
                end
              end
            )

          reject_errors =
            Enum.reduce(
              reject,
              initial_errors,
              fn attribute, %{private: private, not_attribute: not_attribute} = acc ->
                case validate_attribute_name.(attribute) do
                  :ok ->
                    acc

                  :private ->
                    %{private: [attribute | private], not_attribute: not_attribute}

                  :not_attribute ->
                    %{private: private, not_attribute: [attribute | not_attribute]}
                end
              end
            )

          if accept_errors == initial_errors and reject_errors == initial_errors do
            acc
          else
            Map.put(acc, action_name, %{
              accept: accept_errors,
              reject: reject_errors
            })
          end

        _, acc ->
          acc
      end)
      |> Enum.map(fn {action, %{accept: accept, reject: reject}} ->
        accept_private = accept.private |> Enum.reverse()
        accept_not_attribute = accept.not_attribute |> Enum.reverse()
        reject_private = reject.private |> Enum.reverse()
        reject_not_attribute = reject.not_attribute |> Enum.reverse()

        [
          message(accept_private, "are private attributes", [:actions, action, :accept]),
          message(accept_not_attribute, "are not attributes", [:actions, action, :accept]),
          message(reject_private, "are private attributes", [:actions, action, :reject]),
          message(reject_not_attribute, "are not attributes", [:actions, action, :reject])
        ]
        |> Enum.reject(&(&1 == ""))
        |> Enum.join("\n")
      end)

    if result == [] do
      :ok
    else
      raise DslError,
        message: Enum.join(result, "\n"),
        path: []
    end
  end

  defp message(keys, _message, _path) when keys == [] do
    ""
  end

  defp message(keys, message, path) do
    dsl_path = Enum.join(path, " -> ")
    "#{dsl_path}:\n  #{get_message(keys, message)}"
  end

  defp get_message(keys, message) do
    "#{inspect(keys)} #{message}"
  end
end