improvement: add some atomic implementations

This commit is contained in:
Zach Daniel 2024-01-03 16:48:15 -05:00
parent 746fc5df53
commit 6061a2a16a
10 changed files with 172 additions and 29 deletions

View file

@ -567,30 +567,42 @@ defmodule Ash.Changeset do
end end
%{validation: {module, validation_opts}, where: where}, changeset -> %{validation: {module, validation_opts}, where: where}, changeset ->
with {:atomic, fields, condition_expr, error_expr} <- case List.wrap(module.atomic(changeset, validation_opts)) do
module.atomic(changeset, validation_opts), [{:atomic, _, _, _} | _] = atomics ->
{:changing?, true} <- Enum.reduce_while(atomics, changeset, fn
{:changing?, Enum.any?(fields, &changing_attribute?(changeset, &1))}, {:atomic, fields, condition_expr, error_expr}, changeset ->
{:atomic, condition} <- atomic_condition(where, changeset) do with {:changing?, true} <-
case condition do {:changing?,
true -> fields == :* || Enum.any?(fields, &changing_attribute?(changeset, &1))},
{:cont, validate_atomically(changeset, condition_expr, error_expr)} {:atomic, condition} <- atomic_condition(where, changeset) do
case condition do
true ->
{:cont, validate_atomically(changeset, condition_expr, error_expr)}
false -> false ->
{:cont, changeset} {:cont, changeset}
condition -> condition ->
condition_expr = condition_expr =
Ash.Expr.expr(^condition and condition_expr) Ash.Expr.expr(^condition and condition_expr)
{:cont, validate_atomically(changeset, condition_expr, error_expr)} {:cont, validate_atomically(changeset, condition_expr, error_expr)}
end end
else else
:not_atomic -> {:changing?, false} ->
{:halt, :not_atomic} {:cont, changeset}
{:changing?, false} -> :not_atomic ->
{:cont, changeset} {:halt, :not_atomic}
end
end)
|> case do
:not_atomic -> {:halt, :not_atomic}
changeset -> {:cont, changeset}
end
[value] ->
{:halt, value}
end end
end) end)
end end
@ -630,13 +642,24 @@ defmodule Ash.Changeset do
Gets a reference to a field, or the current atomic update expression of that field. Gets a reference to a field, or the current atomic update expression of that field.
""" """
def atomic_ref(changeset, field) do def atomic_ref(changeset, field) do
if base_value = changeset.atomics[field] do case Keyword.fetch(changeset.atomics, field) do
%{type: type, constraints: constraints} = {:ok, atomic} ->
Ash.Resource.Info.attribute(changeset.resource, field) %{type: type, constraints: constraints} =
Ash.Resource.Info.attribute(changeset.resource, field)
Ash.Expr.expr(type(^base_value, ^type, ^constraints)) Ash.Expr.expr(type(^atomic, ^type, ^constraints))
else
Ash.Expr.expr(ref(^field)) :error ->
case Map.fetch(changeset.attributes, field) do
{:ok, new_value} ->
%{type: type, constraints: constraints} =
Ash.Resource.Info.attribute(changeset.resource, field)
Ash.Expr.expr(type(^new_value, ^type, ^constraints))
:error ->
Ash.Expr.expr(ref(^field))
end
end end
end end

View file

@ -48,8 +48,12 @@ defmodule Ash.Resource.Validation do
@callback atomic?() :: boolean @callback atomic?() :: boolean
@callback atomic(changeset :: Ash.Changeset.t(), opts :: Keyword.t()) :: @callback atomic(changeset :: Ash.Changeset.t(), opts :: Keyword.t()) ::
:ok :ok
| {:atomic, involved_fields :: list(atom), condition_expr :: Ash.Expr.t(), | {:atomic, involved_fields :: list(atom) | :*, condition_expr :: Ash.Expr.t(),
error_expr :: Ash.Expr.t()} error_expr :: Ash.Expr.t()}
| [
{:atomic, involved_fields :: list(atom) | :*, condition_expr :: Ash.Expr.t(),
error_expr :: Ash.Expr.t()}
]
| :not_atomic | :not_atomic
| {:error, term()} | {:error, term()}

View file

@ -15,6 +15,11 @@ defmodule Ash.Resource.Validation.ActionIs do
end end
end end
@impl true
def atomic(changeset, opts) do
validate(changeset, opts)
end
@impl true @impl true
def describe(opts) do def describe(opts) do
[message: "must be %{action}", vars: %{action: opts[:action]}] [message: "must be %{action}", vars: %{action: opts[:action]}]

View file

