mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 13:33:20 +12:00
feat: support arguments for actions
This commit is contained in:
parent
1e23199bec
commit
0abf03065a
14 changed files with 225 additions and 6 deletions
|
@ -74,6 +74,7 @@ defmodule Ash.Actions.Create do
|
|||
|> validate_required_belongs_to()
|
||||
|> add_validations()
|
||||
|> require_values()
|
||||
|> Ash.Changeset.cast_arguments(action)
|
||||
end
|
||||
|
||||
defp require_values(changeset) do
|
||||
|
|
|
@ -30,7 +30,8 @@ defmodule Ash.Actions.Destroy do
|
|||
|
||||
changeset = %{changeset | action_type: :destroy, api: api}
|
||||
|
||||
with :ok <- validate(changeset),
|
||||
with %{valid?: true} <- Ash.Changeset.cast_arguments(changeset, action),
|
||||
:ok <- validate(changeset),
|
||||
:ok <- validate_multitenancy(changeset) do
|
||||
destroy_request =
|
||||
Request.new(
|
||||
|
|
|
@ -66,6 +66,7 @@ defmodule Ash.Actions.Update do
|
|||
|> Relationships.handle_relationship_changes()
|
||||
|> set_defaults()
|
||||
|> add_validations()
|
||||
|> Ash.Changeset.cast_arguments(action)
|
||||
end
|
||||
|
||||
defp run_action_changes(changeset, %{changes: changes}, actor) do
|
||||
|
|
|
@ -40,6 +40,7 @@ defmodule Ash.Changeset do
|
|||
:resource,
|
||||
:api,
|
||||
:tenant,
|
||||
arguments: %{},
|
||||
context: %{},
|
||||
after_action: [],
|
||||
before_action: [],
|
||||
|
@ -75,10 +76,12 @@ defmodule Ash.Changeset do
|
|||
@type t :: %__MODULE__{}
|
||||
|
||||
alias Ash.Error.{
|
||||
Changes.InvalidArgument,
|
||||
Changes.InvalidAttribute,
|
||||
Changes.InvalidRelationship,
|
||||
Changes.NoSuchAttribute,
|
||||
Changes.NoSuchRelationship,
|
||||
Changes.Required,
|
||||
Invalid.NoSuchResource
|
||||
}
|
||||
|
||||
|
@ -203,6 +206,31 @@ defmodule Ash.Changeset do
|
|||
%{changeset | context: Map.merge(changeset.context, map)}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def cast_arguments(changeset, action) do
|
||||
Enum.reduce(action.arguments, %{changeset | arguments: %{}}, fn argument, new_changeset ->
|
||||
value = Map.get(changeset.arguments, argument.name)
|
||||
|
||||
if is_nil(value) && !argument.allow_nil? do
|
||||
Ash.Changeset.add_error(
|
||||
changeset,
|
||||
Required.exception(field: argument.name, type: :argument)
|
||||
)
|
||||
else
|
||||
with {:ok, casted} <- Ash.Type.cast_input(argument.type, value),
|
||||
:ok <- Ash.Type.apply_constraints(argument.type, casted, argument.constraints) do
|
||||
%{new_changeset | arguments: Map.put(new_changeset.arguments, argument.name, value)}
|
||||
else
|
||||
_ ->
|
||||
Ash.Changeset.add_error(
|
||||
changeset,
|
||||
InvalidArgument.exception(field: argument.name)
|
||||
)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Appends a record or a list of records to a relationship. Stacks with previous removals/additions.
|
||||
|
||||
|
@ -486,6 +514,31 @@ defmodule Ash.Changeset do
|
|||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Add an argument to the changeset, which will be provided to the action
|
||||
"""
|
||||
def set_argument(changeset, argument, value) do
|
||||
%{changeset | arguments: Map.put(changeset.arguments, argument, value)}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Remove an argument from the changeset
|
||||
"""
|
||||
def delete_argument(changeset, argument_or_arguments) do
|
||||
argument_or_arguments
|
||||
|> List.wrap()
|
||||
|> Enum.reduce(changeset, fn argument, changeset ->
|
||||
%{changeset | arguments: Map.delete(changeset.arguments, argument)}
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Merge a map of arguments to the arguments list
|
||||
"""
|
||||
def set_arguments(changeset, map) do
|
||||
%{changeset | arguments: Map.merge(changeset.arguments, map)}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Force change an attribute if is not currently being changed, by calling the provided function
|
||||
|
||||
|
|
25
lib/ash/error/changes/invalid_argument.ex
Normal file
25
lib/ash/error/changes/invalid_argument.ex
Normal file
|
@ -0,0 +1,25 @@
|
|||
defmodule Ash.Error.Changes.InvalidArgument do
|
||||
@moduledoc "Used when an invalid value is provided for an action argument"
|
||||
use Ash.Error
|
||||
|
||||
def_ash_error([:field, :message], class: :invalid)
|
||||
|
||||
defimpl Ash.ErrorKind do
|
||||
def id(_), do: Ecto.UUID.generate()
|
||||
|
||||
def code(_), do: "invalid_argument"
|
||||
|
||||
def message(error) do
|
||||
"Invalid value provided#{for_field(error)}#{do_message(error)}"
|
||||
end
|
||||
|
||||
defp for_field(%{field: field}) when not is_nil(field), do: " for #{field}"
|
||||
defp for_field(_), do: ""
|
||||
|
||||
defp do_message(%{message: message}) when not is_nil(message) do
|
||||
": #{message}."
|
||||
end
|
||||
|
||||
defp do_message(_), do: "."
|
||||
end
|
||||
end
|
27
lib/ash/resource/actions/argument.ex
Normal file
27
lib/ash/resource/actions/argument.ex
Normal file
|
@ -0,0 +1,27 @@
|
|||
defmodule Ash.Resource.Actions.Argument do
|
||||
@moduledoc "Represents an argument to an action"
|
||||
defstruct [:allow_nil?, :type, :name, constraints: []]
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
def schema do
|
||||
[
|
||||
allow_nil?: [
|
||||
type: :boolean,
|
||||
default: true
|
||||
],
|
||||
type: [
|
||||
type: {:custom, Ash.OptionsHelpers, :ash_type, []},
|
||||
required: true
|
||||
],
|
||||
name: [
|
||||
type: :atom,
|
||||
required: true
|
||||
],
|
||||
constraints: [
|
||||
type: :keyword_list,
|
||||
default: []
|
||||
]
|
||||
]
|
||||
end
|
||||
end
|
|
@ -1,11 +1,12 @@
|
|||
defmodule Ash.Resource.Actions.Create do
|
||||
@moduledoc "Represents a create action on a resource."
|
||||
defstruct [:name, :primary?, :accept, :changes, :description, type: :create]
|
||||
defstruct [:name, :primary?, :accept, :arguments, :changes, :description, type: :create]
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
type: :create,
|
||||
name: atom,
|
||||
accept: [atom],
|
||||
arguments: [Ash.Resource.Actions.Argument.t()],
|
||||
primary?: boolean,
|
||||
description: String.t()
|
||||
}
|
||||
|
|
|
@ -1,11 +1,21 @@
|
|||
defmodule Ash.Resource.Actions.Destroy do
|
||||
@moduledoc "Represents a destroy action on a resource."
|
||||
|
||||
defstruct [:name, :primary?, :changes, :accept, :soft?, :description, type: :destroy]
|
||||
defstruct [
|
||||
:name,
|
||||
:primary?,
|
||||
:arguments,
|
||||
:changes,
|
||||
:accept,
|
||||
:soft?,
|
||||
:description,
|
||||
type: :destroy
|
||||
]
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
type: :destroy,
|
||||
name: atom,
|
||||
arguments: [Ash.Resource.Actions.Argument.t()],
|
||||
primary?: boolean,
|
||||
description: String.t()
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
defmodule Ash.Resource.Actions.Update do
|
||||
@moduledoc "Represents a update action on a resource."
|
||||
|
||||
defstruct [:name, :primary?, :accept, :changes, :description, type: :update]
|
||||
defstruct [:name, :primary?, :accept, :changes, :arguments, :description, type: :update]
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
type: :update,
|
||||
name: atom,
|
||||
accept: [atom],
|
||||
arguments: [Ash.Resource.Actions.Argument.t()],
|
||||
primary?: boolean,
|
||||
description: String.t()
|
||||
}
|
||||
|
|
|
@ -19,4 +19,9 @@ defmodule Ash.Resource.Change.Builtins do
|
|||
|
||||
@doc "A helper for builting filter templates"
|
||||
def actor(value), do: {:_actor, value}
|
||||
|
||||
@doc "A helper to confirm the value of one field against another field, or an argument"
|
||||
def confirm(field, confirmation) do
|
||||
{Ash.Resource.Change.Confirm, [field: field, confirmation: confirmation]}
|
||||
end
|
||||
end
|
||||
|
|
46
lib/ash/resource/change/confirm.ex
Normal file
46
lib/ash/resource/change/confirm.ex
Normal file
|
@ -0,0 +1,46 @@
|
|||
defmodule Ash.Resource.Change.Confirm do
|
||||
@moduledoc false
|
||||
use Ash.Resource.Change
|
||||
alias Ash.Changeset
|
||||
alias Ash.Error.Changes.InvalidAttribute
|
||||
|
||||
def init(opts) do
|
||||
case opts[:field] do
|
||||
nil ->
|
||||
{:error, "Field is required"}
|
||||
|
||||
field when is_atom(field) ->
|
||||
case opts[:confirmation] do
|
||||
nil ->
|
||||
{:error, "Confirmation is required"}
|
||||
|
||||
confirmation when is_atom(confirmation) ->
|
||||
{:ok, [confirmation: confirmation, field: field]}
|
||||
|
||||
confirmation ->
|
||||
{:error, "Expected an atom for confirmation, got: #{inspect(confirmation)}"}
|
||||
end
|
||||
|
||||
field ->
|
||||
{:error, "Expected an atom for field, got: #{inspect(field)}"}
|
||||
end
|
||||
end
|
||||
|
||||
def change(changeset, opts, _) do
|
||||
confirmation_value =
|
||||
Map.get(changeset.arguments, opts[:confirmation]) ||
|
||||
Ash.Changeset.get_attribute(changeset, opts[:value])
|
||||
|
||||
if confirmation_value == Ash.Changeset.get_attribute(changeset, opts[:field]) do
|
||||
changeset
|
||||
else
|
||||
Changeset.add_error(
|
||||
changeset,
|
||||
InvalidAttribute.exception(
|
||||
field: opts[:field],
|
||||
message: "Value did not match confirmation"
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -229,6 +229,22 @@ defmodule Ash.Resource.Dsl do
|
|||
args: [:change]
|
||||
}
|
||||
|
||||
@action_argument %Ash.Dsl.Entity{
|
||||
name: :argument,
|
||||
describe: """
|
||||
Declares an argument on the action
|
||||
|
||||
The type can be either a built in type (see `Ash.Type`) for more, or a module implementing
|
||||
the `Ash.Type` behaviour.
|
||||
""",
|
||||
examples: [
|
||||
"argument :password_confirmation, :string"
|
||||
],
|
||||
target: Ash.Resource.Actions.Argument,
|
||||
args: [:name, :type],
|
||||
schema: Ash.Resource.Actions.Argument.schema()
|
||||
}
|
||||
|
||||
@create %Ash.Dsl.Entity{
|
||||
name: :create,
|
||||
describe: """
|
||||
|
@ -242,6 +258,9 @@ defmodule Ash.Resource.Dsl do
|
|||
entities: [
|
||||
changes: [
|
||||
@change
|
||||
],
|
||||
arguments: [
|
||||
@action_argument
|
||||
]
|
||||
],
|
||||
args: [:name]
|
||||
|
@ -275,6 +294,9 @@ defmodule Ash.Resource.Dsl do
|
|||
entities: [
|
||||
changes: [
|
||||
@change
|
||||
],
|
||||
arguments: [
|
||||
@action_argument
|
||||
]
|
||||
],
|
||||
target: Ash.Resource.Actions.Update,
|
||||
|
@ -293,6 +315,9 @@ defmodule Ash.Resource.Dsl do
|
|||
entities: [
|
||||
changes: [
|
||||
@change
|
||||
],
|
||||
arguments: [
|
||||
@action_argument
|
||||
]
|
||||
],
|
||||
target: Ash.Resource.Actions.Destroy,
|
||||
|
|
|
@ -214,7 +214,7 @@ defmodule Ash.Type do
|
|||
Confirms if a casted value matches the provided constraints.
|
||||
"""
|
||||
@spec apply_constraints(t(), term, constraints()) :: :ok | {:error, String.t()}
|
||||
def apply_constraints({:array, type}, term, constraints) when is_list(constraints) do
|
||||
def apply_constraints({:array, type}, term, constraints) when is_list(term) do
|
||||
list_constraint_errors = list_constraint_errors(term, constraints)
|
||||
|
||||
case list_constraint_errors do
|
||||
|
|
|
@ -15,7 +15,12 @@ defmodule Ash.Test.Changeset.ChangesetTest do
|
|||
|
||||
actions do
|
||||
read :default
|
||||
create :default
|
||||
create :default, primary?: true
|
||||
|
||||
create :create_with_confirmation do
|
||||
argument :confirm_name, :string
|
||||
change confirm(:name, :confirm_name)
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
|
@ -739,4 +744,22 @@ defmodule Ash.Test.Changeset.ChangesetTest do
|
|||
} = changeset
|
||||
end
|
||||
end
|
||||
|
||||
describe "arguments" do
|
||||
test "arguments can be used in valid changes" do
|
||||
Category
|
||||
|> Changeset.new(%{"name" => "foo"})
|
||||
|> Changeset.set_argument(:confirm_name, "foo")
|
||||
|> Api.create!(action: :create_with_confirmation)
|
||||
end
|
||||
|
||||
test "arguments can be used in invalid changes" do
|
||||
assert_raise Ash.Error.Invalid, ~r/Value did not match confirmation/, fn ->
|
||||
Category
|
||||
|> Changeset.new(%{"name" => "foo"})
|
||||
|> Changeset.set_argument(:confirm_name, "bar")
|
||||
|> Api.create!(action: :create_with_confirmation)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue