defmodule Ash.Resource.Change do
@moduledoc """
The behaviour for an action-specific resource change.
`c:init/1` is defined automatically by `use Ash.Resource.Change`, but can be implemented if you want to validate/transform any
options passed to the module.
The main function is `c:change/3`. It takes the changeset, any options that were provided
when this change was configured on a resource, and the context, which currently only has
the actor.
"""
defstruct [:change, :on, :only_when_valid?, :description, where: []]
@type t :: %__MODULE__{}
@type ref :: {module(), Keyword.t()} | module()
@doc false
def schema do
[
on: [
type: {:custom, __MODULE__, :on, []},
default: [:create, :update],
doc: """
The action types the validation should run on. Destroy actions are omitted by default as most changes don't make sense for a destroy.
"""
],
only_when_valid?: [
type: :boolean,
default: false,
doc: """
If the change should only be run on valid changes. By default, all changes are run unless stated otherwise here.
"""
],
description: [
type: :string,
doc: "An optional description for the change"
],
change: [
type:
{:spark_function_behaviour, Ash.Resource.Change, Ash.Resource.Change.Builtins,
{Ash.Resource.Change.Function, 2}},
doc: """
The module and options for a change.
Also accepts a function that takes the changeset and the context.
See `Ash.Resource.Change.Builtins` for more.
""",
required: true
],
where: [
type:
{:list,
{:spark_function_behaviour, Ash.Resource.Validation, Ash.Resource.Validation.Builtins,
{Ash.Resource.Validation.Function, 1}}},
required: false,
default: [],
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 change being ignored.
Also accepts functions take the changeset.
"""
]
]
end
@doc false
def action_schema do
Keyword.delete(schema(), :on)
end
@doc false
def change({module, opts}) when is_atom(module) do
if Keyword.keyword?(opts) do
{:ok, {module, opts}}
else
{:error, "Expected opts to be a keyword, got: #{inspect(opts)}"}
end
end
def change(module) when is_atom(module), do: {:ok, {module, []}}
def change(other) do
{:error, "Expected a module and opts, got: #{inspect(other)}"}
end
@doc false
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
@type context :: %{
optional(:actor) => Ash.Resource.record(),
optional(any) => any
}
@callback init(Keyword.t()) :: {:ok, Keyword.t()} | {:error, term}
@callback change(Ash.Changeset.t(), Keyword.t(), context) :: Ash.Changeset.t()
@callback batch_change([Ash.Changeset.t()], Keyword.t(), context) ::
Enumerable.t(Ash.Changeset.t() | Ash.Notifier.Notification.t())
@callback before_batch([Ash.Changeset.t()], Keyword.t(), context) ::
Enumerable.t(Ash.Changeset.t() | Ash.Notifier.Notification.t())
@callback after_batch(
[{Ash.Changeset.t(), Ash.Resource.record()}],
Keyword.t(),
context
) ::
Enumerable.t(
{:ok, Ash.Resource.record()}
| {:error, Ash.Error.t()}
| Ash.Notifier.Notification.t()
)
@optional_callbacks before_batch: 3, after_batch: 3, batch_change: 3
defmacro __using__(_) do
quote do
@behaviour Ash.Resource.Change
def init(opts), do: {:ok, opts}
defoverridable init: 1
end
end
end