mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 13:33:20 +12:00
547 lines
18 KiB
Elixir
547 lines
18 KiB
Elixir
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
|
|
|
|
if !field.allow_nil? do
|
|
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(action.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
|
|
|
|
@doc """
|
|
Defines the code interface for a given resource + api 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
|
|
```
|
|
|
|
Keep in mind that you can have this "automatically" defined in your resources by using the `define_for`
|
|
flag in a resource.
|
|
|
|
For example:
|
|
|
|
```elixir
|
|
defmodule MyApp.Accounting.Transaction do
|
|
use Ash.Resource
|
|
|
|
...
|
|
|
|
code_interface do
|
|
define_for MyApp.Accounting
|
|
|
|
define :start do
|
|
args [:invoice_id]
|
|
end
|
|
end
|
|
end
|
|
|
|
# Which can now be used like so:
|
|
|
|
MyApp.Accounting.Transaction.start!(invoice.id)
|
|
```
|
|
"""
|
|
defmacro define_interface(api, resource) do
|
|
quote bind_quoted: [api: api, resource: resource], generated: true, location: :keep do
|
|
for interface <- Ash.Resource.Info.interfaces(resource) do
|
|
action = Ash.CodeInterface.require_action(resource, interface)
|
|
|
|
filter_keys =
|
|
if action.type == :read do
|
|
if interface.get_by_identity do
|
|
Ash.Resource.Info.identity(resource, interface.get_by_identity).keys
|
|
else
|
|
if interface.get_by do
|
|
interface.get_by
|
|
end
|
|
end
|
|
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
|
|
|
|
doc = """
|
|
#{action.description || "Calls the #{action.name} action on the #{inspect(resource)} resource."}
|
|
|
|
## Options
|
|
|
|
#{Spark.OptionsHelpers.docs(Ash.Resource.Interface.interface_options(action.type))}
|
|
"""
|
|
|
|
case action.type do
|
|
:read ->
|
|
@doc doc
|
|
@dialyzer {:nowarn_function, {interface.name, Enum.count(args) + 2}}
|
|
def unquote(interface.name)(
|
|
unquote_splicing(arg_vars_function),
|
|
params_or_opts \\ %{},
|
|
opts \\ []
|
|
) do
|
|
if opts == [] && Keyword.keyword?(params_or_opts) do
|
|
apply(
|
|
__MODULE__,
|
|
elem(__ENV__.function, 0),
|
|
[
|
|
unquote_splicing(arg_vars_function),
|
|
%{},
|
|
params_or_opts
|
|
]
|
|
)
|
|
else
|
|
input =
|
|
unquote(args)
|
|
|> Enum.zip([unquote_splicing(arg_vars)])
|
|
|> Enum.reduce(params_or_opts, fn {key, value}, params_or_opts ->
|
|
Map.put(params_or_opts, key, value)
|
|
end)
|
|
|
|
query =
|
|
if unquote(filter_keys) do
|
|
require Ash.Query
|
|
{filters, input} = Map.split(input, unquote(filter_keys))
|
|
|
|
opts[:query]
|
|
|> Kernel.||(unquote(resource))
|
|
|> Ash.Query.for_read(
|
|
unquote(action.name),
|
|
input,
|
|
Keyword.take(opts, [:actor, :tenant, :authorize?, :tracer])
|
|
)
|
|
|> Ash.Query.filter(filters)
|
|
else
|
|
opts[:query]
|
|
|> Kernel.||(unquote(resource))
|
|
|> Ash.Query.for_read(
|
|
unquote(action.name),
|
|
input,
|
|
Keyword.take(opts, [:actor, :tenant, :authorize?, :tracer])
|
|
)
|
|
end
|
|
|
|
if unquote(interface.get? || action.get?) do
|
|
query
|
|
|> unquote(api).read_one(
|
|
Keyword.drop(opts, [:query, :tenant, :authorize?, :actor])
|
|
)
|
|
|> case do
|
|
{:ok, nil} ->
|
|
if unquote(interface.not_found_error?) == false ||
|
|
Keyword.get(opts, :not_found_error?) == false do
|
|
{:ok, nil}
|
|
else
|
|
{:error, Ash.Error.Query.NotFound.exception(resource: query.resource)}
|
|
end
|
|
|
|
{:ok, result} ->
|
|
{:ok, result}
|
|
|
|
{:error, error} ->
|
|
{:error, error}
|
|
end
|
|
else
|
|
unquote(api).read(
|
|
query,
|
|
Keyword.drop(opts, [:query, :tenant, :actor, :authorize?])
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc doc
|
|
# sobelow_skip ["DOS.BinToAtom"]
|
|
@dialyzer {:nowarn_function, {:"#{interface.name}!", Enum.count(args) + 2}}
|
|
def unquote(:"#{interface.name}!")(
|
|
unquote_splicing(arg_vars_function),
|
|
params_or_opts \\ %{},
|
|
opts \\ []
|
|
) do
|
|
if opts == [] && Keyword.keyword?(params_or_opts) do
|
|
apply(
|
|
__MODULE__,
|
|
elem(__ENV__.function, 0),
|
|
[
|
|
unquote_splicing(arg_vars),
|
|
%{},
|
|
params_or_opts
|
|
]
|
|
)
|
|
else
|
|
input =
|
|
unquote(args)
|
|
|> Enum.zip([unquote_splicing(arg_vars)])
|
|
|> Enum.reduce(params_or_opts, fn {key, value}, params_or_opts ->
|
|
Map.put(params_or_opts, key, value)
|
|
end)
|
|
|
|
query =
|
|
if unquote(filter_keys) do
|
|
require Ash.Query
|
|
{filters, input} = Map.split(input, unquote(filter_keys))
|
|
|
|
opts[:query]
|
|
|> Kernel.||(unquote(resource))
|
|
|> Ash.Query.for_read(
|
|
unquote(action.name),
|
|
input,
|
|
Keyword.take(opts, [:actor, :tenant, :authorize?, :tracer])
|
|
)
|
|
|> Ash.Query.filter(filters)
|
|
else
|
|
opts[:query]
|
|
|> Kernel.||(unquote(resource))
|
|
|> Ash.Query.for_read(
|
|
unquote(action.name),
|
|
input,
|
|
Keyword.take(opts, [:actor, :tenant, :authorize?, :tracer])
|
|
)
|
|
end
|
|
|
|
if unquote(interface.get? || action.get?) do
|
|
query
|
|
|> unquote(api).read_one!(
|
|
Keyword.drop(opts, [:query, :tenant, :authorize?, :actor])
|
|
)
|
|
|> case do
|
|
nil ->
|
|
if unquote(interface.not_found_error?) == false ||
|
|
Keyword.get(opts, :not_found_error?) == false do
|
|
nil
|
|
else
|
|
raise Ash.Error.Query.NotFound, resource: query.resource
|
|
end
|
|
|
|
result ->
|
|
result
|
|
end
|
|
else
|
|
unquote(api).read!(
|
|
query,
|
|
Keyword.drop(opts, [:query, :tenant, :actor, :authorize?])
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
:create ->
|
|
@doc doc
|
|
@dialyzer {:nowarn_function, {interface.name, Enum.count(args) + 2}}
|
|
def unquote(interface.name)(
|
|
unquote_splicing(arg_vars_function),
|
|
params_or_opts \\ %{},
|
|
opts \\ []
|
|
) do
|
|
if opts == [] && Keyword.keyword?(params_or_opts) do
|
|
apply(__MODULE__, elem(__ENV__.function, 0), [
|
|
unquote_splicing(arg_vars),
|
|
%{},
|
|
params_or_opts
|
|
])
|
|
else
|
|
input =
|
|
unquote(args)
|
|
|> Enum.zip([unquote_splicing(arg_vars)])
|
|
|> Enum.reduce(params_or_opts, fn {key, value}, params_or_opts ->
|
|
Map.put(params_or_opts, key, value)
|
|
end)
|
|
|
|
changeset =
|
|
opts[:changeset]
|
|
|> Kernel.||(unquote(resource))
|
|
|> Ash.Changeset.for_create(
|
|
unquote(action.name),
|
|
input,
|
|
Keyword.take(opts, [:actor, :tenant, :authorize?, :tracer])
|
|
)
|
|
|
|
unquote(api).create(
|
|
changeset,
|
|
Keyword.drop(opts, [:actor, :changeset, :tenant, :authorize?, :tracer])
|
|
)
|
|
end
|
|
end
|
|
|
|
@doc doc
|
|
@dialyzer {:nowarn_function, {:"#{interface.name}!", Enum.count(args) + 2}}
|
|
# sobelow_skip ["DOS.BinToAtom"]
|
|
def unquote(:"#{interface.name}!")(
|
|
unquote_splicing(arg_vars_function),
|
|
params_or_opts \\ %{},
|
|
opts \\ []
|
|
) do
|
|
if opts == [] && Keyword.keyword?(params_or_opts) do
|
|
apply(__MODULE__, elem(__ENV__.function, 0), [
|
|
unquote_splicing(arg_vars),
|
|
%{},
|
|
params_or_opts
|
|
])
|
|
else
|
|
input =
|
|
unquote(args)
|
|
|> Enum.zip([unquote_splicing(arg_vars)])
|
|
|> Enum.reduce(params_or_opts, fn {key, value}, params_or_opts ->
|
|
Map.put(params_or_opts, key, value)
|
|
end)
|
|
|
|
changeset =
|
|
(opts[:changeset] || unquote(resource))
|
|
|> Ash.Changeset.for_create(
|
|
unquote(action.name),
|
|
input,
|
|
Keyword.take(opts, [:actor, :tenant, :authorize?, :tracer])
|
|
)
|
|
|
|
unquote(api).create!(
|
|
changeset,
|
|
Keyword.drop(opts, [:actor, :changeset, :authorize?, :tracer])
|
|
)
|
|
end
|
|
end
|
|
|
|
:update ->
|
|
@doc doc
|
|
@dialyzer {:nowarn_function, {interface.name, Enum.count(args) + 3}}
|
|
def unquote(interface.name)(
|
|
record,
|
|
unquote_splicing(arg_vars_function),
|
|
params_or_opts \\ %{},
|
|
opts \\ []
|
|
) do
|
|
if opts == [] && Keyword.keyword?(params_or_opts) do
|
|
apply(__MODULE__, elem(__ENV__.function, 0), [
|
|
record,
|
|
unquote_splicing(arg_vars),
|
|
%{},
|
|
params_or_opts
|
|
])
|
|
else
|
|
input =
|
|
unquote(args)
|
|
|> Enum.zip([unquote_splicing(arg_vars)])
|
|
|> Enum.reduce(params_or_opts, fn {key, value}, params_or_opts ->
|
|
Map.put(params_or_opts, key, value)
|
|
end)
|
|
|
|
changeset =
|
|
record
|
|
|> Ash.Changeset.for_update(
|
|
unquote(action.name),
|
|
input,
|
|
Keyword.take(opts, [:actor, :tenant, :authorize?, :tracer])
|
|
)
|
|
|
|
unquote(api).update(
|
|
changeset,
|
|
Keyword.drop(opts, [:actor, :tenant, :authorize?, :tracer])
|
|
)
|
|
end
|
|
end
|
|
|
|
@doc doc
|
|
# sobelow_skip ["DOS.BinToAtom"]
|
|
@dialyzer {:nowarn_function, {:"#{interface.name}!", Enum.count(args) + 3}}
|
|
def unquote(:"#{interface.name}!")(
|
|
record,
|
|
unquote_splicing(arg_vars_function),
|
|
params_or_opts \\ %{},
|
|
opts \\ []
|
|
) do
|
|
if opts == [] && Keyword.keyword?(params_or_opts) do
|
|
apply(__MODULE__, elem(__ENV__.function, 0), [
|
|
record,
|
|
unquote_splicing(arg_vars),
|
|
%{},
|
|
params_or_opts
|
|
])
|
|
else
|
|
input =
|
|
unquote(args)
|
|
|> Enum.zip([unquote_splicing(arg_vars)])
|
|
|> Enum.reduce(params_or_opts, fn {key, value}, params_or_opts ->
|
|
Map.put(params_or_opts, key, value)
|
|
end)
|
|
|
|
changeset =
|
|
record
|
|
|> Ash.Changeset.for_update(
|
|
unquote(action.name),
|
|
input,
|
|
Keyword.take(opts, [:actor, :tenant, :authorize?, :tracer])
|
|
)
|
|
|
|
unquote(api).update!(
|
|
changeset,
|
|
Keyword.drop(opts, [:actor, :tenant, :authorize?, :tracer])
|
|
)
|
|
end
|
|
end
|
|
|
|
:destroy ->
|
|
@doc doc
|
|
@dialyzer {:nowarn_function, {interface.name, Enum.count(args) + 3}}
|
|
def unquote(interface.name)(
|
|
record,
|
|
unquote_splicing(arg_vars_function),
|
|
params_or_opts \\ %{},
|
|
opts \\ []
|
|
) do
|
|
if opts == [] && Keyword.keyword?(params_or_opts) do
|
|
apply(__MODULE__, elem(__ENV__.function, 0), [
|
|
record,
|
|
unquote_splicing(arg_vars),
|
|
%{},
|
|
params_or_opts
|
|
])
|
|
else
|
|
input =
|
|
unquote(args)
|
|
|> Enum.zip([unquote_splicing(arg_vars)])
|
|
|> Enum.reduce(params_or_opts, fn {key, value}, params_or_opts ->
|
|
Map.put(params_or_opts, key, value)
|
|
end)
|
|
|
|
changeset =
|
|
record
|
|
|> Ash.Changeset.for_destroy(
|
|
unquote(action.name),
|
|
input,
|
|
Keyword.take(opts, [:actor, :tenant, :authorize?, :tracer])
|
|
)
|
|
|
|
unquote(api).destroy(
|
|
changeset,
|
|
Keyword.drop(opts, [:actor, :tenant, :authorize?, :tracer])
|
|
)
|
|
end
|
|
end
|
|
|
|
@doc doc
|
|
# sobelow_skip ["DOS.BinToAtom"]
|
|
@dialyzer {:nowarn_function, {:"#{interface.name}!", Enum.count(args) + 3}}
|
|
def unquote(:"#{interface.name}!")(
|
|
record,
|
|
unquote_splicing(arg_vars_function),
|
|
params_or_opts \\ %{},
|
|
opts \\ []
|
|
) do
|
|
if opts == [] && Keyword.keyword?(params_or_opts) do
|
|
apply(__MODULE__, elem(__ENV__.function, 0), [
|
|
record,
|
|
unquote_splicing(arg_vars),
|
|
%{},
|
|
params_or_opts
|
|
])
|
|
else
|
|
input =
|
|
unquote(args)
|
|
|> Enum.zip([unquote_splicing(arg_vars)])
|
|
|> Enum.reduce(params_or_opts, fn {key, value}, params_or_opts ->
|
|
Map.put(params_or_opts, key, value)
|
|
end)
|
|
|
|
changeset =
|
|
record
|
|
|> Ash.Changeset.for_destroy(
|
|
unquote(action.name),
|
|
input,
|
|
Keyword.take(opts, [:actor, :tenant, :authorize?, :tracer])
|
|
)
|
|
|
|
unquote(api).destroy!(
|
|
changeset,
|
|
Keyword.drop(opts, [:actor, :tenant, :authorize?, :tracer])
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|