Packages
ash
3.4.34
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/code_interface.ex
defmodule Ash.CodeInterface do
@moduledoc """
Used to define the functions of a code interface for a resource.
"""
@doc false
def require_action(resource, interface) do
action = Ash.Resource.Info.action(resource, interface.action || interface.name)
unless action do
raise Spark.Error.DslError,
module: resource,
message:
"The interface of #{inspect(resource)} refers to a non-existent action #{interface.action || interface.name}",
path: [:interfaces, :interface, interface.name]
end
action
end
@doc false
def default_value(resource, action, key) do
{field_type, field} =
case Enum.find(action.arguments, fn argument ->
argument.name == key
end) do
nil ->
{:attribute, Ash.Resource.Info.attribute(resource, key)}
argument ->
{:argument, argument}
end
cond do
field.allow_nil? && !(field.name in Map.get(action, :require_attributes, [])) ->
:ok
field.name in Map.get(action, :allow_nil_input, []) ->
:ok
!(field.name in Map.get(action, :accept, [])) ->
:ok
true ->
raise "Code interface for #{action.name} has optional argument #{key} but it is not optional"
end
default =
if field_type == :argument do
field.default
else
if action.type == :update || (action.type == :destroy && action.soft?) do
if is_nil(field.update_default) do
field.default
else
field.update_default
end
else
field.default
end
end
if is_function(default) do
quote do
unquote(Macro.escape(default)).()
end
else
quote do
unquote(Macro.escape(default))
end
end
end
def without_optional(keys) do
Enum.map(keys, fn
{:optional, key} ->
key
key ->
key
end)
end
def unwrap_calc_interface_args(keys, resource, arguments, function_head? \\ false) do
{Enum.map(keys, &unwrap_calc_interface_arg_binding(resource, arguments, &1, function_head?)),
Enum.map(keys, &unwrap_calc_interface_arg_access(&1))}
end
defp unwrap_calc_interface_arg_access({:optional, value}),
do: unwrap_calc_interface_arg_access(value)
defp unwrap_calc_interface_arg_access({:optional, value, _}),
do: unwrap_calc_interface_arg_access(value)
defp unwrap_calc_interface_arg_access(value) do
case value do
:_record ->
[type: :_record, name: :record, value: {:record, [], Elixir}]
{tag, name} ->
[type: tag, name: name, value: {name, [], Elixir}]
name ->
[type: :both, name: name, value: {name, [], Elixir}]
end
end
defp unwrap_calc_interface_arg_binding(resource, arguments, {:optional, binding}, head?) do
access = unwrap_calc_interface_arg_binding(resource, arguments, binding, head?)
if head? do
{:\\, [], [access, default_calc_value(resource, arguments, binding)]}
else
access
end
end
defp unwrap_calc_interface_arg_binding(
resource,
arguments,
{:optional, binding, default},
head?
) do
access = unwrap_calc_interface_arg_binding(resource, arguments, binding, head?)
if head? do
{:\\, [], [access, default]}
else
access
end
end
defp unwrap_calc_interface_arg_binding(_resource, _arguments, {tag, value}, _)
when tag in [:arg, :ref] do
{value, [], Elixir}
end
defp unwrap_calc_interface_arg_binding(resource, _arguments, :_record, false) do
{:=, [],
[
{:%, [], [{:__aliases__, [alias: false], [resource]}, {:%{}, [], []}]},
{:record, [], Elixir}
]}
end
defp unwrap_calc_interface_arg_binding(_resource, _arguments, value, _) do
{value, [], Elixir}
end
@doc false
def default_calc_value(_resource, arguments, {:arg, arg_name}) do
arguments
|> Enum.find(&(&1.name == arg_name))
|> case do
nil ->
nil
argument ->
argument.default
end
end
def default_calc_value(resource, _, {:ref, attribute}) do
resource
|> Ash.Resource.Info.attributes()
|> Enum.find(&(&1.name == attribute))
|> case do
nil ->
nil
attribute ->
attribute.default
end
end
def default_calc_value(resource, arguments, name) do
case default_calc_value(resource, arguments, {:arg, name}) do
nil ->
default_calc_value(resource, arguments, {:ref, name})
value ->
value
end
end
@doc false
# sobelow_skip ["DOS.BinToAtom", "DOS.StringToAtom"]
def resolve_calc_method_names(name) do
if name |> to_string() |> String.ends_with?("?") do
safe_name = name |> to_string() |> String.trim_trailing("?") |> String.to_atom()
bang_name = name
{safe_name, bang_name}
else
safe_name = name
bang_name = "#{name}!" |> String.to_atom()
{safe_name, bang_name}
end
end
@doc """
Defines the code interface for a given resource + domain combination in the current module. For example:
```elixir
defmodule MyApp.Accounting do
require Ash.CodeInterface
Ash.CodeInterface.define_interface(MyApp.Accounting, MyApp.Accounting.Transaction)
Ash.CodeInterface.define_interface(MyApp.Accounting, MyApp.Accounting.Account)
Ash.CodeInterface.define_interface(MyApp.Accounting, MyApp.Accounting.Invoice)
end
```
"""
defmacro define_interface(domain, resource, definitions \\ nil) do
quote bind_quoted: [domain: domain, resource: resource, definitions: definitions],
generated: true,
location: :keep do
calculation_interfaces =
case definitions do
nil ->
Ash.Resource.Info.calculation_interfaces(resource)
definitions ->
Enum.filter(definitions, &match?(%Ash.Resource.CalculationInterface{}, &1))
end
interfaces =
case definitions do
nil ->
Ash.Resource.Info.interfaces(resource)
definitions ->
Enum.filter(definitions, &match?(%Ash.Resource.Interface{}, &1))
end
interfaces_for_defaults =
Enum.group_by(calculation_interfaces, fn interface ->
{interface.name, Enum.count(interface.args, &is_atom/1), Enum.count(interface.args)}
end)
for {{name, arity, optional_arity}, interfaces} <- interfaces_for_defaults do
args =
case interfaces do
[%{args: args, calculation: calculation}] ->
calculation = Ash.Resource.Info.calculation(resource, calculation)
{arg_bindings, _arg_access} =
args
|> Kernel.||([])
|> Ash.CodeInterface.unwrap_calc_interface_args(
resource,
calculation.arguments,
true
)
arg_bindings
multiple ->
multiple
|> Enum.map(fn interface ->
interface.args
|> Enum.flat_map(fn
{:optional, value} ->
calculation = Ash.Resource.Info.calculation(resource, interface.calculation)
[Ash.CodeInterface.default_calc_value(resource, calculation.arguments, value)]
{:optional, _, value} ->
[value]
_ ->
[]
end)
end)
|> Enum.uniq()
|> case do
[_] ->
interface = hd(multiple)
calculation = Ash.Resource.Info.calculation(resource, interface.calculation)
{arg_bindings, _arg_access} =
interface.args
|> Kernel.||([])
|> Ash.CodeInterface.unwrap_calc_interface_args(
resource,
calculation.arguments,
true
)
arg_bindings
_duplicates ->
raise """
The generated function #{name}/#{arity + optional_arity} would have
multiple different sets of default values for arguments. Please use a different
name for conflicting code interface functions.
"""
end
end
{safe_name, bang_name} = Ash.CodeInterface.resolve_calc_method_names(name)
def unquote(bang_name)(unquote_splicing(args), opts \\ [])
def unquote(safe_name)(unquote_splicing(args), opts \\ [])
end
for interface <- calculation_interfaces do
calculation = Ash.Resource.Info.calculation(resource, interface.calculation)
{arg_bindings, arg_access} =
interface.args
|> Kernel.||([])
|> Ash.CodeInterface.unwrap_calc_interface_args(resource, calculation.arguments)
{safe_name, bang_name} = Ash.CodeInterface.resolve_calc_method_names(interface.name)
opts_location = Enum.count(arg_bindings)
interface_options = Ash.Resource.Interface.interface_options(:calculate, nil)
@doc """
#{calculation.description || "Calculates #{calculation.name} action on #{inspect(resource)}."}
#{Ash.CodeInterface.describe_calculation(resource, calculation, interface.args)}
### Options
#{interface_options.docs()}
"""
|> Ash.CodeInterface.trim_double_newlines()
@doc spark_opts: [
{opts_location, interface_options.schema()}
]
def unquote(bang_name)(unquote_splicing(arg_bindings), opts) do
{refs, arguments, record} =
Enum.reduce(
[unquote_splicing(arg_access)],
{%{}, %{}, nil},
fn config, {refs, arguments, record} ->
case config[:type] do
:_record ->
{refs, arguments, config[:value]}
:both ->
{Map.put(refs, config[:name], config[:value]),
Map.put(arguments, config[:name], config[:value]), record}
:ref ->
{Map.put(refs, config[:name], config[:value]), arguments, record}
:arg ->
{refs, Map.put(arguments, config[:name], config[:value]), record}
end
end
)
opts = [domain: unquote(domain), refs: refs, args: arguments, record: record] ++ opts
Ash.calculate!(unquote(resource), unquote(interface.calculation), opts)
end
@doc """
#{calculation.description || "Calculates #{calculation.name} action on #{inspect(resource)}."}
#{Ash.CodeInterface.describe_calculation(resource, calculation, interface.args)}
### Options
#{interface_options.docs()}
"""
|> Ash.CodeInterface.trim_double_newlines()
@doc spark_opts: [
{opts_location, interface_options.schema()}
]
def unquote(safe_name)(unquote_splicing(arg_bindings), opts) do
{refs, arguments, record} =
Enum.reduce(
[unquote_splicing(arg_access)],
{%{}, %{}, nil},
fn config, {refs, arguments, record} ->
case config[:type] do
:_record ->
{refs, arguments, config[:value]}
:both ->
{Map.put(refs, config[:name], config[:value]),
Map.put(arguments, config[:name], config[:value]), record}
:ref ->
{Map.put(refs, config[:name], config[:value]), arguments, record}
:arg ->
{refs, Map.put(arguments, config[:name], config[:value]), record}
end
end
)
opts = [domain: unquote(domain), refs: refs, args: arguments, record: record] ++ opts
Ash.calculate(unquote(resource), unquote(interface.calculation), opts)
end
end
for interface <- interfaces do
action = Ash.CodeInterface.require_action(resource, interface)
filter_keys =
cond do
action.type not in [:read, :update, :destroy] ->
[]
interface.get_by_identity ->
Ash.Resource.Info.identity(resource, interface.get_by_identity).keys
interface.get_by ->
interface.get_by
true ->
[]
end
args = List.wrap(filter_keys) ++ Ash.CodeInterface.without_optional(interface.args || [])
arg_vars = Enum.map(args, &{&1, [], Elixir})
arg_vars_function =
filter_keys
|> List.wrap()
|> Enum.concat(interface.args || [])
|> Enum.map(fn
{:optional, key} ->
default = Ash.CodeInterface.default_value(resource, action, key)
{:\\, [], [{key, [], Elixir}, default]}
key ->
{key, [], Elixir}
end)
unless Enum.uniq(args) == args do
raise """
Arguments #{inspect(args)} for #{interface.name} are not unique!
"""
end
interface =
if Map.get(action, :get?) do
%{interface | get?: true}
else
interface
end
interface_options = Ash.Resource.Interface.interface_options(action.type, interface)
resolve_opts_params =
quote do
{params, opts} =
if opts == [] && Keyword.keyword?(params_or_opts),
do:
{%{},
unquote(interface_options).validate!(params_or_opts)
|> unquote(interface_options).to_options()},
else:
{params_or_opts,
unquote(interface_options).validate!(opts)
|> unquote(interface_options).to_options()}
params =
unquote(args)
|> Enum.zip([unquote_splicing(arg_vars)])
|> Enum.reduce(params, fn {key, value}, params ->
Map.put(params, key, value)
end)
end
resolve_bang_opts_params =
quote do
{params, opts} =
if opts == [] && Keyword.keyword?(params_or_opts),
do:
{if(params_or_opts != [], do: %{}, else: []),
unquote(interface_options).validate!(params_or_opts)
|> unquote(interface_options).to_options()},
else:
{if(Keyword.keyword?(params_or_opts),
do: Map.new(params_or_opts),
else: params_or_opts
),
unquote(interface_options).validate!(opts)
|> unquote(interface_options).to_options()}
params =
if is_list(params) do
to_merge =
unquote(args)
|> Enum.zip([unquote_splicing(arg_vars)])
|> Map.new()
Enum.map(params, fn params ->
Map.merge(params, to_merge)
end)
else
unquote(args)
|> Enum.zip([unquote_splicing(arg_vars)])
|> Enum.reduce(params, fn {key, value}, params ->
Map.put(params, key, value)
end)
end
end
{subject, subject_args, resolve_subject, act, act!} =
case action.type do
:action ->
subject = quote do: input
resolve_subject =
quote do
{input_opts, opts} =
Keyword.split(opts, [:input, :actor, :tenant, :authorize?, :tracer])
{input, input_opts} = Keyword.pop(input_opts, :input)
input_opts = Keyword.put(input_opts, :domain, unquote(domain))
case input do
%Ash.ActionInput{resource: unquote(resource)} ->
input
%Ash.ActionInput{resource: other_resource} ->
raise ArgumentError,
"Action input resource #{inspect(other_resource)} does not match expected resource #{inspect(unquote(resource))}."
input ->
input
end
input =
input
|> Kernel.||(unquote(resource))
|> Ash.ActionInput.for_action(unquote(action.name), params, input_opts)
end
act = quote do: Ash.run_action(input, opts)
act! = quote do: Ash.run_action!(input, opts)
{subject, [], resolve_subject, act, act!}
:read ->
subject = quote do: query
resolve_subject =
quote do
{query_opts, opts} =
Keyword.split(opts, [:query, :actor, :tenant, :authorize?, :tracer, :context])
{query, query_opts} = Keyword.pop(query_opts, :query)
query_opts = Keyword.put(query_opts, :domain, unquote(domain))
query =
case query do
%Ash.Query{resource: unquote(resource)} = query ->
query
%Ash.Query{resource: other_resource} ->
raise ArgumentError,
"Query resource #{inspect(other_resource)} does not match expected resource #{inspect(unquote(resource))}."
unquote(resource) ->
unquote(resource)
|> Ash.Query.new()
other_resource
when is_atom(other_resource) and not is_nil(other_resource) ->
raise ArgumentError,
"Query resource #{inspect(other_resource)} does not match expected resource #{inspect(unquote(resource))}."
query ->
Ash.Query.build(unquote(resource), query || [])
end
query =
if unquote(filter_keys) && !Enum.empty?(unquote(filter_keys)) do
require Ash.Query
{filters, params} = Map.split(params, unquote(filter_keys))
query
|> Ash.Query.for_read(unquote(action.name), params, query_opts)
|> Ash.Query.filter(filters)
else
Ash.Query.for_read(query, unquote(action.name), params, query_opts)
end
end
resolve_not_found_error? =
quote do
{not_found_error?, opts} = Keyword.pop(opts, :not_found_error?)
not_found_error? =
if not_found_error? != nil,
do: not_found_error?,
else: unquote(interface.not_found_error?)
end
act =
if interface.get? do
quote do
unquote(resolve_not_found_error?)
Ash.read_one(query, Keyword.drop(opts, [:stream?, :stream_options]))
|> case do
{:ok, nil} when not_found_error? ->
{:error, Ash.Error.Query.NotFound.exception(resource: query.resource)}
result ->
result
end
end
else
quote do: Ash.read(query, Keyword.drop(opts, [:stream?, :stream_options]))
end
act! =
if interface.get? do
quote do
unquote(resolve_not_found_error?)
Ash.read_one!(query, Keyword.drop(opts, [:stream?, :stream_options]))
|> case do
nil when not_found_error? ->
raise Ash.Error.Query.NotFound, resource: query.resource
result ->
result
end
end
else
quote do
if opts[:stream?] do
Ash.stream!(query, Keyword.drop(opts, [:stream?, :stream_options]))
else
Ash.read!(query, Keyword.drop(opts, [:stream?, :stream_options]))
end
end
end
{subject, [], resolve_subject, act, act!}
:create ->
subject = quote do: changeset
resolve_subject =
quote do
{changeset, opts} = Keyword.pop(opts, :changeset)
{changeset_opts, opts} =
Keyword.split(opts, [
:actor,
:tenant,
:authorize?,
:tracer,
:context,
:skip_unknown_inputs
])
changeset_opts = Keyword.put(changeset_opts, :domain, unquote(domain))
changeset =
if is_map(params) do
changeset
|> Kernel.||(unquote(resource))
|> case do
%Ash.Changeset{resource: unquote(resource)} ->
changeset
%Ash.Changeset{resource: other_resource} ->
raise ArgumentError,
"Changeset #{inspect(changeset)} does not match expected resource #{inspect(unquote(resource))}."
other_resource
when is_atom(other_resource) and other_resource != unquote(resource) ->
raise ArgumentError,
"Resource #{inspect(other_resource)} does not match expected resource #{inspect(unquote(resource))}."
changeset ->
changeset
end
|> Ash.Changeset.for_create(unquote(action.name), params, changeset_opts)
else
{:bulk, params}
end
end
act =
quote do
case changeset do
{:bulk, inputs} ->
bulk_opts =
opts
|> Keyword.delete(:bulk_options)
|> Keyword.merge(Keyword.get(opts, :bulk_options, []))
|> Enum.concat(changeset_opts)
Ash.bulk_create(
inputs,
unquote(resource),
unquote(action.name),
bulk_opts
)
changeset ->
Ash.create(changeset, Keyword.delete(opts, :bulk_options))
end
end
act! =
quote do
case changeset do
{:bulk, inputs} ->
bulk_opts =
opts
|> Keyword.delete(:bulk_options)
|> Keyword.merge(Keyword.get(opts, :bulk_options, []))
|> Enum.concat(changeset_opts)
Ash.bulk_create!(
inputs,
unquote(resource),
unquote(action.name),
bulk_opts
)
changeset ->
Ash.create!(changeset, Keyword.delete(opts, :bulk_options))
end
end
{subject, [], resolve_subject, act, act!}
:update ->
subject = quote do: changeset
subject_args =
if interface.require_reference? do
quote do: [record]
else
[]
end
resolve_subject =
if Enum.empty?(filter_keys) and interface.require_reference? do
quote do
{changeset_opts, opts} =
Keyword.split(opts, [
:actor,
:tenant,
:authorize?,
:tracer,
:context,
:skip_unknown_inputs
])
changeset_opts = Keyword.put(changeset_opts, :domain, unquote(domain))
changeset =
record
|> case do
%Ash.Changeset{resource: unquote(resource)} ->
{filters, params} = Map.split(params, unquote(filter_keys))
record
|> Ash.Changeset.filter(filters)
|> Ash.Changeset.for_update(
unquote(action.name),
params,
changeset_opts
)
%Ash.Changeset{resource: other_resource} ->
raise ArgumentError,
"Changeset #{inspect(record)} does not match expected resource #{inspect(unquote(resource))}."
%struct{} = record when struct == unquote(resource) ->
{filters, params} = Map.split(params, unquote(filter_keys))
record
|> Ash.Changeset.new()
|> Ash.Changeset.filter(filters)
|> Ash.Changeset.for_update(
unquote(action.name),
params,
changeset_opts
)
%Ash.Query{} = query ->
{:atomic, :query, query}
%other_resource{} when other_resource != unquote(resource) ->
raise ArgumentError,
"Record #{inspect(record)} does not match expected resource #{inspect(unquote(resource))}."
[{_key, _val} | _] = id ->
{:atomic, :id, id}
list when is_list(list) ->
{:atomic, :stream, list}
other ->
{:atomic, :id, other}
end
end
else
quote do
filters = Map.take(params, unquote(filter_keys))
{changeset_opts, opts} =
Keyword.split(opts, [
:actor,
:tenant,
:authorize?,
:tracer,
:context,
:skip_unknown_inputs
])
changeset_opts = Keyword.put(changeset_opts, :domain, unquote(domain))
changeset =
{:atomic, :query, Ash.Query.do_filter(unquote(resource), filters)}
end
end
act =
quote do
{filters, params} = Map.split(params, unquote(filter_keys))
case changeset do
{:atomic, method, id} ->
bulk_opts =
opts
|> Keyword.drop([:bulk_options, :atomic_upgrade?])
|> Keyword.merge(Keyword.get(opts, :bulk_options, []))
|> Enum.concat(changeset_opts)
|> Keyword.put(:resource, unquote(resource))
|> then(fn bulk_opts ->
if method == :id || unquote(interface.get?) do
bulk_opts
|> Keyword.put(:return_records?, true)
|> Keyword.put(:return_errors?, true)
|> Keyword.put_new(:authorize_with, :error)
|> Keyword.put(:notify?, true)
else
bulk_opts
end
end)
|> Keyword.put_new(:strategy, [:atomic, :stream, :atomic_batches])
bulk_opts =
if method in [:stream, :query] do
Keyword.put(bulk_opts, :filter, filters)
else
bulk_opts
end
case Ash.CodeInterface.bulk_query(unquote(resource), method, id) do
{:ok, query} ->
query
|> Ash.bulk_update(unquote(action.name), params, bulk_opts)
|> case do
%Ash.BulkResult{} = result
when method in [:stream, :query] and not unquote(interface.get?) ->
result
%Ash.BulkResult{status: :success, records: [record]} = result ->
if opts[:return_notifications?] do
{:ok, record, result.notifications}
else
{:ok, record}
end
%Ash.BulkResult{status: :success, records: []} = result ->
{:error,
Ash.Error.to_error_class(
Ash.Error.Query.NotFound.exception(
resource: unquote(resource),
primary_key: id
)
)}
%Ash.BulkResult{status: :error, errors: errors} ->
{:error, Ash.Error.to_error_class(errors)}
end
{:error, error} ->
{:error, Ash.Error.to_error_class(error)}
end
changeset ->
Ash.update(changeset, Keyword.delete(opts, :bulk_options))
end
end
act! =
quote do
case changeset do
{:atomic, method, id} ->
{filters, params} = Map.split(params, unquote(filter_keys))
bulk_opts =
opts
|> Keyword.drop([:bulk_options, :atomic_upgrade?])
|> Keyword.merge(Keyword.get(opts, :bulk_options, []))
|> Enum.concat(changeset_opts)
|> Keyword.put(:resource, unquote(resource))
|> then(fn bulk_opts ->
if method == :id || unquote(interface.get?) do
bulk_opts
|> Keyword.put(:return_records?, true)
|> Keyword.put(:return_errors?, true)
|> Keyword.put_new(:authorize_with, :error)
|> Keyword.put(:notify?, true)
else
bulk_opts
end
end)
|> Keyword.put_new(:strategy, [:atomic, :stream, :atomic_batches])
bulk_opts =
if method in [:stream] do
Keyword.put(bulk_opts, :filter, filters)
else
bulk_opts
end
case Ash.CodeInterface.bulk_query(unquote(resource), method, id) do
{:ok, query} ->
query
|> Ash.bulk_update!(unquote(action.name), params, bulk_opts)
|> case do
%Ash.BulkResult{} = result
when method in [:stream, :query] and not unquote(interface.get?) ->
result
%Ash.BulkResult{status: :success, records: [record]} = result ->
if opts[:return_notifications?] do
{record, result.notifications}
else
record
end
%Ash.BulkResult{status: :success, records: []} = result ->
raise Ash.Error.to_error_class(
Ash.Error.Query.NotFound.exception(
resource: unquote(resource),
primary_key: id
)
)
end
{:error, error} ->
raise Ash.Error.to_error_class(error)
end
changeset ->
Ash.update!(changeset, Keyword.delete(opts, :bulk_options))
end
end
{subject, subject_args, resolve_subject, act, act!}
:destroy ->
subject = quote do: changeset
subject_args =
if interface.require_reference? do
quote do: [record]
else
[]
end
resolve_subject =
if interface.require_reference? do
quote do
{changeset_opts, opts} =
Keyword.split(opts, [
:actor,
:tenant,
:authorize?,
:tracer,
:context,
:skip_unknown_inputs
])
changeset_opts = Keyword.put(changeset_opts, :domain, unquote(domain))
changeset =
record
|> case do
%Ash.Changeset{resource: unquote(resource)} ->
{filters, params} = Map.split(params, unquote(filter_keys))
record
|> Ash.Changeset.filter(filters)
|> Ash.Changeset.for_destroy(
unquote(action.name),
params,
changeset_opts
)
%Ash.Changeset{resource: other_resource} ->
raise ArgumentError,
"Changeset #{inspect(record)} does not match expected resource #{inspect(unquote(resource))}."
%struct{} = record when struct == unquote(resource) ->
{filters, params} = Map.split(params, unquote(filter_keys))
record
|> Ash.Changeset.new()
|> Ash.Changeset.filter(filters)
|> Ash.Changeset.for_destroy(
unquote(action.name),
params,
changeset_opts
)
%Ash.Query{} = query ->
{:atomic, :query, query}
%other_resource{} when other_resource != unquote(resource) ->
raise ArgumentError,
"Record #{inspect(record)} does not match expected resource #{inspect(unquote(resource))}."
[{_key, _val} | _] = id ->
{:atomic, :id, id}
list when is_list(list) ->
{:atomic, :stream, list}
other ->
{:atomic, :id, other}
end
end
else
quote do
filters = Map.take(params, unquote(filter_keys))
{changeset_opts, opts} =
Keyword.split(opts, [
:actor,
:tenant,
:authorize?,
:tracer,
:context,
:skip_unknown_inputs
])
changeset_opts = Keyword.put(changeset_opts, :domain, unquote(domain))
changeset =
{:atomic, :query, Ash.Query.do_filter(unquote(resource), filters)}
end
end
act =
quote do
case changeset do
{:atomic, method, id} ->
{filters, params} = Map.split(params, unquote(filter_keys))
bulk_opts =
opts
|> Keyword.drop([:bulk_options, :return_destroyed?])
|> Keyword.merge(Keyword.get(opts, :bulk_options, []))
|> Enum.concat(changeset_opts)
|> Keyword.put(:resource, unquote(resource))
|> then(fn bulk_opts ->
if method == :id || unquote(interface.get?) do
bulk_opts
|> Keyword.put(:return_records?, opts[:return_destroyed?])
|> Keyword.put(:return_errors?, true)
|> Keyword.put_new(:authorize_with, :error)
|> Keyword.put(:notify?, true)
else
Keyword.put(bulk_opts, :return_records?, opts[:return_destroyed?])
end
end)
|> Keyword.put_new(:strategy, [:atomic, :stream, :atomic_batches])
bulk_opts =
if method in [:stream, :query] do
Keyword.put(bulk_opts, :filter, filters)
else
bulk_opts
end
case Ash.CodeInterface.bulk_query(unquote(resource), method, id) do
{:ok, query} ->
query
|> Ash.bulk_destroy(unquote(action.name), params, bulk_opts)
|> case do
%Ash.BulkResult{} = result
when method in [:stream, :query] and not unquote(interface.get?) ->
result
%Ash.BulkResult{status: :success, records: [record]} = result ->
if opts[:return_destroyed?] do
if opts[:return_notifications?] do
{:ok, record, result.notifications}
else
{:ok, record}
end
else
if opts[:return_notifications?] do
{:ok, result.notifications}
else
:ok
end
end
%Ash.BulkResult{status: :success, records: empty} = result
when empty in [[], nil] ->
if opts[:return_destroyed?] do
{:error,
Ash.Error.to_error_class(
Ash.Error.Query.NotFound.exception(
resource: unquote(resource),
primary_key: id
)
)}
else
if opts[:return_notifications?] do
{:ok, result.notifications}
else
:ok
end
end
%Ash.BulkResult{status: :error, errors: errors} ->
{:error, Ash.Error.to_error_class(errors)}
end
{:error, error} ->
{:error, Ash.Error.to_error_class(error)}
end
changeset ->
Ash.destroy(changeset, Keyword.delete(opts, :bulk_options))
end
end
act! =
quote do
case changeset do
{:atomic, method, id} ->
{filters, params} = Map.split(params, unquote(filter_keys))
bulk_opts =
opts
|> Keyword.drop([:bulk_options, :return_destroyed?])
|> Keyword.merge(Keyword.get(opts, :bulk_options, []))
|> Enum.concat(changeset_opts)
|> Keyword.put(:resource, unquote(resource))
|> then(fn bulk_opts ->
if method == :id || unquote(interface.get?) do
bulk_opts
|> Keyword.put(:return_records?, opts[:return_destroyed?])
|> Keyword.put(:return_errors?, true)
|> Keyword.put_new(:authorize_with, :error)
|> Keyword.put(:notify?, true)
else
Keyword.put(bulk_opts, :return_records?, opts[:return_destroyed?])
end
end)
|> Keyword.put_new(:strategy, [:atomic, :stream, :atomic_batches])
bulk_opts =
if method in [:stream, :query] do
Keyword.put(bulk_opts, :filter, filters)
else
bulk_opts
end
case Ash.CodeInterface.bulk_query(unquote(resource), method, id) do
{:ok, query} ->
query
|> Ash.bulk_destroy!(unquote(action.name), params, bulk_opts)
|> case do
%Ash.BulkResult{} = result
when method in [:stream, :query] and not unquote(interface.get?) ->
result
%Ash.BulkResult{status: :success, records: [record]} = result ->
if opts[:return_destroyed?] do
if opts[:return_notifications?] do
{record, result.notifications}
else
record
end
else
if opts[:return_notifications?] do
result.notifications
else
:ok
end
end
%Ash.BulkResult{status: :success, records: empty} = result
when empty in [[], nil] ->
if opts[:return_destroyed?] do
raise Ash.Error.to_error_class(
Ash.Error.Query.NotFound.exception(
resource: unquote(resource),
primary_key: id
)
)
else
if opts[:return_notifications?] do
{:ok, result.notifications}
else
:ok
end
end
end
{:error, error} ->
raise Ash.Error.to_error_class(error)
end
changeset ->
Ash.destroy!(changeset, Keyword.delete(opts, :bulk_options))
end
end
{subject, subject_args, resolve_subject, act, act!}
end
subject_name = elem(subject, 0)
resolve_subject =
quote do
unquote(resolve_subject)
end
common_args =
quote do: [
unquote_splicing(subject_args),
unquote_splicing(arg_vars_function),
params_or_opts \\ %{},
opts \\ []
]
first_opts_location = Enum.count(subject_args) + Enum.count(arg_vars_function)
@doc """
#{action.description || "Calls the #{action.name} action on #{inspect(resource)}."}
#{Ash.CodeInterface.describe_action(resource, action, interface.args)}
## Options
#{interface_options.docs()}
"""
|> Ash.CodeInterface.trim_double_newlines()
@dialyzer {:nowarn_function, {interface.name, length(common_args)}}
@doc spark_opts: [
{first_opts_location, interface_options.schema()},
{first_opts_location + 1, interface_options.schema()}
]
def unquote(interface.name)(unquote_splicing(common_args)) do
unquote(resolve_opts_params)
unquote(resolve_subject)
unquote(act)
end
@doc """
#{action.description || "Calls the #{action.name} action on #{inspect(resource)}."}
Raises any errors instead of returning them
#{Ash.CodeInterface.describe_action(resource, action, interface.args)}
## Options
#{interface_options.docs()}
"""
|> Ash.CodeInterface.trim_double_newlines()
# sobelow_skip ["DOS.BinToAtom"]
@dialyzer {:nowarn_function, {:"#{interface.name}!", length(common_args)}}
@doc spark_opts: [
{first_opts_location, interface_options.schema()},
{first_opts_location + 1, interface_options.schema()}
]
def unquote(:"#{interface.name}!")(unquote_splicing(common_args)) do
unquote(resolve_bang_opts_params)
unquote(resolve_subject)
unquote(act!)
end
# sobelow_skip ["DOS.BinToAtom"]
if subject_name in [:changeset, :query, :input] do
subject_opts =
Keyword.take(interface_options.schema(), [
:actor,
:tenant,
:authorize?,
:tracer,
:changeset,
:query,
:input
])
@dialyzer {:nowarn_function,
{:"#{subject_name}_to_#{interface.name}", length(common_args)}}
@doc spark_opts: [
{first_opts_location, interface_options.schema()},
{first_opts_location + 1, interface_options.schema()}
]
@doc """
Returns the #{subject_name} corresponding to the action.
## Options
#{Spark.Options.docs(subject_opts)}
"""
|> Ash.CodeInterface.trim_double_newlines()
@doc spark_opts: [
{first_opts_location, subject_opts},
{first_opts_location + 1, subject_opts}
]
def unquote(:"#{subject_name}_to_#{interface.name}")(unquote_splicing(common_args)) do
unquote(resolve_opts_params)
unquote(resolve_subject)
unquote(subject)
end
end
# sobelow_skip ["DOS.BinToAtom"]
@doc """
Runs authorization checks for `#{inspect(resource)}.#{action.name}`
See `Ash.can/3` for more information
## Options
#{Ash.Resource.Interface.CanOpts.docs()}
"""
|> Ash.CodeInterface.trim_double_newlines()
@dialyzer {:nowarn_function, {:"can_#{interface.name}", length(common_args) + 1}}
@doc spark_opts: [
{first_opts_location + 1, Ash.Resource.Interface.CanOpts.schema()},
{first_opts_location + 2, Ash.Resource.Interface.CanOpts.schema()}
]
def unquote(:"can_#{interface.name}")(actor, unquote_splicing(common_args)) do
{params, opts} =
if opts == [] && Keyword.keyword?(params_or_opts),
do:
{%{},
Ash.Resource.Interface.CanOpts.validate!(params_or_opts)
|> unquote(interface_options).to_options()},
else:
{params_or_opts,
Ash.Resource.Interface.CanOpts.validate!(opts)
|> unquote(interface_options).to_options()}
opts = Keyword.put(opts, :actor, actor)
unquote(resolve_subject)
case unquote(subject) do
%struct{} when struct in [Ash.Changeset, Ash.Query, Ash.ActionInput] ->
Ash.can(unquote(subject), actor, opts)
{:atomic, _, input} ->
raise "Ash.can_#{unquote(interface.name)} does not support #{inspect(input)} as input."
{:bulk, input} ->
raise "Ash.can_#{unquote(interface.name)} does not support #{inspect(input)} as input."
other ->
raise "Ash.can_#{unquote(interface.name)} does not support #{inspect(other)} as input."
end
end
# sobelow_skip ["DOS.BinToAtom"]
@dialyzer {:nowarn_function, {:"can_#{interface.name}?", length(common_args) + 1}}
@doc spark_opts: [
{first_opts_location + 1, Ash.Resource.Interface.CanQuestionMarkOpts.schema()},
{first_opts_location + 2, Ash.Resource.Interface.CanQuestionMarkOpts.schema()}
]
@doc """
Runs authorization checks for `#{inspect(resource)}.#{action.name}`, returning a boolean.
See `Ash.can?/3` for more information
## Options
#{Ash.Resource.Interface.CanQuestionMarkOpts.docs()}
"""
|> Ash.CodeInterface.trim_double_newlines()
def unquote(:"can_#{interface.name}?")(actor, unquote_splicing(common_args)) do
{params, opts} =
if opts == [] && Keyword.keyword?(params_or_opts),
do:
{%{},
Ash.Resource.Interface.CanQuestionMarkOpts.validate!(params_or_opts)
|> unquote(interface_options).to_options()},
else:
{params_or_opts,
Ash.Resource.Interface.CanQuestionMarkOpts.validate!(opts)
|> unquote(interface_options).to_options()}
opts = Keyword.put(opts, :actor, actor)
unquote(resolve_subject)
case unquote(subject) do
%struct{} when struct in [Ash.Changeset, Ash.Query, Ash.ActionInput] ->
Ash.can?(unquote(subject), actor, opts)
{:atomic, _, input} ->
raise "Ash.can_#{unquote(interface.name)}? does not support #{inspect(input)} as input."
{:bulk, input} ->
raise "Ash.can_#{unquote(interface.name)}? does not support #{inspect(input)} as input."
other ->
raise "Ash.can_#{unquote(interface.name)}? does not support #{inspect(other)} as input."
end
end
end
end
end
def describe_action(resource, action, args) do
resource
|> Ash.Resource.Info.action_inputs(action.name)
|> Enum.filter(&is_atom/1)
|> Enum.uniq()
|> case do
[] ->
""
inputs ->
{arguments, inputs} = Enum.split_with(inputs, &(&1 in (args || [])))
arguments =
Enum.map(arguments, &describe_input(resource, action, &1))
inputs =
Enum.map(inputs, &describe_input(resource, action, &1))
case {arguments, inputs} do
{[], []} ->
""
{arguments, []} ->
"""
# Arguments
#{Enum.join(arguments, "\n")}
"""
{[], inputs} ->
"""
# Inputs
#{Enum.join(inputs, "\n")}
"""
{arguments, inputs} ->
"""
# Arguments
#{Enum.join(arguments, "\n")}
# Inputs
#{Enum.join(inputs, "\n")}
"""
end
end
end
def describe_calculation(resource, calculation, args) do
calculation.arguments
|> Enum.map(& &1.name)
|> case do
[] ->
""
inputs ->
{arguments, inputs} = Enum.split_with(inputs, &(&1 in args))
arguments = Enum.sort_by(arguments, fn arg -> Enum.find_index(args, &(&1 == arg)) end)
arguments =
Enum.map(arguments, &describe_input(resource, calculation, &1))
inputs =
Enum.map(inputs, &describe_input(resource, calculation, &1))
case {arguments, inputs} do
{[], []} ->
""
{arguments, []} ->
"""
# Arguments
#{Enum.join(arguments, "\n")}
"""
{[], inputs} ->
"""
# Inputs
#{Enum.join(inputs, "\n")}
"""
{arguments, inputs} ->
"""
# Arguments
#{Enum.join(arguments, "\n")}
# Inputs
#{Enum.join(inputs, "\n")}
"""
end
end
end
defp describe_input(resource, %{arguments: arguments}, name) do
case Enum.find(arguments, &(&1.name == name)) do
nil ->
case Ash.Resource.Info.field(resource, name) do
nil ->
"* #{name}"
field ->
describe(field)
end
argument ->
describe(argument)
end
end
defp describe(%{name: name, description: description}) when not is_nil(description) do
"* #{name} - #{description}"
end
defp describe(%{name: name}) do
"* #{name}"
end
def trim_double_newlines(str) do
str
|> String.replace(~r/\n{2,}/, "\n")
|> String.trim_trailing()
end
@doc false
def bulk_query(resource, method, id) do
case method do
:query ->
{:ok, id}
:stream ->
{:ok, id}
:id ->
case Ash.Filter.get_filter(resource, id) do
{:ok, filter} ->
{:ok, Ash.Query.do_filter(resource, filter)}
{:error, error} ->
{:error, error}
end
end
end
end