Skip to content

Commit 7b3efe9

Browse files
authored
fix: update Validations.ActionIs to accept atom or list(atom) (#1893)
1 parent a572140 commit 7b3efe9

File tree

4 files changed

+124
-7
lines changed

4 files changed

+124
-7
lines changed

documentation/topics/resources/validations.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ The validations section allows you to add validations across multiple actions of
129129
validations do
130130
validate present([:foo, :bar]), on: :update
131131
validate present([:foo, :bar, :baz], at_least: 2), on: :create
132-
validate present([:foo, :bar, :baz], at_least: 2), where: [action_is(:action1, :action2)]
132+
validate present([:foo, :bar, :baz], at_least: 2), where: [action_is([:action1, :action2])]
133133
validate absent([:foo, :bar, :baz], exactly: 1), on: [:update, :destroy]
134134
validate {MyCustomValidation, [foo: :bar]}, on: :create
135135
end

lib/ash/resource/validation/action_is.ex

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,46 @@ defmodule Ash.Resource.Validation.ActionIs do
22
@moduledoc "Validates that the action is the specified action."
33
use Ash.Resource.Validation
44

5+
@opt_schema [
6+
action: [
7+
type: {:wrap_list, :atom},
8+
doc: "The action or actions to compare against",
9+
required: true
10+
]
11+
]
12+
13+
opt_schema = @opt_schema
14+
15+
defmodule Opts do
16+
@moduledoc false
17+
use Spark.Options.Validator, schema: opt_schema
18+
end
19+
20+
@impl true
21+
def init(opts) do
22+
case Opts.validate(opts) do
23+
{:ok, opts} ->
24+
{:ok, Opts.to_options(opts)}
25+
26+
{:error, error} ->
27+
{:error, Exception.message(error)}
28+
end
29+
end
30+
531
@impl true
632
def validate(changeset, opts, _context) do
7-
if changeset.action.name == opts[:action] do
33+
if changeset.action.name in List.wrap(opts[:action]) do
834
:ok
935
else
1036
# We use "unknown" here because it doesn't make sense to surface
1137
# this error to clients potentially (and this should really only be used as a condition anyway)
12-
[message: message, vars: vars] = describe(opts)
38+
description = describe(opts)
1339

14-
{:error, Ash.Error.Unknown.UnknownError.exception(error: message, vars: vars)}
40+
{:error,
41+
Ash.Error.Unknown.UnknownError.exception(
42+
error: description[:message],
43+
vars: description[:vars]
44+
)}
1545
end
1646
end
1747

@@ -22,6 +52,12 @@ defmodule Ash.Resource.Validation.ActionIs do
2252

2353
@impl true
2454
def describe(opts) do
25-
[message: "must be %{action}", vars: %{action: opts[:action]}]
55+
case opts[:action] do
56+
actions when is_list(actions) ->
57+
[message: "action must be one of %{action}", vars: %{action: Enum.join(actions, ", ")}]
58+
59+
_ ->
60+
[message: "must be %{action}", vars: %{action: opts[:action]}]
61+
end
2662
end
2763
end

lib/ash/resource/validation/builtins.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,15 @@ defmodule Ash.Resource.Validation.Builtins do
7979
end
8080

8181
@doc """
82-
Validates that the action is a specific action. Primarily meant for use in `where`.
82+
Validates that the action name matches the provided action name or names. Primarily meant for use in `where`.
8383
8484
## Examples
8585
8686
validate present(:foo), where: [action_is(:bar)]
87+
88+
validate present(:foo), where: action_is([:bar, :baz])
8789
"""
88-
@spec action_is(action :: atom) :: Validation.ref()
90+
@spec action_is(action :: atom | list(atom)) :: Validation.ref()
8991
def action_is(action) do
9092
{Validation.ActionIs, action: action}
9193
end
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
defmodule Ash.Test.Resource.Validation.ActionIsTest do
2+
@moduledoc false
3+
use ExUnit.Case, async: true
4+
5+
alias Ash.Resource.Validation.ActionIs
6+
7+
alias Ash.Test.Domain, as: Domain
8+
9+
defmodule Post do
10+
use Ash.Resource, domain: Domain
11+
12+
actions do
13+
default_accept :*
14+
defaults [:read, :destroy, create: :*, update: :*]
15+
end
16+
17+
attributes do
18+
uuid_primary_key :id
19+
20+
attribute :status, :string, public?: true
21+
end
22+
end
23+
24+
test "passes when action is equal to the one provided action" do
25+
changeset =
26+
Ash.Changeset.for_create(Post, :create)
27+
28+
assert_validation_success(changeset, action: :create)
29+
end
30+
31+
test "fails when action is equal to the one provided action" do
32+
changeset =
33+
Ash.Changeset.for_update(%Post{}, :update)
34+
35+
assert_validation_error(changeset, [action: :create], "must be create")
36+
end
37+
38+
test "passes when action is in list of provided actions" do
39+
changesets = [
40+
Ash.Changeset.for_create(Post, :create, %{status: "new"}),
41+
Ash.Changeset.for_update(%Post{}, :update, %{status: "new"})
42+
]
43+
44+
for changeset <- changesets do
45+
assert_validation_success(changeset, action: [:create, :update])
46+
end
47+
end
48+
49+
test "fails when action is not in list of provided actions" do
50+
changeset = Ash.Changeset.for_destroy(%Post{}, :destroy)
51+
52+
assert_validation_error(
53+
changeset,
54+
[action: [:create, :update]],
55+
"action must be one of create, update"
56+
)
57+
end
58+
59+
defp assert_validation_success(changeset, opts) do
60+
assert :ok = ActionIs.validate(changeset, opts, %{})
61+
end
62+
63+
defp assert_validation_error(changeset, opts, expected_message) do
64+
assert {:error, %Ash.Error.Unknown.UnknownError{error: message, vars: vars}} =
65+
ActionIs.validate(changeset, opts, %{})
66+
67+
assert expected_message == translate_message(message, vars)
68+
end
69+
70+
defp translate_message(message, vars) do
71+
Enum.reduce(vars, message, fn {key, value}, acc ->
72+
if String.contains?(acc, "%{#{key}}") do
73+
String.replace(acc, "%{#{key}}", to_string(value))
74+
else
75+
acc
76+
end
77+
end)
78+
end
79+
end

0 commit comments

Comments
 (0)