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
validate {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",
links: []
],
where: [
type: {:list, {:ash_behaviour, Ash.Resource.Validation, Ash.Resource.Validation.Builtins}},
required: false,
default: [],
links: [
modules: [
"ash:module:Ash.Resource.Validation.Builtins"
]
],
doc: """
Validations that should pass in order for this validation to apply.
These validations failing will not invalidate the changes, but will instead result in this validation being ignored.
"""
],
on: [
type: {:custom, __MODULE__, :on, []},
default: [:create, :update],
links: [],
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,
links: [],
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",
links: []
],
description: [
type: :string,
doc: "An optional description for the validation",
links: []
],
before_action?: [
type: :boolean,
default: false,
links: [],
doc: "If set to `true`, the validation will be run in a before_action hook"
]
]
@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