lib/ash/resource/transformers/validate_manage_relationship_opts.ex

defmodule Ash.Resource.Transformers.ValidateManagedRelationshipOpts do
  @moduledoc """
  Confirms that all action types declared on a resource are supported by its data layer
  """
  use Spark.Dsl.Transformer

  alias Ash.Changeset.ManagedRelationshipHelpers
  alias Spark.Dsl.Transformer
  require Logger

  def after_compile?, do: true

  def transform(dsl_state) do
    relationships = Transformer.get_entities(dsl_state, [:relationships])

    dsl_state
    |> Transformer.get_entities([:actions])
    |> Enum.reject(&(&1.type == :read))
    |> Enum.each(fn action ->
      action.changes
      |> Enum.filter(
        &match?(%Ash.Resource.Change{change: {Ash.Resource.Change.ManageRelationship, _}}, &1)
      )
      |> Enum.each(fn %Ash.Resource.Change{change: {_, opts}} ->
        unless Enum.find(action.arguments, &(&1.name == opts[:argument])) do
          raise Spark.Error.DslError,
            path:
              [
                :actions,
                action.type,
                action.name,
                :change,
                :manage_relationship
              ] ++ Enum.uniq([opts[:argument], opts[:relationship]]),
            message: "Action #{action.name} has no argument `#{inspect(opts[:argument])}`."
        end

        relationship =
          Enum.find(relationships, &(&1.name == opts[:relationship])) ||
            raise Spark.Error.DslError,
              path:
                [
                  :actions,
                  action.type,
                  action.name,
                  :change,
                  :manage_relationship
                ] ++ Enum.uniq([opts[:argument], opts[:relationship]]),
              message: "No such relationship #{opts[:relationship]} exists."

        if ensure_compiled?(relationship) do
          try do
            opts =
              if opts[:opts][:type] == :replace do
                Logger.warn(
                  "type: :replace has been renamed to `:append_and_remove` in 2.0, and it will be removed in 2.1"
                )

                Keyword.update!(opts, :opts, fn inner_opts ->
                  Keyword.put(inner_opts, :type, :append_and_remove)
                end)
              else
                opts
              end

            manage_opts =
              if opts[:opts][:type] do
                defaults = Ash.Changeset.manage_relationship_opts(opts[:opts][:type])

                Enum.reduce(defaults, Ash.Changeset.manage_relationship_schema(), fn {key, value},
                                                                                     manage_opts ->
                  Spark.OptionsHelpers.set_default!(manage_opts, key, value)
                end)
              else
                Ash.Changeset.manage_relationship_schema()
              end

            opts = Spark.OptionsHelpers.validate!(opts[:opts], manage_opts)

            ManagedRelationshipHelpers.sanitize_opts(relationship, opts)
          rescue
            e ->
              reraise Spark.Error.DslError,
                      [
                        path:
                          [
                            :actions,
                            action.type,
                            action.name,
                            :change,
                            :manage_relationship
                          ] ++ Enum.uniq([opts[:argument], opts[:relationship]]),
                        message: """
                        The following error was raised when validating options provided to manage_relationship.

                        #{Exception.format(:error, e, __STACKTRACE__)}
                        """
                      ],
                      __STACKTRACE__
          end
        end
      end)
    end)

    {:ok, dsl_state}
  end

  defp ensure_compiled?(%Ash.Resource.Relationships.ManyToMany{
         through: through,
         destination: destination
       }) do
    Code.ensure_loaded?(through) && Code.ensure_loaded?(destination)
  end

  defp ensure_compiled?(%{destination: destination}) do
    Code.ensure_loaded?(destination)
  end
end