mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 05:23:03 +12:00
improvement: breaking change! don't define code interface by default
In an effort to improve compile times in general, and remove unnecessary compile time dependencies, the code interface is not defined by default. It is also now possible to define the code interface directly in the resource module, via ```elixir code_interface do define_for ApiModule end ``` If you need to reenable the code interface, simply add the following to your api module: ```elixir resources do define_interfaces? true ... end ```
This commit is contained in:
parent
a955d89b8f
commit
87627993b8
23 changed files with 602 additions and 511 deletions
|
@ -9,7 +9,6 @@ locals_without_parens = [
|
|||
args: 1,
|
||||
argument: 2,
|
||||
argument: 3,
|
||||
as: 1,
|
||||
attribute: 1,
|
||||
attribute: 2,
|
||||
attribute: 3,
|
||||
|
@ -34,6 +33,8 @@ locals_without_parens = [
|
|||
define: 1,
|
||||
define: 2,
|
||||
define_field?: 1,
|
||||
define_for: 1,
|
||||
define_interfaces?: 1,
|
||||
description: 1,
|
||||
destination_field: 1,
|
||||
destination_field_on_join_table: 1,
|
||||
|
@ -122,7 +123,6 @@ locals_without_parens = [
|
|||
validate: 2,
|
||||
validate_destination_field?: 1,
|
||||
violation_message: 1,
|
||||
warn_on_compile_failure?: 1,
|
||||
where: 1,
|
||||
writable?: 1
|
||||
]
|
||||
|
|
|
@ -493,6 +493,11 @@ defmodule Ash.Api do
|
|||
|> Enum.map(& &1.resource)
|
||||
end
|
||||
|
||||
@spec define_interfaces?(atom) :: boolean
|
||||
def define_interfaces?(api) do
|
||||
Extension.get_opt(api, [:resources], :define_interfaces?, false)
|
||||
end
|
||||
|
||||
@spec resource_references(Ash.Api.t()) :: [Ash.Api.ResourceReference.t()]
|
||||
def resource_references(api) do
|
||||
Extension.get_entities(api, [:resources])
|
||||
|
|
|
@ -3,27 +3,12 @@ defmodule Ash.Api.Dsl do
|
|||
name: :resource,
|
||||
describe: "A reference to a resource",
|
||||
target: Ash.Api.ResourceReference,
|
||||
modules: [:resource],
|
||||
args: [:resource],
|
||||
examples: [
|
||||
"resource MyApp.User"
|
||||
],
|
||||
# This is an internal tool used by embedded resources,
|
||||
# so we hide it from the documentation
|
||||
hide: [:warn_on_compile_failure?],
|
||||
schema: [
|
||||
warn_on_compile_failure?: [
|
||||
type: :atom,
|
||||
default: true
|
||||
],
|
||||
as: [
|
||||
type: :atom,
|
||||
required: false,
|
||||
doc: """
|
||||
A short name for the resource.
|
||||
|
||||
Can be used in calls to Api modules, e.g `Api.read(:special_thing)`.
|
||||
"""
|
||||
],
|
||||
resource: [
|
||||
type: :atom,
|
||||
required: true,
|
||||
|
@ -44,13 +29,23 @@ defmodule Ash.Api.Dsl do
|
|||
end
|
||||
"""
|
||||
],
|
||||
schema: [
|
||||
define_interfaces?: [
|
||||
type: :boolean,
|
||||
default: false,
|
||||
doc: """
|
||||
If set to true, the code interface of each resource will be defined in the api.
|
||||
|
||||
Keep in mind that this can increase the compile times of your application.
|
||||
"""
|
||||
]
|
||||
],
|
||||
entities: [
|
||||
@resource
|
||||
]
|
||||
}
|
||||
|
||||
@transformers [
|
||||
Ash.Api.Transformers.EnsureResourcesCompiled,
|
||||
Ash.Api.Transformers.ValidateRelatedResourceInclusion,
|
||||
Ash.Api.Transformers.ValidateRelationshipAttributes,
|
||||
Ash.Api.Transformers.ValidateManyToManyJoinAttributes
|
||||
|
@ -63,6 +58,12 @@ defmodule Ash.Api.Dsl do
|
|||
|
||||
Apis are the entrypoints for working with your resources.
|
||||
|
||||
Apis may optionally include a list of resources, in which case they can be
|
||||
used as an `Ash.Registry` in various places. This is for backwards compatibility,
|
||||
but if at all possible you should define an `Ash.Registry` if you are using an extension
|
||||
that requires a list of resources. For example, most extensions look for two application
|
||||
environment variables called `:ash_apis` and `:ash_registries` to find any potential registries
|
||||
|
||||
# Table of Contents
|
||||
#{Ash.Dsl.Extension.doc_index(@sections)}
|
||||
|
||||
|
|
|
@ -1,366 +1,16 @@
|
|||
defmodule Ash.Api.Interface do
|
||||
@moduledoc false
|
||||
|
||||
@doc false
|
||||
def require_action(resource, interface) do
|
||||
action = Ash.Resource.Info.action(resource, interface.action || interface.name)
|
||||
|
||||
unless action do
|
||||
raise Ash.Error.Dsl.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
|
||||
|
||||
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.Api.Interface.require_action(resource, interface)
|
||||
|
||||
args = interface.args || []
|
||||
arg_vars = Enum.map(args, &{&1, [], Elixir})
|
||||
|
||||
doc = """
|
||||
#{action.description || "Calls the #{action.name} action on the #{inspect(resource)} resource."}
|
||||
|
||||
## Options
|
||||
|
||||
#{Ash.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),
|
||||
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 =
|
||||
opts[:query]
|
||||
|> Kernel.||(unquote(resource))
|
||||
|> Ash.Query.for_read(
|
||||
unquote(action.name),
|
||||
input,
|
||||
Keyword.take(opts, [:actor, :tenant])
|
||||
)
|
||||
|
||||
if unquote(interface.get?) do
|
||||
query
|
||||
|> unquote(api).read_one(Keyword.drop(opts, [:query, :tenant]))
|
||||
|> case do
|
||||
{:ok, nil} ->
|
||||
{:error, Ash.Error.Query.NotFound.exception(resource: query.resource)}
|
||||
|
||||
{:ok, result} ->
|
||||
{:ok, result}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
else
|
||||
unquote(api).read(query, Keyword.drop(opts, [:query, :tenant]))
|
||||
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),
|
||||
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 =
|
||||
opts[:query]
|
||||
|> Kernel.||(unquote(resource))
|
||||
|> Ash.Query.for_read(
|
||||
unquote(action.name),
|
||||
input,
|
||||
Keyword.take(opts, [:actor, :tenant])
|
||||
)
|
||||
|
||||
if unquote(interface.get?) do
|
||||
query
|
||||
|> unquote(api).read_one!(Keyword.drop(opts, [:query, :tenant]))
|
||||
|> case do
|
||||
nil ->
|
||||
raise Ash.Error.Query.NotFound, resource: query.resource
|
||||
|
||||
result ->
|
||||
result
|
||||
end
|
||||
else
|
||||
unquote(api).read!(query, Keyword.drop(opts, [:query, :tenant]))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
:create ->
|
||||
@doc doc
|
||||
@dialyzer {:nowarn_function, {interface.name, Enum.count(args) + 2}}
|
||||
def unquote(interface.name)(
|
||||
unquote_splicing(arg_vars),
|
||||
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])
|
||||
)
|
||||
|
||||
unquote(api).create(changeset, Keyword.drop(opts, [:actor, :changeset, :tenant]))
|
||||
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),
|
||||
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])
|
||||
)
|
||||
|
||||
unquote(api).create!(changeset, Keyword.drop(opts, [:actor, :changeset]))
|
||||
end
|
||||
end
|
||||
|
||||
:update ->
|
||||
@doc doc
|
||||
@dialyzer {:nowarn_function, {interface.name, Enum.count(args) + 3}}
|
||||
def unquote(interface.name)(
|
||||
record,
|
||||
unquote_splicing(arg_vars),
|
||||
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])
|
||||
)
|
||||
|
||||
unquote(api).update(changeset, Keyword.drop(opts, [:actor, :tenant]))
|
||||
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),
|
||||
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])
|
||||
)
|
||||
|
||||
unquote(api).update!(changeset, Keyword.drop(opts, [:actor, :tenant]))
|
||||
end
|
||||
end
|
||||
|
||||
:destroy ->
|
||||
@doc doc
|
||||
@dialyzer {:nowarn_function, {interface.name, Enum.count(args) + 3}}
|
||||
def unquote(interface.name)(
|
||||
record,
|
||||
unquote_splicing(arg_vars),
|
||||
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])
|
||||
)
|
||||
|
||||
unquote(api).destroy(changeset, Keyword.drop(opts, [:actor, :tenant]))
|
||||
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),
|
||||
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])
|
||||
)
|
||||
|
||||
unquote(api).destroy!(changeset, Keyword.drop(opts, [:actor, :tenant]))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacro __using__(_) do
|
||||
quote bind_quoted: [], generated: true do
|
||||
alias Ash.Api
|
||||
|
||||
for resource <- Ash.Api.resources(__MODULE__) do
|
||||
Ash.Api.Interface.define_interface(__MODULE__, resource)
|
||||
if Ash.Api.define_interfaces?(__MODULE__) do
|
||||
require Ash.CodeInterface
|
||||
|
||||
for resource <- Ash.Api.resources(__MODULE__) do
|
||||
Ash.CodeInterface.define_interface(__MODULE__, resource)
|
||||
end
|
||||
end
|
||||
|
||||
def get!(resource, id_or_filter, params \\ []) do
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
defmodule Ash.Api.ResourceReference do
|
||||
@moduledoc "Represents a resource in an API"
|
||||
|
||||
defstruct [:resource, :as, warn_on_compile_failure?: true]
|
||||
defstruct [:resource, :as]
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
end
|
||||
|
|
|
@ -9,47 +9,16 @@ defmodule Ash.Api.Transformers.EnsureResourcesCompiled do
|
|||
alias Ash.Dsl.Transformer
|
||||
require Logger
|
||||
|
||||
def transform(module, dsl, times \\ 3) do
|
||||
@impl true
|
||||
def after_compile?, do: true
|
||||
|
||||
@impl true
|
||||
def transform(_api, dsl) do
|
||||
dsl
|
||||
|> Transformer.get_entities([:resources])
|
||||
|> Enum.filter(& &1.warn_on_compile_failure?)
|
||||
|> Enum.map(& &1.resource)
|
||||
|> Enum.map(fn resource ->
|
||||
try do
|
||||
# This is to get the compiler to ensure that the resource is compiled
|
||||
# For some very strange reason, `Code.ensure_compiled/1` isn't enough
|
||||
resource.ash_dsl_config()
|
||||
rescue
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
|
||||
case Code.ensure_compiled(resource) do
|
||||
{:module, _module} ->
|
||||
false
|
||||
|
||||
{:error, error} ->
|
||||
# The module is being compiled but is in a deadlock that may or may not be resolved
|
||||
{resource, error}
|
||||
end
|
||||
|> Enum.each(fn resource ->
|
||||
resource.ash_dsl_config()
|
||||
end)
|
||||
|> Enum.filter(& &1)
|
||||
|> case do
|
||||
[] ->
|
||||
{:ok, dsl}
|
||||
|
||||
rejected ->
|
||||
if times == 0 do
|
||||
for {resource, error} <- rejected do
|
||||
Logger.error(
|
||||
"Could not ensure that #{inspect(resource)} was compiled: #{inspect(error)}"
|
||||
)
|
||||
end
|
||||
|
||||
:halt
|
||||
else
|
||||
transform(module, dsl, times - 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,6 +6,14 @@ defmodule Ash.Api.Transformers.ValidateManyToManyJoinAttributes do
|
|||
|
||||
alias Ash.Dsl.Transformer
|
||||
|
||||
@impl true
|
||||
def after_compile?, do: true
|
||||
|
||||
@impl true
|
||||
def after?(Ash.Api.Transformers.EnsureResourcesCompiled), do: true
|
||||
def after?(_), do: false
|
||||
|
||||
@impl true
|
||||
def transform(_api, dsl) do
|
||||
dsl
|
||||
|> Transformer.get_entities([:resources])
|
||||
|
@ -36,7 +44,4 @@ defmodule Ash.Api.Transformers.ValidateManyToManyJoinAttributes do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def after?(Ash.Api.Transformers.EnsureResourcesCompiled), do: true
|
||||
def after?(_), do: false
|
||||
end
|
||||
|
|
|
@ -6,6 +6,9 @@ defmodule Ash.Api.Transformers.ValidateRelatedResourceInclusion do
|
|||
|
||||
alias Ash.Dsl.Transformer
|
||||
|
||||
@impl true
|
||||
def after_compile?, do: true
|
||||
|
||||
@impl true
|
||||
def after?(Ash.Api.Transformers.EnsureResourcesCompiled), do: true
|
||||
def after?(_), do: false
|
||||
|
@ -15,7 +18,6 @@ defmodule Ash.Api.Transformers.ValidateRelatedResourceInclusion do
|
|||
resources =
|
||||
dsl
|
||||
|> Transformer.get_entities([:resources])
|
||||
|> Enum.filter(& &1.warn_on_compile_failure?)
|
||||
|> Enum.map(& &1.resource)
|
||||
|
||||
resources
|
||||
|
|
|
@ -6,10 +6,17 @@ defmodule Ash.Api.Transformers.ValidateRelationshipAttributes do
|
|||
|
||||
alias Ash.Dsl.Transformer
|
||||
|
||||
@impl true
|
||||
def after_compile?, do: true
|
||||
|
||||
@impl true
|
||||
def after?(Ash.Api.Transformers.EnsureResourcesCompiled), do: true
|
||||
def after?(_), do: false
|
||||
|
||||
@impl true
|
||||
def transform(_api, dsl) do
|
||||
dsl
|
||||
|> Transformer.get_entities([:resources])
|
||||
|> Enum.filter(& &1.warn_on_compile_failure?)
|
||||
|> Enum.map(& &1.resource)
|
||||
|> Enum.each(fn resource ->
|
||||
attribute_names =
|
||||
|
@ -71,7 +78,4 @@ defmodule Ash.Api.Transformers.ValidateRelationshipAttributes do
|
|||
"Relationship `#{relationship.name}` expects destination field `#{relationship.destination_field}` to be defined on #{inspect(relationship.destination)}"
|
||||
end
|
||||
end
|
||||
|
||||
def after?(Ash.Api.Transformers.EnsureResourcesCompiled), do: true
|
||||
def after?(_), do: false
|
||||
end
|
||||
|
|
359
lib/ash/code_interface.ex
Normal file
359
lib/ash/code_interface.ex
Normal file
|
@ -0,0 +1,359 @@
|
|||
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 Ash.Error.Dsl.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
|
||||
|
||||
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)
|
||||
|
||||
args = interface.args || []
|
||||
arg_vars = Enum.map(args, &{&1, [], Elixir})
|
||||
|
||||
doc = """
|
||||
#{action.description || "Calls the #{action.name} action on the #{inspect(resource)} resource."}
|
||||
|
||||
## Options
|
||||
|
||||
#{Ash.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),
|
||||
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 =
|
||||
opts[:query]
|
||||
|> Kernel.||(unquote(resource))
|
||||
|> Ash.Query.for_read(
|
||||
unquote(action.name),
|
||||
input,
|
||||
Keyword.take(opts, [:actor, :tenant])
|
||||
)
|
||||
|
||||
if unquote(interface.get?) do
|
||||
query
|
||||
|> unquote(api).read_one(Keyword.drop(opts, [:query, :tenant]))
|
||||
|> case do
|
||||
{:ok, nil} ->
|
||||
{:error, Ash.Error.Query.NotFound.exception(resource: query.resource)}
|
||||
|
||||
{:ok, result} ->
|
||||
{:ok, result}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
else
|
||||
unquote(api).read(query, Keyword.drop(opts, [:query, :tenant]))
|
||||
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),
|
||||
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 =
|
||||
opts[:query]
|
||||
|> Kernel.||(unquote(resource))
|
||||
|> Ash.Query.for_read(
|
||||
unquote(action.name),
|
||||
input,
|
||||
Keyword.take(opts, [:actor, :tenant])
|
||||
)
|
||||
|
||||
if unquote(interface.get?) do
|
||||
query
|
||||
|> unquote(api).read_one!(Keyword.drop(opts, [:query, :tenant]))
|
||||
|> case do
|
||||
nil ->
|
||||
raise Ash.Error.Query.NotFound, resource: query.resource
|
||||
|
||||
result ->
|
||||
result
|
||||
end
|
||||
else
|
||||
unquote(api).read!(query, Keyword.drop(opts, [:query, :tenant]))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
:create ->
|
||||
@doc doc
|
||||
@dialyzer {:nowarn_function, {interface.name, Enum.count(args) + 2}}
|
||||
def unquote(interface.name)(
|
||||
unquote_splicing(arg_vars),
|
||||
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])
|
||||
)
|
||||
|
||||
unquote(api).create(changeset, Keyword.drop(opts, [:actor, :changeset, :tenant]))
|
||||
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),
|
||||
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])
|
||||
)
|
||||
|
||||
unquote(api).create!(changeset, Keyword.drop(opts, [:actor, :changeset]))
|
||||
end
|
||||
end
|
||||
|
||||
:update ->
|
||||
@doc doc
|
||||
@dialyzer {:nowarn_function, {interface.name, Enum.count(args) + 3}}
|
||||
def unquote(interface.name)(
|
||||
record,
|
||||
unquote_splicing(arg_vars),
|
||||
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])
|
||||
)
|
||||
|
||||
unquote(api).update(changeset, Keyword.drop(opts, [:actor, :tenant]))
|
||||
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),
|
||||
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])
|
||||
)
|
||||
|
||||
unquote(api).update!(changeset, Keyword.drop(opts, [:actor, :tenant]))
|
||||
end
|
||||
end
|
||||
|
||||
:destroy ->
|
||||
@doc doc
|
||||
@dialyzer {:nowarn_function, {interface.name, Enum.count(args) + 3}}
|
||||
def unquote(interface.name)(
|
||||
record,
|
||||
unquote_splicing(arg_vars),
|
||||
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])
|
||||
)
|
||||
|
||||
unquote(api).destroy(changeset, Keyword.drop(opts, [:actor, :tenant]))
|
||||
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),
|
||||
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])
|
||||
)
|
||||
|
||||
unquote(api).destroy!(changeset, Keyword.drop(opts, [:actor, :tenant]))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -149,9 +149,16 @@ defmodule Ash.Dsl do
|
|||
|
||||
@opts opts
|
||||
@before_compile Ash.Dsl
|
||||
@after_compile __MODULE__
|
||||
@ash_is parent
|
||||
@ash_parent parent
|
||||
|
||||
defmacro __after_compile__(_, _) do
|
||||
quote do
|
||||
Ash.Dsl.Extension.run_after_compile()
|
||||
end
|
||||
end
|
||||
|
||||
Module.register_attribute(__MODULE__, :persist, accumulate: true)
|
||||
|
||||
opts
|
||||
|
@ -222,16 +229,11 @@ defmodule Ash.Dsl do
|
|||
|
||||
Module.register_attribute(__MODULE__, :ash_is, persist: true)
|
||||
Module.put_attribute(__MODULE__, :ash_is, @ash_is)
|
||||
@on_load :on_load
|
||||
|
||||
ash_dsl_config = Macro.escape(Ash.Dsl.Extension.set_state(@persist))
|
||||
Ash.Dsl.Extension.set_state(@persist)
|
||||
|
||||
def ash_dsl_config do
|
||||
unquote(ash_dsl_config)
|
||||
end
|
||||
|
||||
def on_load do
|
||||
Ash.Dsl.Extension.load()
|
||||
@ash_dsl_config
|
||||
end
|
||||
|
||||
@opts
|
||||
|
|
|
@ -105,16 +105,35 @@ defmodule Ash.Dsl.Extension do
|
|||
@callback sections() :: [Ash.Dsl.section()]
|
||||
@callback transformers() :: [module]
|
||||
|
||||
defp dsl!(resource) do
|
||||
resource.ash_dsl_config()
|
||||
rescue
|
||||
UndefinedFunctionError ->
|
||||
try do
|
||||
Module.get_attribute(resource, :ash_dsl_config) || %{}
|
||||
rescue
|
||||
ArgumentError ->
|
||||
try do
|
||||
resource.ash_dsl_config()
|
||||
rescue
|
||||
_ ->
|
||||
reraise ArgumentError,
|
||||
"""
|
||||
No such entity #{inspect(resource)} found.
|
||||
""",
|
||||
__STACKTRACE__
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Get the entities configured for a given section"
|
||||
def get_entities(resource, path) do
|
||||
Ash.Helpers.try_compile(resource)
|
||||
:persistent_term.get({resource, :ash, path}, %{entities: []}).entities
|
||||
dsl!(resource)[path][:entities] || []
|
||||
end
|
||||
|
||||
@doc "Get a value that was persisted while transforming or compiling the resource, e.g `:primary_key`"
|
||||
def get_persisted(resource, key, default \\ nil) do
|
||||
Ash.Helpers.try_compile(resource)
|
||||
:persistent_term.get({resource, key}, default)
|
||||
Map.get(dsl!(resource)[:persist] || %{}, key, default)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -131,22 +150,10 @@ defmodule Ash.Dsl.Extension do
|
|||
value
|
||||
|
||||
_ ->
|
||||
Ash.Helpers.try_compile(resource)
|
||||
|
||||
Keyword.get(
|
||||
:persistent_term.get({resource, :ash, path}, %{opts: []}).opts,
|
||||
value,
|
||||
default
|
||||
)
|
||||
Keyword.get(dsl!(resource)[path][:opts] || [], value, default)
|
||||
end
|
||||
else
|
||||
Ash.Helpers.try_compile(resource)
|
||||
|
||||
Keyword.get(
|
||||
:persistent_term.get({resource, :ash, path}, %{opts: []}).opts,
|
||||
value,
|
||||
default
|
||||
)
|
||||
Keyword.get(dsl!(resource)[path][:opts] || [], value, default)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -404,19 +411,6 @@ defmodule Ash.Dsl.Extension do
|
|||
body =
|
||||
quote location: :keep do
|
||||
@extensions unquote(extensions)
|
||||
# Due to a few strange stateful bugs I've seen,
|
||||
# we clear the process of any potentially related state
|
||||
for {key, _value} <- Process.get() do
|
||||
if is_tuple(key) and elem(key, 0) == __MODULE__ do
|
||||
Process.delete(key)
|
||||
end
|
||||
end
|
||||
|
||||
for {key, _value} <- :persistent_term.get() do
|
||||
if is_tuple(key) and elem(key, 0) == __MODULE__ do
|
||||
:persistent_term.erase(key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
imports =
|
||||
|
@ -433,7 +427,7 @@ defmodule Ash.Dsl.Extension do
|
|||
end
|
||||
|
||||
@doc false
|
||||
defmacro set_state(additional_persisted_data \\ []) do
|
||||
defmacro set_state(additional_persisted_data) do
|
||||
quote generated: true,
|
||||
location: :keep,
|
||||
bind_quoted: [additional_persisted_data: additional_persisted_data] do
|
||||
|
@ -461,7 +455,7 @@ defmodule Ash.Dsl.Extension do
|
|||
&Map.merge(&1, persist)
|
||||
)
|
||||
|
||||
Ash.Dsl.Extension.write_dsl_to_persistent_term(__MODULE__, ash_dsl_config)
|
||||
@ash_dsl_config ash_dsl_config
|
||||
|
||||
for {key, _value} <- Process.get() do
|
||||
if is_tuple(key) and elem(key, 0) == __MODULE__ do
|
||||
|
@ -473,27 +467,35 @@ defmodule Ash.Dsl.Extension do
|
|||
@extensions
|
||||
|> Enum.flat_map(& &1.transformers())
|
||||
|> Transformer.sort()
|
||||
|> Enum.reject(& &1.after_compile?())
|
||||
|
||||
__MODULE__
|
||||
|> Ash.Dsl.Extension.run_transformers(
|
||||
transformers_to_run,
|
||||
ash_dsl_config
|
||||
ash_dsl_config,
|
||||
true
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defmacro load do
|
||||
defmacro run_after_compile do
|
||||
quote do
|
||||
Ash.Dsl.Extension.write_dsl_to_persistent_term(
|
||||
__MODULE__,
|
||||
ash_dsl_config()
|
||||
)
|
||||
transformers_to_run =
|
||||
@extensions
|
||||
|> Enum.flat_map(& &1.transformers())
|
||||
|> Ash.Dsl.Transformer.sort()
|
||||
|> Enum.filter(& &1.after_compile?())
|
||||
|
||||
:ok
|
||||
__MODULE__
|
||||
|> Ash.Dsl.Extension.run_transformers(
|
||||
transformers_to_run,
|
||||
Module.get_attribute(__MODULE__, :ash_dsl_config),
|
||||
false
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def run_transformers(mod, transformers, ash_dsl_config) do
|
||||
def run_transformers(mod, transformers, ash_dsl_config, store?) do
|
||||
Enum.reduce_while(transformers, ash_dsl_config, fn transformer, dsl ->
|
||||
result =
|
||||
try do
|
||||
|
@ -509,11 +511,17 @@ defmodule Ash.Dsl.Extension do
|
|||
end
|
||||
|
||||
case result do
|
||||
:ok ->
|
||||
{:cont, dsl}
|
||||
|
||||
:halt ->
|
||||
{:halt, dsl}
|
||||
|
||||
{:ok, new_dsl} ->
|
||||
write_dsl_to_persistent_term(mod, new_dsl)
|
||||
if store? do
|
||||
Module.put_attribute(mod, :ash_dsl_config, new_dsl)
|
||||
end
|
||||
|
||||
{:cont, new_dsl}
|
||||
|
||||
{:error, error} ->
|
||||
|
@ -522,21 +530,6 @@ defmodule Ash.Dsl.Extension do
|
|||
end)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def write_dsl_to_persistent_term(mod, dsl) do
|
||||
dsl
|
||||
|> Map.delete(:persist)
|
||||
|> Enum.each(fn {section_path, value} ->
|
||||
:persistent_term.put({mod, :ash, section_path}, value)
|
||||
end)
|
||||
|
||||
Enum.each(Map.get(dsl, :persist, %{}), fn {key, value} ->
|
||||
:persistent_term.put({mod, key}, value)
|
||||
end)
|
||||
|
||||
dsl
|
||||
end
|
||||
|
||||
defp raise_transformer_error(transformer, error) do
|
||||
if Exception.exception?(error) do
|
||||
raise error
|
||||
|
|
|
@ -9,15 +9,15 @@ defmodule Ash.Dsl.Transformer do
|
|||
Use the `after?/1` and `before?/1` callbacks to ensure that your transformer
|
||||
runs either before or after some other transformer.
|
||||
|
||||
The pattern for requesting information from other modules that use the DSL and are
|
||||
also currently compiling has not yet been determined. If you have that requirement
|
||||
you will need extra utilities to ensure that some other DSL based module has either
|
||||
completed or reached a certain point in its transformers. These utilities have not
|
||||
yet been written.
|
||||
Return `true` in `after_compile/0` to have the transformer run in an `after_compile` hook,
|
||||
but keep in mind that no modifications to the dsl structure will be retained, so there is no
|
||||
point in returning a new dsl structure from `transform/2` if `after_compile/0` is defined. Instead,
|
||||
simply return `:ok` or `{:error, error}`
|
||||
"""
|
||||
@callback transform(module, map) :: {:ok, map} | {:error, term} | :halt
|
||||
@callback transform(module, map) :: :ok | {:ok, map} | {:error, term} | :halt
|
||||
@callback before?(module) :: boolean
|
||||
@callback after?(module) :: boolean
|
||||
@callback after_compile?() :: boolean
|
||||
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
|
@ -25,8 +25,9 @@ defmodule Ash.Dsl.Transformer do
|
|||
|
||||
def before?(_), do: false
|
||||
def after?(_), do: false
|
||||
def after_compile?, do: false
|
||||
|
||||
defoverridable before?: 1, after?: 1
|
||||
defoverridable before?: 1, after?: 1, after_compile?: 0
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -564,7 +564,7 @@ defmodule Ash.EmbeddableType do
|
|||
@parent parent
|
||||
|
||||
resources do
|
||||
resource @parent, warn_on_compile_failure?: false
|
||||
resource @parent, []
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1843,6 +1843,7 @@ defmodule Ash.Filter do
|
|||
defp validate_not_crossing_datalayer_boundaries(refs, resource, expr) do
|
||||
refs
|
||||
|> Enum.map(&Ash.Resource.Info.related(resource, &1.relationship_path))
|
||||
|> Enum.filter(& &1)
|
||||
|> Enum.group_by(&Ash.DataLayer.data_layer/1)
|
||||
|> Map.to_list()
|
||||
|> case do
|
||||
|
|
|
@ -3,7 +3,16 @@ defmodule Ash.Helpers do
|
|||
|
||||
@spec try_compile(term) :: :ok
|
||||
def try_compile(module) when is_atom(module) do
|
||||
Code.ensure_loaded(module)
|
||||
try do
|
||||
# This is to get the compiler to ensure that the resource is compiled
|
||||
# For some very strange reason, `Code.ensure_compiled/1` isn't enough
|
||||
module.ash_dsl_config()
|
||||
rescue
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
|
||||
Code.ensure_compiled!(module)
|
||||
:ok
|
||||
end
|
||||
|
||||
|
|
|
@ -56,8 +56,11 @@ defmodule Ash.OptionsHelpers do
|
|||
{:list, values} ->
|
||||
{:list, sanitize_type(values)}
|
||||
|
||||
{:ash_behaviour, behaviour, builtins} ->
|
||||
{:custom, __MODULE__, :ash_behaviour, [behaviour, builtins]}
|
||||
{:ash_behaviour, behaviour, _builtins} ->
|
||||
{:custom, __MODULE__, :ash_behaviour, [behaviour]}
|
||||
|
||||
{:ash_behaviour, behaviour} ->
|
||||
{:custom, __MODULE__, :ash_behaviour, [behaviour]}
|
||||
|
||||
:ash_resource ->
|
||||
:atom
|
||||
|
@ -72,7 +75,7 @@ defmodule Ash.OptionsHelpers do
|
|||
end
|
||||
end
|
||||
|
||||
def ash_behaviour({module, opts}, _behaviour, _builtins) when is_atom(module) do
|
||||
def ash_behaviour({module, opts}, _behaviour) when is_atom(module) do
|
||||
if Keyword.keyword?(opts) do
|
||||
# We can't check if it implements the behaviour here, unfortunately
|
||||
# As it may not be immediately available
|
||||
|
@ -82,11 +85,11 @@ defmodule Ash.OptionsHelpers do
|
|||
end
|
||||
end
|
||||
|
||||
def ash_behaviour(module, behaviour, builtins) when is_atom(module) do
|
||||
ash_behaviour({module, []}, behaviour, builtins)
|
||||
def ash_behaviour(module, behaviour) when is_atom(module) do
|
||||
ash_behaviour({module, []}, behaviour)
|
||||
end
|
||||
|
||||
def ash_behaviour(other, _, _) do
|
||||
def ash_behaviour(other, _) do
|
||||
{:error, "Expected a module and opts, got: #{inspect(other)}"}
|
||||
end
|
||||
|
||||
|
|
56
lib/ash/registry/dsl.ex
Normal file
56
lib/ash/registry/dsl.ex
Normal file
|
@ -0,0 +1,56 @@
|
|||
# defmodule Ash.Registry.Dsl do
|
||||
# @entry %Ash.Dsl.Entity{
|
||||
# name: :entry,
|
||||
# describe: "A reference to a a module",
|
||||
# target: Ash.Registry.EntryReference,
|
||||
# args: [:entry],
|
||||
# examples: [
|
||||
# "entry MyApp.Post"
|
||||
# ],
|
||||
# schema: [
|
||||
# entry: [
|
||||
# type: :atom,
|
||||
# required: true,
|
||||
# doc: "The module of the entry"
|
||||
# ]
|
||||
# ]
|
||||
# }
|
||||
|
||||
# @entries %Ash.Dsl.Section{
|
||||
# name: :entries,
|
||||
# describe: "List the entries present in this registry",
|
||||
# examples: [
|
||||
# """
|
||||
# entries do
|
||||
# entry MyApp.User
|
||||
# entry MyApp.Post
|
||||
# entry MyApp.Comment
|
||||
# end
|
||||
# """
|
||||
# ],
|
||||
# entities: [
|
||||
# @entry
|
||||
# ]
|
||||
# }
|
||||
|
||||
# @sections [@resources]
|
||||
|
||||
# @moduledoc """
|
||||
# A small DSL for declaring APIs
|
||||
|
||||
# Apis are the entrypoints for working with your resources.
|
||||
|
||||
# Apis may optionally include a list of resources, in which case they can be
|
||||
# used as an `Ash.Registry` in various places. This is for backwards compatibility,
|
||||
# but if at all possible you should define an `Ash.Registry` if you are using an extension
|
||||
# that requires a list of resources. For example, most extensions look for two application
|
||||
# environment variables called `:ash_apis` and `:ash_registries` to find any potential registries
|
||||
|
||||
# # Table of Contents
|
||||
# #{Ash.Dsl.Extension.doc_index(@sections)}
|
||||
|
||||
# #{Ash.Dsl.Extension.doc(@sections)}
|
||||
# """
|
||||
|
||||
# use Ash.Dsl.Extension, sections: @sections, transformers: @transformers
|
||||
# end
|
|
@ -315,6 +315,7 @@ defmodule Ash.Resource.Dsl do
|
|||
target: Ash.Resource.Change,
|
||||
transform: {Ash.Resource.Change, :transform, []},
|
||||
schema: Ash.Resource.Change.action_schema(),
|
||||
modules: [:change],
|
||||
args: [:change]
|
||||
}
|
||||
|
||||
|
@ -378,6 +379,7 @@ defmodule Ash.Resource.Dsl do
|
|||
target: Ash.Resource.Change,
|
||||
transform: {Ash.Resource.Change, :transform, []},
|
||||
schema: Ash.Resource.Change.schema(),
|
||||
modules: [:change],
|
||||
args: [:change]
|
||||
}
|
||||
|
||||
|
@ -735,11 +737,20 @@ defmodule Ash.Resource.Dsl do
|
|||
examples: [
|
||||
"""
|
||||
code_interface do
|
||||
auto_define MyApp.Api
|
||||
define :create_user, action: :create
|
||||
define :get_user_by_id, action: :get_by_id, args: [:id], get?: true
|
||||
end
|
||||
"""
|
||||
],
|
||||
schema: [
|
||||
define_for: [
|
||||
type: {:ash_behaviour, Ash.Api},
|
||||
doc:
|
||||
"Defines the code interface on the resource module directly, using the provided Api.",
|
||||
default: false
|
||||
]
|
||||
],
|
||||
entities: [
|
||||
@define
|
||||
]
|
||||
|
|
|
@ -88,6 +88,16 @@ defmodule Ash.Resource.Info do
|
|||
Extension.get_entities(resource, [:code_interface])
|
||||
end
|
||||
|
||||
@spec define_interface_in_resource?(Ash.Resource.t()) :: boolean
|
||||
def define_interface_in_resource?(resource) do
|
||||
!!Extension.get_opt(resource, [:code_interface], :define_for, false)
|
||||
end
|
||||
|
||||
@spec define_interface_for(Ash.Resource.t()) :: atom | nil
|
||||
def define_interface_for(resource) do
|
||||
Extension.get_opt(resource, [:code_interface], :define_for, nil)
|
||||
end
|
||||
|
||||
@spec extensions(Ash.Resource.t()) :: [module]
|
||||
def extensions(resource) do
|
||||
Extension.get_persisted(resource, :extensions, [])
|
||||
|
|
|
@ -6,6 +6,19 @@ defmodule Ash.Resource.Interface do
|
|||
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
defmacro __using__(_) do
|
||||
quote bind_quoted: [], generated: true do
|
||||
if Ash.Resource.Info.define_interface_in_resource?(__MODULE__) do
|
||||
require Ash.CodeInterface
|
||||
|
||||
Ash.CodeInterface.define_interface(
|
||||
Ash.Resource.Info.define_interface_for(__MODULE__),
|
||||
__MODULE__
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def interface_options(action_type) do
|
||||
[
|
||||
tenant: [
|
||||
|
|
|
@ -243,16 +243,11 @@ defmodule Ash.Test.Changeset.ChangesetTest do
|
|||
|> Changeset.new()
|
||||
end
|
||||
|
||||
test "it returns an error for a non-resource record" do
|
||||
assert %Changeset{
|
||||
action_type: :update,
|
||||
attributes: %{},
|
||||
data: %NonResource{},
|
||||
errors: [%Ash.Error.Invalid.NoSuchResource{}],
|
||||
valid?: false
|
||||
} =
|
||||
%NonResource{name: "foo"}
|
||||
|> Changeset.new()
|
||||
test "it raises an error for a non-resource record" do
|
||||
assert_raise ArgumentError, ~r/No such entity/, fn ->
|
||||
%NonResource{name: "foo"}
|
||||
|> Changeset.new()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -39,6 +39,8 @@ defmodule Ash.Test.CodeInterfaceTest do
|
|||
use Ash.Api
|
||||
|
||||
resources do
|
||||
define_interfaces?(true)
|
||||
|
||||
resource(User)
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue