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
%{validation: {module, validation_opts}, where: where}, changeset ->
with {:atomic, fields, condition_expr, error_expr} <-
module.atomic(changeset, validation_opts),
{:changing?, true} <-
{:changing?, Enum.any?(fields, &changing_attribute?(changeset, &1))},
{:atomic, condition} <- atomic_condition(where, changeset) do
case condition do
true ->
{:cont, validate_atomically(changeset, condition_expr, error_expr)}
case List.wrap(module.atomic(changeset, validation_opts)) do
[{:atomic, _, _, _} | _] = atomics ->
Enum.reduce_while(atomics, changeset, fn
{:atomic, fields, condition_expr, error_expr}, changeset ->
with {:changing?, true} <-
{:changing?,
fields == :* || Enum.any?(fields, &changing_attribute?(changeset, &1))},
{:atomic, condition} <- atomic_condition(where, changeset) do
case condition do
true ->
{:cont, validate_atomically(changeset, condition_expr, error_expr)}
false ->
{:cont, changeset}
false ->
{:cont, changeset}
condition ->
condition_expr =
Ash.Expr.expr(^condition and condition_expr)
condition ->
condition_expr =
Ash.Expr.expr(^condition and condition_expr)
{:cont, validate_atomically(changeset, condition_expr, error_expr)}
end
else
:not_atomic ->
{:halt, :not_atomic}
{:cont, validate_atomically(changeset, condition_expr, error_expr)}
end
else
{:changing?, false} ->
{:cont, changeset}
{:changing?, false} ->
{:cont, changeset}
:not_atomic ->
{:halt, :not_atomic}
end
end)
|> case do
:not_atomic -> {:halt, :not_atomic}
changeset -> {:cont, changeset}
end
[value] ->
{:halt, value}
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.
"""
def atomic_ref(changeset, field) do
if base_value = changeset.atomics[field] do
%{type: type, constraints: constraints} =
Ash.Resource.Info.attribute(changeset.resource, field)
case Keyword.fetch(changeset.atomics, field) do
{:ok, atomic} ->
%{type: type, constraints: constraints} =
Ash.Resource.Info.attribute(changeset.resource, field)
Ash.Expr.expr(type(^base_value, ^type, ^constraints))
else
Ash.Expr.expr(ref(^field))
Ash.Expr.expr(type(^atomic, ^type, ^constraints))
: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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -16,6 +16,7 @@ defmodule Ash.Resource.Validation.AttributeIn do
use Ash.Resource.Validation
alias Ash.Error.Changes.InvalidAttribute
require Ash.Expr
@impl true
def init(opts) do
@ -42,11 +43,26 @@ defmodule Ash.Resource.Validation.AttributeIn do
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
def describe(opts) do
[
message: "must equal %{value}",
vars: [field: opts[:attribute], value: opts[:value]]
message: "must be in %{list}",
vars: [field: opts[:attribute], list: opts[:list]]
]
end
end

View file

@ -4,6 +4,7 @@ defmodule Ash.Resource.Validation.Changing do
use Ash.Resource.Validation
alias Ash.Error.Changes.InvalidAttribute
require Ash.Expr
@opt_schema [
field: [
@ -51,6 +52,22 @@ defmodule Ash.Resource.Validation.Changing do
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
def describe(_opts) do
[

View file

@ -4,6 +4,7 @@ defmodule Ash.Resource.Validation.Compare do
use Ash.Resource.Validation
alias Ash.Error.Changes.InvalidAttribute
require Ash.Expr
@impl true
def init(opts) do
@ -67,6 +68,66 @@ defmodule Ash.Resource.Validation.Compare do
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
def describe(opts) do
[

View file

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