lib/validation/topo.ex

defmodule AshGeo.Validation.Topo do
  @opts_schema [
    function: [
      type: {:in, AshGeo.topo_functions()},
      required: true,
      doc: "Topo function to use for comparison"
    ],
    geometry_a: [
      type: :atom,
      required: true,
      doc: "Geometry A"
    ],
    geometry_b: [
      type: :atom,
      required: true,
      doc: "Geometry B"
    ]
  ]

  @moduledoc """
  Validate that the specified `Topo` function return true.

  ### Options

  #{Spark.Options.docs(@opts_schema)}

  ### See also

  - `AshGeo.topo_functions/0`
  """
  @moduledoc since: "0.1.0"

  use Ash.Resource.Validation
  alias Ash.Error.Changes.InvalidArgument

  @impl Ash.Resource.Validation
  def init(opts) do
    case Spark.Options.validate(opts, @opts_schema) do
      {:ok, opts} ->
        {:ok, opts}

      {:error, error} ->
        {:error, Exception.message(error)}
    end
  end

  @impl Ash.Resource.Validation
  def validate(cs, opts, _context) do
    geometry_a = attribute_value(cs, opts[:geometry_a])
    geometry_b = attribute_value(cs, opts[:geometry_b])

    case apply(Topo, opts[:function], [geometry_a, geometry_b]) do
      true -> :ok
      false -> invalid_argument_error(opts, geometry_a, geometry_b)
    end
  end

  defp attribute_value(_cs, attribute) when is_function(attribute, 0) do
    attribute.()
  end

  defp attribute_value(cs, attribute) when is_atom(attribute) do
    Ash.Changeset.get_argument_or_attribute(cs, attribute)
  end

  defp attribute_value(_, attribute), do: attribute

  defp invalid_argument_error(opts, geometry_a, geometry_b) do
    {:error,
     InvalidArgument.exception(
       field: opts[:geometry_a],
       value: geometry_a,
       message:
         case opts[:function] do
           :contains? -> "#{geometry_a} does not contain #{geometry_b}"
           :disjoint? -> "#{geometry_a} is not disjoint with #{geometry_b}"
           :equals? -> "#{geometry_a} does not equal #{geometry_b}"
           :intersects? -> "#{geometry_a} does not intersect #{geometry_b}"
           :within? -> "#{geometry_a} is not within #{geometry_b}"
         end
     )}
  end
end