lib/middleware/deprecated_args.ex

defmodule AbsintheUtils.Middleware.DeprecatedArgs do
  @moduledoc """
  Absinthe middleware for handling deprecated or renamed `field` arguments (`arg`).

  It automatically handles mutual exclusivity and changing the argument keys (names)
  before passing it to the resolver.
  For this to work, this middleware must be added before the resolver.

  Middleware options:

   - `legacy_arg_identifier`: The identifier of the field that has been deprecated.
     If this argument is provided, this middleware will rename it to the `new_arg_identifier`.

   - `new_arg_identifier`: The identifier of the new field, if passed, nothing changes.

   - `is_required`: if at least one of the arguments must be provided.
     If the validation fails, an Absinthe compliant error will be returned and
     the operation marked as resolved (it will not reach the resolver).

  All options are required.

  ## Example usage

  ```
  field :query_with_deprecated_required_args, non_null(:string) do
    arg(:old_arg, :string, deprecate: "Use `newParam` instead.")
    arg(:new_arg, :string)

    # Add the middleware before your resolver
    middleware(
      DeprecatedArgs,
      %{
        legacy_arg_identifier: :old_arg,
        new_arg_identifier: :new_arg,
        is_required: true
      }
    )

    resolve(&MyApp.my_resolver/3)
  end
  ```
  """

  alias AbsintheUtils.Helpers.Errors

  @behaviour Absinthe.Middleware

  @impl true
  def call(
        resolution = %{arguments: arguments},
        %{
          legacy_arg_identifier: legacy_arg_identifier,
          new_arg_identifier: new_arg_identifier,
          is_required: _is_required
        }
      )
      when is_map_key(arguments, legacy_arg_identifier) and
             is_map_key(arguments, new_arg_identifier) do
    Absinthe.Resolution.put_result(
      resolution,
      {
        :error,
        %{
          message:
            "Arguments #{camelize(legacy_arg_identifier)} and " <>
              "#{camelize(new_arg_identifier)} cannot be passed together",
          extensions: %{code: "MUTUALLY_EXCLUSIVE_ARG_VIOLATION"}
        }
      }
    )
  end

  @impl true
  def call(
        resolution = %{arguments: arguments},
        %{
          legacy_arg_identifier: legacy_arg_identifier,
          new_arg_identifier: new_arg_identifier,
          is_required: _is_required
        } = opts
      )
      when is_map_key(arguments, legacy_arg_identifier) do
    {value, arguments} = Map.pop(arguments, legacy_arg_identifier)

    if Map.get(opts, :non_null, false) == true and value === nil do
      Errors.put_error(
        resolution,
        "Argument #{camelize(legacy_arg_identifier)} cannot be null",
        "NOT_NULL_VIOLATION"
      )
    else
      %{
        resolution
        | arguments: Map.put(arguments, new_arg_identifier, value)
      }
    end
  end

  @impl true
  def call(
        resolution = %{arguments: arguments},
        %{
          legacy_arg_identifier: _legacy_arg_identifier,
          new_arg_identifier: new_arg_identifier,
          is_required: _is_required
        } = opts
      )
      when is_map_key(arguments, new_arg_identifier) do
    if Map.get(opts, :non_null, false) == true and
         Map.get(arguments, new_arg_identifier) == nil do
      Errors.put_error(
        resolution,
        "Argument #{camelize(new_arg_identifier)} cannot be null",
        "NOT_NULL_VIOLATION"
      )
    else
      resolution
    end
  end

  @impl true
  def call(
        resolution,
        %{
          legacy_arg_identifier: _legacy_arg_identifier,
          new_arg_identifier: _new_arg_identifier,
          is_required: false
        }
      ) do
    resolution
  end

  @impl true
  def call(
        resolution,
        %{
          legacy_arg_identifier: legacy_arg_identifier,
          new_arg_identifier: new_arg_identifier,
          is_required: true
        }
      ) do
    Errors.put_error(
      resolution,
      "Exactly one of the following arguments must be provided: " <>
        "#{camelize(legacy_arg_identifier)}, #{camelize(new_arg_identifier)}",
      "MUTUALLY_EXCLUSIVE_ARG_VIOLATION"
    )
  end

  defp camelize(atom) do
    atom
    |> to_string()
    |> Absinthe.Utils.camelize(lower: true)
  end
end