lib/absinthe/constraints/phase.ex

defmodule Absinthe.Constrains.Phase do
  @moduledoc """
  Defines an Absinthe Document Phase that validates inputs against constraints defined by the `constraints` directive.

  Use add_to_pipeline/2 to add this phase to your Absinthe pipeline.
  """
  use Absinthe.Phase

  alias Absinthe.Blueprint

  alias Absinthe.Constrains.Validator

  def add_to_pipeline(pipeline, opts) do
    Absinthe.Pipeline.insert_before(pipeline, Absinthe.Phase.Document.Validation.Result, {__MODULE__, opts})
  end

  @impl Absinthe.Phase
  def run(blueprint, _opts) do
    # We use postwalk instead of prewalk in order to append the location to errors in input object fields.
    result = Blueprint.postwalk(blueprint, &handle_node/1)
    {:ok, result}
  end

  # This matches: Absinthe.Blueprint.Input.Argument
  # We handle this node in order to append the location to errors in input field coming from query variables.
  # This also requires a postwalk, in order to first handle each input object field separatly withouth the location,
  # and then append the location to every error that doesn't have one.
  defp handle_node(
         %{
           input_value: %{
             normalized: %Absinthe.Blueprint.Input.Object{fields: _}
           },
           source_location: source_location
         } = node
       ) do
    update_in(
      node,
      [
        Access.key(:input_value),
        Access.key(:normalized),
        Access.key(:fields),
        Access.all(),
        Access.key(:errors),
        Access.all(),
        Access.key(:locations)
      ],
      fn
        locations when is_nil(locations) or locations == [] ->
          locations ++ [source_location]

        locations ->
          locations
      end
    )
  end

  # Handles the following nodes:
  # Absinthe.Blueprint.Input.Field
  # Absinthe.Blueprint.Input.Argument
  defp handle_node(
         %{
           input_value: %{normalized: %{value: value}},
           schema_node: %{__private__: private}
         } = node
       ) do
    Keyword.get(private, :constraints, [])
    |> Enum.flat_map(&handle_constraint(&1, value, node))
    |> Enum.reduce(node, &add_error/2)
  end

  defp handle_node(node), do: node

  defp handle_constraint(config, value, node) do
    Validator.handle_constraint(config, value)
    |> make_errors(node)
  end

  defp make_errors(error_messages, %{name: name, source_location: source_location}) do
    error_messages
    |> Enum.map(fn message ->
      %Absinthe.Phase.Error{
        phase: __MODULE__,
        message: "\"#{name}\" #{message}",
        locations: List.wrap(source_location)
      }
    end)
  end

  defp add_error(error, node), do: Absinthe.Phase.put_error(node, error)
end