defmodule Ash.Resource.Validation do @moduledoc """ Represents a validation in Ash. See `Ash.Resource.Validation.Builtins` for a list of builtin validations. To write your own validation, define a module that implements the `c:init/1` callback to validate options at compile time, and `c:validate/2` callback to do the validation. Then, in a resource, you can say: ``` validations do validation {MyValidation, [foo: :bar]} end ``` To make it more readable, you can define a function in the module that returns that tuple, and import it into your resource. ``` defmodule MyValidation do def my_validation(value) do {__MODULE__, foo: value} end end ``` ``` defmodule MyResource do ... import MyValidation validations do validate my_validation(:foo) end end ``` """ defstruct [ :validation, :module, :opts, :expensive?, :description, :message, :before_action?, :where, on: [] ] @type t :: %__MODULE__{ validation: {atom(), list(atom())}, module: atom(), opts: list(atom()), expensive?: boolean(), description: String.t() | nil, where: list({atom(), list(atom())}), on: list(atom()) } @type path :: [atom | integer] @callback init(Keyword.t()) :: {:ok, Keyword.t()} | {:error, String.t()} @callback validate(Ash.Changeset.t(), Keyword.t()) :: :ok | {:error, term} @schema [ validation: [ type: {:ash_behaviour, Ash.Resource.Validation, Ash.Resource.Validation.Builtins}, required: true, doc: "The module/opts pair of the validation" ], where: [ type: {:list, {:ash_behaviour, Ash.Resource.Validation, Ash.Resource.Validation.Builtins}}, required: false, doc: """ Validations that should pass in order for this validation to apply. These validations failing will not invalidate the changes, but instead just result in this validation being ignored. """ ], on: [ type: {:custom, __MODULE__, :on, []}, default: [:create, :update], doc: """ The action types the validation should run on. Many validations don't make sense in the context of deletion, so by default it is left out of the list. """ ], expensive?: [ type: :boolean, default: false, doc: "If a validation is expensive, it won't be run on invalid changes. All inexpensive validations are always run, to provide informative errors." ], message: [ type: :string, doc: "If provided, overrides any message set by the validation error" ], description: [ type: :string, doc: "An optional description for the validation" ], before_action?: [ type: :boolean, default: false, doc: "If set to `true`, the validation is not run when building changesets using `Ash.Changeset.for_*`. The validation will only ever be run once the action itself is called." ] ] @action_schema Keyword.delete(@schema, :on) defmacro __using__(_) do quote do @behaviour Ash.Resource.Validation def init(opts), do: {:ok, opts} defoverridable init: 1 end end @doc false def transform(%{validation: {module, opts}} = validation) do case module.init(opts) do {:ok, opts} -> {:ok, %{validation | validation: {module, opts}, module: module, opts: opts}} {:error, error} -> {:error, error} end end def opt_schema, do: @schema def action_schema, do: @action_schema def on(list) do list |> List.wrap() |> Enum.all?(&(&1 in [:create, :update, :destroy])) |> case do true -> {:ok, List.wrap(list)} false -> {:error, "Expected items of [:create, :update, :destroy], got: #{inspect(list)}"} end end end