lib/ash_phoenix/form/wrapped_value.ex

defmodule AshPhoenix.Form.WrappedValue do
  @moduledoc "A sentinal value used when editing a union that has non-map values"
  use Ash.Resource,
    data_layer: :embedded

  attributes do
    attribute :value, :term
  end

  actions do
    create :create do
      primary? true
    end

    update :update do
      primary? true
    end
  end

  changes do
    change fn changeset, _ ->
      if Ash.Changeset.changing_attribute?(changeset, :value) do
        value = Ash.Changeset.get_attribute(changeset, :value)

        with {:ok, casted} <-
               Ash.Type.Helpers.cast_input(
                 changeset.context.type,
                 value,
                 changeset.context.constraints,
                 changeset
               ),
             {:constrained, {:ok, casted}} when not is_nil(casted) <-
               {:constrained,
                Ash.Type.apply_constraints(
                  changeset.context.type,
                  casted,
                  changeset.context.constraints
                )} do
          Ash.Changeset.force_change_attribute(changeset, :value, casted)
        else
          {:constrained, {:ok, nil}} ->
            Ash.Changeset.force_change_attribute(changeset, :value, nil)

          {:constrained, {:error, error}, argument} ->
            add_invalid_errors(value, changeset, :value, error)

          {:error, error} ->
            add_invalid_errors(value, changeset, :value, error)
        end
      else
        changeset
      end
    end
  end

  defp add_invalid_errors(value, changeset, attribute, message) do
    messages =
      if Keyword.keyword?(message) do
        [message]
      else
        List.wrap(message)
      end

    Enum.reduce(messages, changeset, fn message, changeset ->
      if is_exception(message) do
        error =
          message
          |> Ash.Error.to_ash_error()

        errors =
          case error do
            %class{errors: errors}
            when class in [
                   Ash.Error.Invalid,
                   Ash.Error.Unknown,
                   Ash.Error.Forbidden,
                   Ash.Error.Framework
                 ] ->
              errors

            error ->
              [error]
          end

        Enum.reduce(errors, changeset, fn error, changeset ->
          Ash.Changeset.add_error(changeset, Ash.Error.set_path(error, attribute))
        end)
      else
        opts = Ash.Type.Helpers.error_to_exception_opts(message, %{name: attribute})

        Enum.reduce(opts, changeset, fn opts, changeset ->
          error =
            Ash.Error.Changes.InvalidAttribute.exception(
              value: value,
              field: Keyword.get(opts, :field),
              message: Keyword.get(opts, :message),
              vars: opts
            )

          error =
            if opts[:path] do
              Ash.Error.set_path(error, opts[:path])
            else
              error
            end

          Ash.Changeset.add_error(changeset, error)
        end)
      end
    end)
  end
end