lib/ash/resource/validation/builtins.ex

defmodule Ash.Resource.Validation.Builtins do
  @moduledoc """
  Built in validations that are available to all resources

  The functions in this module are imported by default in the validations section.
  """

  alias Ash.Resource.Validation

  @doc """
  Validates that an attribute's value is in a given list
  """
  def one_of(attribute, values) do
    {Validation.OneOf, attribute: attribute, values: values}
  end

  @doc "Validates that an attribute is being changed"
  def changing(field) do
    {Validation.Changing, field: field}
  end

  @doc "Validates that a field or argument matches another field or argument"
  def confirm(field, confirmation) do
    {Validation.Confirm, [field: field, confirmation: confirmation]}
  end

  @doc "Validates that an attribute on the original record does not equal a specific value"
  def attribute_does_not_equal(attribute, value) do
    {Validation.AttributeDoesNotEqual, attribute: attribute, value: value}
  end

  @doc "Validates that an attribute on the original record equals a specific value"
  def attribute_equals(attribute, value) do
    {Validation.AttributeEquals, attribute: attribute, value: value}
  end

  @doc "Validates that an attribute on the original record meets the given length criteria"
  def string_length(attribute, opts \\ []) do
    {Validation.StringLength, Keyword.merge(opts, attribute: attribute)}
  end

  @doc "Validates that attribute meets the given criteria"
  def compare(attribute, opts \\ []) do
    {Validation.Compare, Keyword.merge(opts, attribute: attribute)}
  end

  @doc """
  Validates that an attribute's value matches a given regex or string, using the provided error, message if not.

  `String.match?/2` is used to determine if it matches.
  """

  def match(attribute, match, message \\ nil) do
    message = message || "must match #{match}"

    {Validation.Match, attribute: attribute, match: match, message: message}
  end

  @doc """
  Validates the presence of a list of attributes

  If no options are provided, validates that they are all present.

  #{Ash.OptionsHelpers.docs(Keyword.delete(Validation.Present.schema(), :attributes))}
  """
  def present(attributes, opts \\ []) do
    if opts == [] do
      attributes = List.wrap(attributes)
      {Validation.Present, attributes: attributes, exactly: Enum.count(attributes)}
    else
      opts = Keyword.put(opts, :attributes, List.wrap(attributes))
      {Validation.Present, opts}
    end
  end

  @doc """
  Validates the absence of a list of attributes

  If no options are provided, validates that they are all absent.

  The docs behave the same as `present/2`, except they validate absence.
  """
  def absent(attributes, opts \\ []) do
    if opts == [] do
      {Validation.Present, attributes: List.wrap(attributes), exactly: 0}
    else
      attributes = List.wrap(attributes)
      count = Enum.count(attributes)

      new_opts =
        case Keyword.fetch(opts, :at_least) do
          {:ok, value} ->
            Keyword.put(opts, :at_most, count - value)

          :error ->
            Keyword.put(opts, :at_most, 0)
        end

      new_opts =
        case Keyword.fetch(opts, :at_most) do
          {:ok, value} ->
            Keyword.put(new_opts, :at_least, count - value)

          :error ->
            Keyword.put(new_opts, :at_least, 0)
        end

      present(attributes, new_opts)
    end
  end
end