From 92344029d386334146bb545fea31299117e68cac Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Thu, 1 Apr 2021 11:53:52 -0400 Subject: [PATCH] improvement: `before_action?` on `validate`, validate inline --- .formatter.exs | 1 + lib/ash/changeset/changeset.ex | 30 +++++++++------- lib/ash/resource/validation.ex | 17 ++++++++- lib/ash/resource/validation/confirm.ex | 2 +- lib/ash/resource/validation/match.ex | 38 ++++++++++++++------ lib/ash/resource/validation/one_of.ex | 2 +- lib/ash/resource/validation/string_length.ex | 30 +++++++++++++--- 7 files changed, 90 insertions(+), 30 deletions(-) diff --git a/.formatter.exs b/.formatter.exs index 8633b12e..6b0f4743 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -12,6 +12,7 @@ locals_without_parens = [ attribute: 2, attribute: 3, base_filter: 1, + before_action?: 1, belongs_to: 2, belongs_to: 3, calculate: 3, diff --git a/lib/ash/changeset/changeset.ex b/lib/ash/changeset/changeset.ex index 62a37bbc..7a5bb8ba 100644 --- a/lib/ash/changeset/changeset.ex +++ b/lib/ash/changeset/changeset.ex @@ -636,11 +636,7 @@ defmodule Ash.Changeset do module.change(changeset, opts, %{actor: actor}) %{validation: _} = validation, changeset -> - if validation.expensive? and not changeset.valid? do - changeset - else - do_validation(changeset, validation) - end + validate(changeset, validation) end) end @@ -693,19 +689,29 @@ defmodule Ash.Changeset do defp default(:update, %{update_default: value}), do: value defp add_validations(changeset) do - Ash.Changeset.before_action(changeset, fn changeset -> - changeset.resource - # We use the `changeset.action_type` to support soft deletes - # Because a delete is an `update` with an action type of `update` - |> Ash.Resource.Info.validations(changeset.action_type) - |> Enum.reduce(changeset, fn validation, changeset -> + changeset.resource + # We use the `changeset.action_type` to support soft deletes + # Because a delete is an `update` with an action type of `update` + |> Ash.Resource.Info.validations(changeset.action_type) + |> Enum.reduce(changeset, &validate(&2, &1)) + end + + defp validate(changeset, validation) do + if validation.before_action? do + before_action(changeset, fn changeset -> if validation.expensive? and not changeset.valid? do changeset else do_validation(changeset, validation) end end) - end) + else + if validation.expensive? and not changeset.valid? do + changeset + else + do_validation(changeset, validation) + end + end end defp do_validation(changeset, validation) do diff --git a/lib/ash/resource/validation.ex b/lib/ash/resource/validation.ex index 21a812f3..87ad52bb 100644 --- a/lib/ash/resource/validation.ex +++ b/lib/ash/resource/validation.ex @@ -39,7 +39,16 @@ defmodule Ash.Resource.Validation do end ``` """ - defstruct [:validation, :module, :opts, :expensive?, :description, :message, on: []] + defstruct [ + :validation, + :module, + :opts, + :expensive?, + :description, + :message, + :before_action?, + on: [] + ] defmacro __using__(_) do quote do @@ -92,6 +101,12 @@ defmodule Ash.Resource.Validation do 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." ] ] diff --git a/lib/ash/resource/validation/confirm.ex b/lib/ash/resource/validation/confirm.ex index 9e3b13c3..875d550c 100644 --- a/lib/ash/resource/validation/confirm.ex +++ b/lib/ash/resource/validation/confirm.ex @@ -35,7 +35,7 @@ defmodule Ash.Resource.Validation.Confirm do Changeset.get_argument(changeset, opts[:field]) || Changeset.get_attribute(changeset, opts[:field]) - if confirmation_value == value do + if Comp.equal?(confirmation_value, value) do :ok else {:error, diff --git a/lib/ash/resource/validation/match.ex b/lib/ash/resource/validation/match.ex index 2fe27f80..bf029493 100644 --- a/lib/ash/resource/validation/match.ex +++ b/lib/ash/resource/validation/match.ex @@ -36,20 +36,38 @@ defmodule Ash.Resource.Validation.Match do @impl true def validate(changeset, opts) do case Ash.Changeset.fetch_change(changeset, opts[:attribute]) do - {:ok, changing_to} when is_binary(changing_to) -> - if String.match?(changing_to, opts[:match]) do - :ok - else - {:error, - InvalidAttribute.exception( - field: opts[:attribute], - message: opts[:message], - vars: [match: opts[:match]] - )} + {:ok, changing_to} -> + case string_value(changing_to, opts) do + {:ok, changing_to} -> + if String.match?(changing_to, opts[:match]) do + :ok + else + {:error, + InvalidAttribute.exception( + field: opts[:attribute], + message: opts[:message], + vars: [match: opts[:match]] + )} + end + + {:error, error} -> + {:error, error} end _ -> :ok end end + + defp string_value(value, opts) do + {:ok, to_string(value)} + rescue + _ -> + {:error, + InvalidAttribute.exception( + field: opts[:attribute], + message: opts[:message], + vars: [match: opts[:match]] + )} + end end diff --git a/lib/ash/resource/validation/one_of.ex b/lib/ash/resource/validation/one_of.ex index dae9f827..9f07dfad 100644 --- a/lib/ash/resource/validation/one_of.ex +++ b/lib/ash/resource/validation/one_of.ex @@ -37,7 +37,7 @@ defmodule Ash.Resource.Validation.OneOf do :ok {:ok, changing_to} -> - if changing_to in opts[:values] do + if Enum.any?(opts[:values], &Comp.equal?(&1, changing_to)) do :ok else {:error, diff --git a/lib/ash/resource/validation/string_length.ex b/lib/ash/resource/validation/string_length.ex index d6a7766d..77717c76 100644 --- a/lib/ash/resource/validation/string_length.ex +++ b/lib/ash/resource/validation/string_length.ex @@ -36,12 +36,32 @@ defmodule Ash.Resource.Validation.StringLength do @impl true def validate(changeset, opts) do - Ash.Changeset.get_attribute(changeset, opts[:attribute]) - |> do_validate(Enum.into(opts, %{})) - end + case Ash.Changeset.get_attribute(changeset, opts[:attribute]) do + nil -> + :ok - defp do_validate(nil, _) do - :ok + value -> + value = + try do + {:ok, to_string(value)} + rescue + _ -> + {:error, + InvalidAttribute.exception( + field: opts[:attribute], + message: "%{field} could not be parsed", + vars: [field: opts[:attribute]] + )} + end + + case value do + {:ok, value} -> + do_validate(value, Enum.into(opts, %{})) + + {:error, error} -> + {:error, error} + end + end end defp do_validate(value, %{exact: exact} = opts) do