Packages
ash
3.5.20
3.29.3
3.29.2
3.29.1
3.29.0
3.28.0
3.27.8
3.27.7
3.27.6
3.27.5
3.27.4
3.27.3
3.27.2
3.27.1
3.27.0
3.26.0
3.25.2
3.25.1
3.25.0
retired
3.24.7
3.24.6
3.24.5
3.24.4
3.24.3
3.24.2
3.24.1
3.24.0
3.23.1
3.23.0
3.22.2
3.22.1
3.22.0
3.21.3
3.21.2
3.21.1
3.21.0
3.20.0
3.19.3
3.19.2
3.19.1
3.19.0
3.18.0
3.17.1
3.17.0
3.16.0
3.15.0
3.14.1
3.14.0
retired
3.13.2
3.13.1
3.13.0
3.12.0
3.11.3
3.11.2
3.11.1
3.11.0
3.10.1
3.10.0
3.9.0
3.8.0
3.7.6
3.7.5
3.7.4
3.7.3
3.7.2
3.7.1
3.7.0
retired
3.6.3
retired
3.6.2
3.6.1
3.6.0
3.5.43
3.5.42
3.5.41
3.5.40
3.5.39
3.5.38
3.5.37
3.5.36
3.5.35
3.5.34
3.5.33
3.5.32
3.5.31
3.5.30
3.5.29
3.5.28
3.5.27
3.5.26
3.5.25
3.5.24
3.5.23
3.5.22
3.5.21
3.5.20
3.5.19
3.5.18
3.5.17
3.5.16
3.5.15
3.5.14
3.5.13
3.5.12
3.5.11
3.5.10
3.5.9
3.5.8
3.5.7
3.5.6
3.5.5
3.5.4
3.5.3
3.5.2
3.5.1
3.5.0
3.4.74
retired
3.4.73
3.4.72
3.4.71
3.4.70
3.4.69
3.4.68
3.4.67
3.4.66
3.4.65
3.4.64
3.4.63
3.4.62
3.4.61
3.4.60
3.4.59
3.4.58
3.4.57
3.4.56
3.4.55
3.4.54
3.4.53
3.4.52
3.4.51
3.4.50
3.4.49
3.4.48
3.4.47
3.4.46
3.4.45
3.4.44
3.4.43
3.4.42
3.4.41
3.4.40
3.4.39
3.4.38
3.4.37
3.4.36
3.4.35
3.4.34
3.4.33
3.4.32
3.4.31
3.4.30
3.4.29
3.4.28
3.4.27
3.4.26
3.4.25
3.4.24
3.4.23
3.4.22
3.4.21
3.4.20
3.4.19
3.4.18
3.4.17
3.4.16
3.4.15
3.4.14
3.4.13
3.4.12
3.4.11
3.4.10
3.4.9
3.4.8
3.4.7
3.4.6
3.4.5
3.4.4
3.4.3
3.4.2
3.4.1
3.4.0
3.3.3
3.3.2
3.3.1
3.3.0
3.2.6
3.2.5
3.2.4
3.2.3
3.2.2
3.2.1
3.2.0
3.1.8
3.1.7
3.1.6
3.1.5
3.1.4
3.1.3
3.1.2
3.1.1
3.1.0
3.0.16
3.0.15
3.0.14
3.0.13
3.0.12
3.0.11
3.0.10
3.0.9
3.0.8
3.0.7
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
3.0.0-rc.46
3.0.0-rc.45
3.0.0-rc.44
3.0.0-rc.43
3.0.0-rc.42
3.0.0-rc.41
3.0.0-rc.40
3.0.0-rc.39
3.0.0-rc.38
3.0.0-rc.37
3.0.0-rc.36
3.0.0-rc.35
3.0.0-rc.34
3.0.0-rc.33
3.0.0-rc.32
3.0.0-rc.31
3.0.0-rc.29
3.0.0-rc.28
3.0.0-rc.27
3.0.0-rc.26
3.0.0-rc.25
3.0.0-rc.24
3.0.0-rc.23
3.0.0-rc.22
3.0.0-rc.21
3.0.0-rc.20
3.0.0-rc.19
3.0.0-rc.18
3.0.0-rc.17
3.0.0-rc.16
3.0.0-rc.15
3.0.0-rc.14
3.0.0-rc.13
3.0.0-rc.12
3.0.0-rc.11
3.0.0-rc.10
3.0.0-rc.9
3.0.0-rc.8
3.0.0-rc.7
3.0.0-rc.6
3.0.0-rc.5
3.0.0-rc.4
3.0.0-rc.3
3.0.0-rc.1
3.0.0-rc.0
2.21.15
2.21.14
2.21.13
2.21.12
2.21.11
2.21.10
2.21.9
2.21.8
2.21.7
2.21.6
2.21.5
2.21.4
2.21.3
2.21.2
2.21.1
2.21.0
2.20.3
2.20.2
2.20.1
2.20.0
2.19.14
2.19.13
2.19.12
2.19.11
2.19.10
2.19.9
2.19.8
2.19.7
2.19.6
2.19.5
2.19.4
2.19.3
retired
2.19.2
retired
2.19.1
retired
2.19.0
retired
2.18.2
2.18.1
2.18.0
2.17.24
2.17.23
2.17.22
2.17.21
2.17.20
2.17.19
2.17.18
2.17.17
2.17.16
2.17.15
2.17.14
2.17.13
2.17.12
2.17.11
2.17.10
2.17.9
2.17.8
2.17.7
2.17.6
2.17.5
2.17.4
2.17.3
2.17.2
2.17.1
2.17.0
2.16.1
2.16.0
2.15.20
2.15.19
2.15.18
2.15.17
2.15.16
2.15.15
2.15.14
2.15.13
2.15.12
2.15.11
2.15.10
2.15.9
2.15.8
2.15.7
2.15.6
2.15.5
2.15.4
2.15.2
2.15.1
2.15.0
2.14.21
2.14.20
2.14.19
2.14.18
2.14.17
2.14.16
2.14.15
2.14.14
2.14.13
2.14.12
2.14.11
2.14.10
2.14.9
2.14.8
2.14.7
2.14.6
2.14.5
2.14.4
2.14.3
2.14.2
2.14.1
2.14.0
2.13.4
retired
2.13.3
2.13.2
2.13.1
2.13.0
2.12.1
2.12.0
2.11.11
2.11.10
2.11.9
2.11.8
2.11.7
2.11.6
2.11.5
2.11.4
2.11.3
2.11.2
2.11.1
2.11.0
2.11.0-rc.3
2.11.0-rc.2
2.11.0-rc.1
2.11.0-rc.0
2.10.2
2.10.1
2.10.0
2.9.29
2.9.28
2.9.27
2.9.26
2.9.25
2.9.24
2.9.23
2.9.22
2.9.21
2.9.20
2.9.19
2.9.18
2.9.17
2.9.16
2.9.15
2.9.14
2.9.13
2.9.12
2.9.11
2.9.10
2.9.9
2.9.8
2.9.7
2.9.6
2.9.5
2.9.4
2.9.3
2.9.2
2.9.1
2.9.0
2.8.1
2.8.0
2.7.1
2.7.0
2.6.31
2.6.30
2.6.29
2.6.28
2.6.27
2.6.26
2.6.25
2.6.24
2.6.23
2.6.22
2.6.21
2.6.20
2.6.19
2.6.18
2.6.17
2.6.16
2.6.15
2.6.14
2.6.13
2.6.11
2.6.10
2.6.9
2.6.8
2.6.7
2.6.6
2.6.5
2.6.4
2.6.3
2.6.2
2.6.1
2.6.0
2.5.16
2.5.15
2.5.14
2.5.13
2.5.12
2.5.11
2.5.10
2.5.9
2.5.8
2.5.7
2.5.6
2.5.5
2.5.4
2.5.3
2.5.2
2.5.1
2.5.0
2.5.0-rc.6
2.5.0-rc.5
2.5.0-rc.4
2.5.0-rc.3
2.5.0-rc.2
2.5.0-rc.1
2.5.0-rc.0
2.4.30
2.4.29
2.4.28
2.4.27
2.4.26
2.4.25
2.4.24
2.4.23
2.4.22
2.4.21
2.4.20
2.4.19
2.4.18
2.4.17
2.4.16
2.4.15
2.4.14
2.4.13
2.4.12
2.4.11
2.4.10
2.4.9
2.4.8
2.4.7
2.4.6
2.4.5
2.4.4
2.4.3
2.4.2
2.4.1
2.4.0
2.3.0
2.2.0
2.1.0
2.0.0
2.0.0-rc.15
2.0.0-rc.14
2.0.0-rc.13
2.0.0-rc.12
2.0.0-rc.11
2.0.0-rc.10
2.0.0-rc.9
2.0.0-rc.8
2.0.0-rc.7
2.0.0-rc.6
2.0.0-rc.5
2.0.0-rc.4
2.0.0-rc.3
2.0.0-rc.2
2.0.0-rc.1
2.0.0-rc.0
2.0.0-pre.8
2.0.0-pre.7
2.0.0-pre.6
2.0.0-pre.5
2.0.0-pre.4
2.0.0-pre.3
2.0.0-pre.2
2.0.0-pre.1
2.0.0-pre.0
1.53.3
1.53.2
1.53.0
1.52.0-rc.22
1.52.0-rc.21
1.52.0-rc.20
1.52.0-rc.19
1.52.0-rc.18
1.52.0-rc.17
1.52.0-rc.16
1.52.0-rc.15
1.52.0-rc.14
1.52.0-rc.13
1.52.0-rc.12
1.52.0-rc.11
1.52.0-rc.10
1.52.0-rc.9
1.52.0-rc.8
1.52.0-rc.7
1.52.0-rc.6
1.52.0-rc.5
1.52.0-rc.4
1.52.0-rc.3
1.52.0-rc.2
1.52.0-rc.1
1.52.0-rc.0
1.51.2
1.51.1
retired
1.51.0
1.50.21
1.50.20
1.50.19
1.50.18
1.50.17
1.50.16
1.50.15
1.50.14
1.50.13
1.50.12
1.50.11
1.50.10
1.50.9
1.50.8
1.50.7
1.50.6
1.50.5
1.50.4
1.50.3
1.50.2
1.50.1
1.50.0
1.49.0
1.48.0-rc.30
1.48.0-rc.29
1.48.0-rc.28
1.48.0-rc.27
1.48.0-rc.26
1.48.0-rc.25
1.48.0-rc.24
1.48.0-rc.23
1.48.0-rc.22
1.48.0-rc.21
1.48.0-rc.20
1.48.0-rc.19
1.48.0-rc.18
1.48.0-rc.17
1.48.0-rc.16
1.48.0-rc.15
1.48.0-rc.14
1.48.0-rc.13
1.48.0-rc.12
1.48.0-rc.11
1.48.0-rc.10
1.48.0-rc.9
1.48.0-rc.8
1.48.0-rc.7
1.48.0-rc.6
1.48.0-rc.5
1.48.0-rc.4
1.48.0-rc.3
1.48.0-rc.2
1.48.0-rc.1
1.48.0-rc.0
1.47.12
1.47.11
1.47.10
1.47.9
1.47.8
1.47.7
1.47.6
1.47.5
1.47.4
1.47.3
1.47.2
1.47.1
1.47.0
1.46.13
1.46.12
1.46.11
1.46.10
1.46.9
1.46.8
1.46.7
1.46.6
1.46.5
1.46.4
1.46.3
1.46.2
1.46.1
1.46.0
1.45.0-rc9
1.45.0-rc8
1.45.0-rc7
1.45.0-rc6
1.45.0-rc5
1.45.0-rc4
1.45.0-rc3
1.45.0-rc20
1.45.0-rc2
1.45.0-rc19
1.45.0-rc18
1.45.0-rc17
1.45.0-rc16
1.45.0-rc15
1.45.0-rc14
1.45.0-rc13
1.45.0-rc12
1.45.0-rc11
1.45.0-rc10
1.45.0-rc1
1.45.0-rc0
1.44.13
1.44.12
1.44.11
1.44.10
1.44.9
1.44.8
1.44.7
1.44.6
1.44.5
1.44.4
1.44.3
1.44.2
1.44.1
1.44.0
1.43.12
1.43.11
1.43.10
1.43.9
1.43.8
1.43.7
1.43.6
1.43.5
1.43.4
1.43.3
1.43.2
1.43.1
1.43.0
1.42.0
1.41.12
1.41.11
1.41.10
1.41.9
1.41.8
1.41.7
1.41.6
1.41.5
1.41.4
1.41.3
1.41.2
1.41.1
1.41.0
1.40.0
1.39.7
1.39.6
1.39.5
1.39.4
1.39.3
1.39.2
1.39.1
1.39.0
1.38.0
1.37.2
1.37.1
1.37.0
1.36.22
1.36.21
1.36.19
1.36.18
1.36.17
1.36.16
1.36.15
1.36.14
1.36.13
1.36.12
1.36.11
1.36.10
1.36.9
1.36.8
1.36.7
1.36.6
1.36.5
1.36.4
1.36.3
1.36.2
1.36.0
1.35.1
1.35.0
1.34.9
1.34.8
1.34.7
1.34.6
1.34.5
1.34.4
1.34.3
1.34.2
1.34.1
1.34.0
1.33.0
1.32.2
1.32.1
1.32.0
1.31.1
1.31.0
1.30.2
1.30.1
1.29.0-rc1
1.29.0-rc0
1.28.1
1.28.0
1.27.1
1.27.0
1.26.13
1.26.12
1.26.11
1.26.10
1.26.9
1.26.8
1.26.7
1.26.6
1.26.5
1.26.4
1.26.2
1.26.1
1.26.0
1.25.8
1.25.7
1.25.6
1.25.5
1.25.4
1.25.3
1.25.2
1.25.1
1.25.0
1.24.2
1.24.1
1.24.0
1.23.3
1.23.2
1.23.1
1.23.0
1.22.1
1.22.0
1.20.1
1.20.0
1.19.1
1.19.0
1.18.1
1.18.0
1.17.1
1.17.0
1.16.2
1.15.1
1.15.0
1.14.0
1.13.4
1.13.3
1.13.2
1.13.1
1.13.0
1.12.0
1.11.1
1.11.0
1.10.0
1.9.0
1.8.0
1.7.0
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.5.1
1.5.0
1.4.1
1.4.0
1.3.1
1.3.0
1.2.1
1.2.0
1.1.3
1.1.2
1.1.0
1.0.3
1.0.2
1.0.1
1.0.0
0.13.1
0.13.0
0.12.0
0.10.0
0.9.1
0.9.0
0.8.0
0.7.0
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.0
0.3.0
0.2.0
0.1.9
0.1.8
0.1.3
0.1.1
0.1.0
A declarative, extensible framework for building Elixir applications.
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/ash/action_input.ex
defmodule Ash.ActionInput do
@moduledoc """
Input for a custom action
Much like an `Ash.Query` and `Ash.Changeset` are used to provide inputs into
CRUD actions, this struct provides the inputs required to execute a generic
action.
"""
alias Ash.Error.Action.InvalidArgument
require Ash.Flags
defstruct [
:action,
:domain,
:resource,
:tenant,
:to_tenant,
invalid_keys: MapSet.new(),
arguments: %{},
params: %{},
context: %{},
valid?: true,
errors: []
]
@typedoc """
An action input struct for generic (non-CRUD) actions.
Contains all the information needed to execute a generic action including
arguments, context, tenant information, and validation state. Built using
`for_action/4` and modified with functions like `set_argument/3` and `set_context/2`.
"""
@type t :: %__MODULE__{
arguments: map(),
params: map(),
tenant: term(),
action: Ash.Resource.Actions.Action.t() | nil,
resource: Ash.Resource.t(),
invalid_keys: MapSet.t(),
context: map(),
domain: Ash.Domain.t(),
valid?: boolean()
}
@doc """
Creates a new action input from a resource.
This creates a basic action input struct that can be used as a starting point
for building inputs for generic actions. Use `for_action/4` to create an input
bound to a specific action.
## Examples
# Create a new action input for a resource
iex> Ash.ActionInput.new(MyApp.Post)
%Ash.ActionInput{resource: MyApp.Post, domain: nil, ...}
# Usually you'll want to use for_action/4 instead
iex> MyApp.Post |> Ash.ActionInput.for_action(:send_notification, %{message: "Hello"})
%Ash.ActionInput{action: %{name: :send_notification}, arguments: %{message: "Hello"}, ...}
## See also
- `for_action/4` for creating action-specific inputs
- `set_argument/3` for adding arguments
- `set_context/2` for adding context
"""
@spec new(Ash.Resource.t(), Ash.Domain.t()) :: t
def new(resource, domain \\ nil) do
%__MODULE__{resource: resource, domain: domain}
end
@for_action_opts [
domain: [
type: {:spark, Ash.Domain},
doc: "The domain to use for the action. The resource's domain is used by default."
],
context: [
type: :map,
doc: "Context to set on the action input.",
default: %{}
],
authorize?: [
type: {:or, [:boolean, {:literal, nil}]},
doc:
"Whether or not to run authorization on the action. Default behavior of this option is controlled by the domain."
],
tenant: [
type: :any,
doc: "The tenant to use for the action."
],
scope: [
type: :any,
doc:
"A value that implements the `Ash.Scope.ToOpts` protocol, for passing around actor/tenant/context in a single value. See `Ash.Scope.ToOpts` for more."
],
actor: [
type: :any,
doc: "The actor performing the action"
],
skip_unknown_inputs: [
type: {:wrap_list, {:or, [:atom, :string]}},
doc: "A list of unknown inputs to skip. Use `:*` to skip all unknown inputs."
],
tracer: [
type: :any,
doc: "A tracer or list of tracers to trace action execution."
],
private_arguments: [
type: :map,
default: %{},
doc: "A list of private arguments to be set before the action is invoked."
]
]
for_action_opts = @for_action_opts
defmodule Opts do
@moduledoc false
use Spark.Options.Validator, schema: for_action_opts
end
@doc """
Creates a new input for a generic action.
This is the primary way to create action inputs for generic actions. It validates
the action exists, sets up the input with proper defaults, and validates any
provided arguments according to the action's argument definitions.
## Examples
# Create input for a simple action
iex> MyApp.Post
...> |> Ash.ActionInput.for_action(:send_notification, %{message: "Hello"})
%Ash.ActionInput{action: %{name: :send_notification}, arguments: %{message: "Hello"}, ...}
# Create input with options
iex> MyApp.Post
...> |> Ash.ActionInput.for_action(:complex_action, %{data: "test"},
...> actor: current_user, authorize?: true)
%Ash.ActionInput{arguments: %{data: "test"}, ...}
# Create input and then modify it
iex> input = MyApp.Post
...> |> Ash.ActionInput.for_action(:example, %{})
...> |> Ash.ActionInput.set_context(%{source: "api"})
iex> input.action.name
:example
## Options
#{Opts.docs()}
## See also
- `new/2` for creating basic inputs
- `set_argument/3` for adding arguments after creation
- `Ash.run_action/2` for executing the action with the input
- `d:Ash.Resource.Dsl.actions.action` for defining generic actions
- [Generic Actions Guide](/documentation/topics/actions/generic-actions.md) for understanding generic actions
- [Actions Guide](/documentation/topics/actions/actions.md) for general action concepts
"""
@doc spark_opts: [{4, @for_action_opts}]
@spec for_action(
resource_or_input :: Ash.Resource.t() | t(),
action :: atom,
params :: map,
opts :: Keyword.t()
) :: t()
def for_action(resource_or_input, action, params, opts \\ []) do
with {:ok, opts} <- Opts.validate(opts),
{:ok, input} <- set_action_for_input(resource_or_input, action),
opts <- Opts.to_options(opts),
{:ok, input} <- set_domain_for_input(input, opts) do
{input, _opts} = Ash.Actions.Helpers.set_context_and_get_opts(input.domain, input, opts)
input =
Enum.reduce(opts[:private_arguments] || %{}, input, fn {k, v}, input ->
Ash.ActionInput.set_private_argument(input, k, v)
end)
input
|> cast_params(params, opts)
|> set_defaults()
|> require_arguments()
else
{:error, reason} -> raise Ash.Error.to_error_class(reason)
end
end
defp set_action_for_input(%Ash.ActionInput{resource: resource} = input, action_name)
when is_atom(resource) do
case Ash.Resource.Info.action(resource, action_name) do
nil ->
{:error,
Ash.Error.Invalid.NoSuchAction.exception(
resource: resource,
action: action_name,
type: :action
)}
action when action.type == :action ->
{:ok, %{input | action: action}}
action ->
{:error,
Ash.Error.Invalid.NoSuchAction.exception(
resource: resource,
action: action,
type: :action
)}
end
end
defp set_action_for_input(resource, action) when is_atom(resource) do
resource
|> new()
|> set_action_for_input(action)
end
defp set_domain_for_input(input, opts) do
domain =
input.domain || opts[:domain] || Ash.Resource.Info.domain(input.resource) ||
Ash.Actions.Helpers.maybe_embedded_domain(input.resource)
if domain do
{:ok, %{input | domain: domain}}
else
{:error,
ArgumentError.exception(
"Could not determine domain for action. Provide the `domain` option or configure a domain in the resource directly."
)}
end
end
@doc """
Sets the tenant to use when calling the action.
In multitenant applications, this configures which tenant's data the action
should operate on. The tenant value is used for data isolation and access control.
## Examples
# Set tenant using a string identifier
iex> MyApp.Post
...> |> Ash.ActionInput.new()
...> |> Ash.ActionInput.set_tenant("org_123")
...> |> then(& &1.tenant)
"org_123"
# Set tenant using a struct that implements Ash.ToTenant
iex> org = %MyApp.Organization{id: 456}
iex> input = MyApp.Post
...> |> Ash.ActionInput.for_action(:send_notification, %{})
...> |> Ash.ActionInput.set_tenant(org)
iex> input.tenant == org
true
# Use with action execution
iex> MyApp.Post
...> |> Ash.ActionInput.for_action(:cleanup, %{})
...> |> Ash.ActionInput.set_tenant("tenant_456")
...> |> Ash.run_action()
## See also
- `for_action/4` for setting tenant when creating inputs
- `Ash.ToTenant` protocol for custom tenant conversion
- `set_context/2` for adding tenant to action context
"""
@spec set_tenant(t(), Ash.ToTenant.t()) :: t()
def set_tenant(input, tenant) do
%{input | tenant: tenant, to_tenant: Ash.ToTenant.to_tenant(tenant, input.resource)}
end
defp require_arguments(input) do
input.action.arguments
|> Enum.filter(&(&1.allow_nil? == false))
|> Enum.reduce(input, fn argument, input ->
case fetch_argument(input, argument.name) do
{:ok, value} when not is_nil(value) ->
input
_ ->
if argument.name in input.invalid_keys do
input
else
add_error(
input,
Ash.Error.Changes.Required.exception(
resource: input.resource,
field: argument.name,
type: :argument
)
)
end
end
end)
end
defp set_defaults(input) do
input.action.arguments
|> Enum.reject(&is_nil(&1.default))
|> Enum.reduce(input, fn argument, input ->
case fetch_argument(input, argument.name) do
{:ok, value} when not is_nil(value) ->
input
_ ->
if argument.name in input.invalid_keys do
input
else
set_argument(input, argument.name, default(argument))
end
end
end)
end
defp default(%{default: {mod, func, args}}), do: apply(mod, func, args)
defp default(%{default: function}) when is_function(function, 0), do: function.()
defp default(%{default: value}), do: value
@doc """
Gets the value of an argument provided to the input.
Returns the argument value if found, or `nil` if not found. Arguments are
validated and cast according to the action's argument definitions when set.
## Examples
# Get an argument that exists
iex> MyApp.Post
...> |> Ash.ActionInput.for_action(:send_email, %{recipient: "user@example.com"})
...> |> Ash.ActionInput.get_argument(:recipient)
"user@example.com"
# Get an argument that doesn't exist returns nil
iex> MyApp.Post
...> |> Ash.ActionInput.for_action(:send_email, %{})
...> |> Ash.ActionInput.get_argument(:missing_arg)
nil
# Arguments can be accessed by string or atom key
iex> MyApp.Post
...> |> Ash.ActionInput.for_action(:example, %{"message" => "hello"})
...> |> Ash.ActionInput.get_argument(:message)
"hello"
## See also
- `fetch_argument/2` for safer argument access with explicit error handling
- `set_argument/3` for setting argument values
- `for_action/4` for providing initial arguments
"""
@spec get_argument(t, atom | String.t()) :: term
def get_argument(input, argument) when is_atom(argument) or is_binary(argument) do
case fetch_argument(input, argument) do
{:ok, value} -> value
:error -> nil
end
end
@doc """
Fetches the value of an argument provided to the input.
Returns `{:ok, value}` if the argument exists, or `:error` if not found.
This is the safer alternative to `get_argument/2` when you need to distinguish
between a `nil` value and a missing argument.
## Examples
# Fetch an argument that exists
iex> MyApp.Post
...> |> Ash.ActionInput.for_action(:send_notification, %{priority: :high})
...> |> Ash.ActionInput.fetch_argument(:priority)
{:ok, :high}
# Fetch an argument that doesn't exist
iex> MyApp.Post
...> |> Ash.ActionInput.for_action(:send_notification, %{})
...> |> Ash.ActionInput.fetch_argument(:missing_arg)
:error
# Distinguish between nil and missing arguments
iex> MyApp.Post
...> |> Ash.ActionInput.for_action(:example, %{optional_field: nil})
...> |> Ash.ActionInput.fetch_argument(:optional_field)
{:ok, nil}
# Use in conditional logic
iex> input = MyApp.Post |> Ash.ActionInput.for_action(:process, %{})
iex> case Ash.ActionInput.fetch_argument(input, :mode) do
...> {:ok, mode} -> "Processing in \#{mode} mode"
...> :error -> "Using default processing mode"
...> end
"Using default processing mode"
## See also
- `get_argument/2` for simpler argument access
- `set_argument/3` for setting argument values
- `for_action/4` for providing initial arguments
"""
@spec fetch_argument(t, atom | String.t()) :: {:ok, term()} | :error
def fetch_argument(input, argument) when is_atom(argument) or is_binary(argument) do
with :error <- Map.fetch(input.arguments, argument) do
Map.fetch(input.arguments, to_string(argument))
end
end
@doc """
Sets an argument value on the action input.
The argument value is validated and cast according to the action's argument
definition. If validation fails, errors will be added to the input and it
will be marked as invalid.
## Examples
# Set a simple argument
iex> MyApp.Post
...> |> Ash.ActionInput.for_action(:send_notification, %{})
...> |> Ash.ActionInput.set_argument(:message, "Hello World")
...> |> Ash.ActionInput.get_argument(:message)
"Hello World"
# Set multiple arguments by chaining
iex> input = MyApp.Post
...> |> Ash.ActionInput.for_action(:complex_action, %{})
...> |> Ash.ActionInput.set_argument(:priority, :high)
...> |> Ash.ActionInput.set_argument(:batch_size, 100)
iex> Ash.ActionInput.get_argument(input, :priority)
:high
# Arguments are validated according to type
iex> input = MyApp.Post
...> |> Ash.ActionInput.for_action(:schedule_job, %{})
...> |> Ash.ActionInput.set_argument(:run_at, ~U[2024-01-01 10:00:00Z])
iex> input.valid?
true
## See also
- `get_argument/2` for retrieving argument values
- `set_private_argument/3` for setting private arguments
- `for_action/4` for providing initial arguments
"""
@spec set_argument(input :: t(), name :: atom, value :: term()) :: t()
def set_argument(input, argument, value) do
if input.action do
argument =
Enum.find(
input.action.arguments,
&(&1.name == argument || to_string(&1.name) == argument)
)
if argument do
with value <- Ash.Type.Helpers.handle_indexed_maps(argument.type, value),
constraints <- Ash.Type.include_source(argument.type, input, argument.constraints),
{:ok, casted} <-
Ash.Type.cast_input(argument.type, value, constraints),
{:constrained, {:ok, casted}, argument} when not is_nil(casted) <-
{:constrained, Ash.Type.apply_constraints(argument.type, casted, constraints),
argument} do
%{input | arguments: Map.put(input.arguments, argument.name, casted)}
else
{:constrained, {:ok, nil}, _argument} ->
%{input | arguments: Map.put(input.arguments, argument.name, nil)}
{:constrained, {:error, error}, argument} ->
input = %{
input
| arguments: Map.put(input.arguments, argument.name, value)
}
add_invalid_errors(value, input, argument, error)
{:error, error} ->
input = %{
input
| arguments: Map.put(input.arguments, argument.name, value)
}
add_invalid_errors(value, input, argument, error)
end
else
input
end
else
input
end
end
@doc """
Sets a private argument value on the action input.
Private arguments are not exposed in the public API and can only be set
internally. This function will only work for arguments marked as `public?: false`
in the action definition.
## Examples
# Set a private argument (assuming :internal_flag is private)
iex> MyApp.Post
...> |> Ash.ActionInput.for_action(:example, %{})
...> |> Ash.ActionInput.set_private_argument(:internal_flag, true)
...> |> Ash.ActionInput.get_argument(:internal_flag)
true
# Attempting to set a public argument as private will error
iex> input = MyApp.Post
...> |> Ash.ActionInput.for_action(:example, %{})
...> |> Ash.ActionInput.set_private_argument(:public_arg, "value")
iex> input.valid?
false
# Use in action implementations for internal state
iex> MyApp.Post
...> |> Ash.ActionInput.for_action(:complex_workflow, %{data: "user_data"})
...> |> Ash.ActionInput.set_private_argument(:workflow_step, 1)
## See also
- `set_argument/3` for setting public arguments
- `get_argument/2` for retrieving argument values
- Action argument definitions with `public?: false`
"""
@spec set_private_argument(input :: t(), name :: atom, value :: term()) :: t()
def set_private_argument(input, name, value) do
argument =
Enum.find(
input.action.arguments,
&(&1.name == name || to_string(&1.name) == name)
)
cond do
is_nil(argument) ->
input
argument.public? ->
add_invalid_errors(
value,
input,
argument,
"can't set public arguments with set_private_argument/3"
)
true ->
set_argument(input, name, value)
end
end
@doc """
Deep merges the provided map into the input context.
Context is used to pass additional information through the action pipeline
that can be accessed by action implementations, changes, and validations.
The context is merged deeply, so nested maps will be combined rather than replaced.
Do not use the `private` key in your custom context, as that is reserved for
internal use.
## Examples
# Set simple context values
iex> MyApp.Post
...> |> Ash.ActionInput.new()
...> |> Ash.ActionInput.set_context(%{source: "api", user_id: 123})
...> |> then(& &1.context.source)
"api"
# Context is merged deeply
iex> input = MyApp.Post
...> |> Ash.ActionInput.new()
...> |> Ash.ActionInput.set_context(%{metadata: %{version: 1}})
...> |> Ash.ActionInput.set_context(%{metadata: %{trace_id: "abc123"}})
iex> input.context.metadata
%{version: 1, trace_id: "abc123"}
# Use context in action implementations
iex> MyApp.Post
...> |> Ash.ActionInput.for_action(:process_data, %{data: "test"})
...> |> Ash.ActionInput.set_context(%{
...> request_id: "req_456",
...> feature_flags: %{new_algorithm: true}
...> })
## See also
- `for_action/4` for setting context when creating inputs
- Action implementations can access context for custom logic
- `set_tenant/2` for tenant-specific context
"""
@spec set_context(t(), map | nil) :: t()
def set_context(input, nil), do: input
def set_context(input, map) do
%{
input
| context:
input.context
|> Ash.Helpers.deep_merge_maps(map)
|> then(&Ash.Helpers.deep_merge_maps(&1, map[:shared] || %{}))
}
end
defp cast_params(input, params, opts) do
input = %{
input
| params: Map.merge(input.params, Enum.into(params, %{}))
}
skip_unknown_inputs =
List.wrap(opts[:skip_unknown_inputs] || input.action.skip_unknown_inputs)
Enum.reduce(params, input, fn {name, value}, input ->
cond do
has_argument?(input.action, name) ->
set_argument(input, name, value)
match?("_" <> _, name) ->
input
:* in skip_unknown_inputs ->
input
name in skip_unknown_inputs ->
input
true ->
error =
Ash.Error.Invalid.NoSuchInput.exception(
resource: input.resource,
action: input.action.name,
input: name,
inputs: Enum.map(input.action.arguments, & &1.name)
)
add_error(input, Ash.Error.set_path(error, name))
end
end)
end
defp has_argument?(action, name) when is_atom(name) do
Enum.any?(action.arguments, &(&1.public? && &1.name == name))
end
defp has_argument?(action, name) when is_binary(name) do
Enum.any?(action.arguments, &(&1.public? && to_string(&1.name) == name))
end
defp add_invalid_errors(value, input, attribute, message) do
input = %{input | invalid_keys: MapSet.put(input.invalid_keys, attribute.name)}
messages =
if Keyword.keyword?(message) do
[message]
else
List.wrap(message)
end
Enum.reduce(messages, input, fn message, input ->
if is_exception(message) do
error =
message
|> Ash.Error.to_ash_error()
errors =
case error do
%class{errors: errors}
when class in [
Ash.Error.Invalid,
Ash.Error.Unknown,
Ash.Error.Forbidden,
Ash.Error.Framework
] ->
errors
error ->
[error]
end
Enum.reduce(errors, input, fn error, input ->
add_error(input, Ash.Error.set_path(error, attribute.name))
end)
else
opts = Ash.Type.Helpers.error_to_exception_opts(message, attribute)
Enum.reduce(opts, input, fn opts, input ->
error =
InvalidArgument.exception(
value: value,
field: Keyword.get(opts, :field),
message: Keyword.get(opts, :message),
vars: opts
)
error =
if opts[:path] do
Ash.Error.set_path(error, opts[:path])
else
error
end
add_error(input, error)
end)
end
end)
end
@doc """
Adds an error to the errors list and marks the action input as invalid.
This function allows you to add validation errors or other issues to the
action input. Once an error is added, the input will be marked as invalid
and action execution will be prevented.
## Examples
# Add a simple string error
iex> input = MyApp.Post
...> |> Ash.ActionInput.for_action(:send_notification, %{})
...> |> Ash.ActionInput.add_error("Missing required configuration")
iex> input.valid?
false
# Add an error with a specific path
iex> input = MyApp.Post
...> |> Ash.ActionInput.for_action(:process_data, %{})
...> |> Ash.ActionInput.add_error("Invalid format", [:data, :format])
iex> input.errors |> List.first() |> Map.get(:path)
[:data, :format]
# Add multiple errors
iex> input = MyApp.Post
...> |> Ash.ActionInput.for_action(:complex_action, %{})
...> |> Ash.ActionInput.add_error(["Error 1", "Error 2"])
iex> length(input.errors)
2
# Add structured error with keyword list
iex> MyApp.Post
...> |> Ash.ActionInput.for_action(:validate_input, %{})
...> |> Ash.ActionInput.add_error(field: :email, message: "is invalid")
## See also
- `Ash.Error.to_ash_error/3` for more on supported error values
- Action implementations can use this to add custom validation errors
- `set_argument/3` automatically adds errors for invalid argument values
"""
@spec add_error(
t(),
Ash.Error.error_input() | list(Ash.Error.error_input()),
Ash.Error.path_input()
) :: t()
@spec add_error(t(), Ash.Error.error_input() | list(Ash.Error.error_input())) :: t()
def add_error(input, errors, path \\ [])
def add_error(input, [], _path) do
input
end
def add_error(input, errors, path) when is_list(errors) do
if Keyword.keyword?(errors) do
errors
|> to_change_errors()
|> Ash.Error.set_path(path)
|> handle_error(input)
else
Enum.reduce(errors, input, &add_error(&2, &1, path))
end
end
def add_error(input, error, path) when is_binary(error) do
add_error(
input,
InvalidArgument.exception(message: error),
path
)
end
def add_error(input, error, path) do
error
|> Ash.Error.set_path(path)
|> handle_error(input)
end
defp handle_error(error, input) do
%{input | valid?: false, errors: [error | input.errors]}
end
defp to_change_errors(keyword) do
errors =
if keyword[:fields] && keyword[:fields] != [] do
Enum.map(keyword[:fields], fn field ->
InvalidArgument.exception(
field: field,
message: keyword[:message],
value: keyword[:value],
vars: keyword
)
end)
else
InvalidArgument.exception(
field: keyword[:field],
message: keyword[:message],
value: keyword[:value],
vars: keyword
)
end
if keyword[:path] do
Enum.map(errors, &Ash.Error.set_path(&1, keyword[:path]))
else
errors
end
end
end