@ -43,6 +43,11 @@ defmodule Ash.Resource.Validation.ArgumentDoesNotEqual do
end end
end end
@impl true
def atomic(changeset, opts) do
validate(changeset, opts)
end
@impl true @impl true
def describe(opts) do def describe(opts) do
[ [

View file

@ -42,6 +42,11 @@ defmodule Ash.Resource.Validation.ArgumentEquals do
end end
end end
@impl true
def atomic(changeset, opts) do
validate(changeset, opts)
end
@impl true @impl true
def describe(opts) do def describe(opts) do
[ [

View file

@ -42,6 +42,11 @@ defmodule Ash.Resource.Validation.ArgumentIn do
end end
end end
@impl true
def atomic(changeset, opts) do
validate(changeset, opts)
end
@impl true @impl true
def describe(opts) do def describe(opts) do
[ [

View file

@ -16,6 +16,7 @@ defmodule Ash.Resource.Validation.AttributeIn do
use Ash.Resource.Validation use Ash.Resource.Validation
alias Ash.Error.Changes.InvalidAttribute alias Ash.Error.Changes.InvalidAttribute
require Ash.Expr
@impl true @impl true
def init(opts) do def init(opts) do
@ -42,11 +43,26 @@ defmodule Ash.Resource.Validation.AttributeIn do
end end
end end
@impl true
def atomic(changeset, opts) do
field_value = Ash.Changeset.atomic_ref(changeset, opts[:attribute])
{:atomic, [opts[:attribute]], Ash.Expr.expr(^field_value in ^opts[:list]),
Ash.Expr.expr(
error(^InvalidAttribute, %{
field: ^opts[:attribute],
value: ^field_value,
message: "must be in %{list}",
vars: %{field: ^opts[:attribute], list: ^opts[:list]}
})
)}
end
@impl true @impl true
def describe(opts) do def describe(opts) do
[ [
message: "must equal %{value}", message: "must be in %{list}",
vars: [field: opts[:attribute], value: opts[:value]] vars: [field: opts[:attribute], list: opts[:list]]
] ]
end end
end end

View file

@ -4,6 +4,7 @@ defmodule Ash.Resource.Validation.Changing do
use Ash.Resource.Validation use Ash.Resource.Validation
alias Ash.Error.Changes.InvalidAttribute alias Ash.Error.Changes.InvalidAttribute
require Ash.Expr
@opt_schema [ @opt_schema [
field: [ field: [
@ -51,6 +52,22 @@ defmodule Ash.Resource.Validation.Changing do
end end
end end
@impl true
def atomic(changeset, opts) do
new_value = Ash.Changeset.atomic_ref(changeset, opts[:field])
old_value = Ash.Expr.expr(ref(^opts[:field]))
{:atomic, [opts[:field]], Ash.Expr.expr(^new_value != ^old_value),
Ash.Expr.expr(
error(^InvalidAttribute, %{
field: ^opts[:field],
value: ^new_value,
message: "must be changing",
vars: %{field: ^opts[:field]}
})
)}
end
@impl true @impl true
def describe(_opts) do def describe(_opts) do
[ [

View file

@ -4,6 +4,7 @@ defmodule Ash.Resource.Validation.Compare do
use Ash.Resource.Validation use Ash.Resource.Validation
alias Ash.Error.Changes.InvalidAttribute alias Ash.Error.Changes.InvalidAttribute
require Ash.Expr
@impl true @impl true
def init(opts) do def init(opts) do
@ -67,6 +68,66 @@ defmodule Ash.Resource.Validation.Compare do
end end
end end
def atomic(changeset, opts) do
case Ash.Changeset.fetch_argument(changeset, opts[:attribute]) do
:error ->
ref = Ash.Changeset.atomic_ref(changeset, opts[:attribute])
opts
|> Keyword.take([
:greater_than,
:less_than,
:greater_than_or_equal_to,
:less_than_or_equal_to
])
|> Enum.map(fn
{:greater_than, value} ->
{:atomic, [opts[:attribute]], Ash.Expr.expr(^ref <= ^value),
Ash.Expr.expr(
error(^InvalidAttribute, %{
field: ^opts[:attribute],
value: ^ref,
message: "must be greater than %{greater_than}",
vars: %{field: ^opts[:attribute], greater_than: ^value}
})
)}
{:less_than, value} ->
{:atomic, [opts[:attribute]], Ash.Expr.expr(^ref >= ^value),
Ash.Expr.expr(
error(^InvalidAttribute, %{
field: ^opts[:attribute],
value: ^ref,
message: "must be less than %{less_than}",
vars: %{field: ^opts[:attribute], less_than: ^value}
})
)}
{:greater_than_or_equal_to, value} ->
{:atomic, [opts[:attribute]], Ash.Expr.expr(^ref < ^value),
Ash.Expr.expr(
error(^InvalidAttribute, %{
field: ^opts[:attribute],
value: ^ref,
message: "must be greater than or equal to %{greater_than_or_equal_to}",
vars: %{field: ^opts[:attribute], greater_than_or_equal_to: ^value}
})
)}
{:less_than_or_equal_to, value} ->
{:atomic, [opts[:attribute]], Ash.Expr.expr(^ref > ^value),
Ash.Expr.expr(
error(^InvalidAttribute, %{
field: ^opts[:attribute],
value: ^ref,
message: "must be less than or equal to %{less_than_or_equal_to}",
vars: %{field: ^opts[:attribute], less_than_or_equal_to: ^value}
})
)}
end)
end
end
@impl true @impl true
def describe(opts) do def describe(opts) do
[ [

View file

@ -105,6 +105,7 @@ defmodule Ash.Test.Actions.UpdateTest do
accept([:name]) accept([:name])
validate attribute_equals(:name, "fred") validate attribute_equals(:name, "fred")
validate compare(:score, greater_than_or_equal_to: 0, less_than_or_equal_to: 10)
end end
update :duplicate_name do update :duplicate_name do
@ -128,6 +129,7 @@ defmodule Ash.Test.Actions.UpdateTest do
uuid_primary_key :id uuid_primary_key :id
attribute :name, :string attribute :name, :string
attribute :bio, :string attribute :bio, :string
attribute :score, :integer
end end
relationships do relationships do