mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 13:33:20 +12:00
chore: generalize DSL tooling
This commit is contained in:
parent
36749669ca
commit
13ca0b2976
5 changed files with 280 additions and 135 deletions
|
@ -43,6 +43,9 @@ defmodule Ash do
|
||||||
_ -> []
|
_ -> []
|
||||||
end)
|
end)
|
||||||
|> Enum.any?(&(&1 == behaviour))
|
|> Enum.any?(&(&1 == behaviour))
|
||||||
|
rescue
|
||||||
|
_ ->
|
||||||
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
def uuid do
|
def uuid do
|
||||||
|
|
|
@ -365,41 +365,16 @@ defmodule Ash.Api do
|
||||||
|
|
||||||
alias Ash.Dsl.Extension
|
alias Ash.Dsl.Extension
|
||||||
|
|
||||||
defmacro __using__(opts) do
|
use Ash.Dsl, default_extensions: [extensions: [Ash.Api.Dsl]]
|
||||||
extensions = [Ash.Api.Dsl | opts[:extensions] || []]
|
|
||||||
|
|
||||||
body =
|
def handle_opts(_) do
|
||||||
quote do
|
quote do
|
||||||
@before_compile Ash.Api
|
|
||||||
@behaviour Ash.Api
|
@behaviour Ash.Api
|
||||||
end
|
end
|
||||||
|
|
||||||
preparations = Extension.prepare(extensions)
|
|
||||||
|
|
||||||
[body | preparations]
|
|
||||||
end
|
|
||||||
|
|
||||||
defmacro __before_compile__(_env) do
|
|
||||||
quote generated: true, unquote: false do
|
|
||||||
alias Ash.Dsl.Extension
|
|
||||||
|
|
||||||
@on_load :on_load
|
|
||||||
|
|
||||||
ash_dsl_config = Macro.escape(Extension.set_state())
|
|
||||||
|
|
||||||
@doc false
|
|
||||||
def ash_dsl_config do
|
|
||||||
unquote(ash_dsl_config)
|
|
||||||
end
|
|
||||||
|
|
||||||
def on_load do
|
|
||||||
Extension.load()
|
|
||||||
|
|
||||||
__MODULE__
|
|
||||||
|> Ash.Api.resources()
|
|
||||||
|> Enum.each(&Code.ensure_loaded/1)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_before_compile(_) do
|
||||||
|
quote do
|
||||||
use Ash.Api.Interface
|
use Ash.Api.Interface
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
233
lib/ash/dsl/dsl.ex
Normal file
233
lib/ash/dsl/dsl.ex
Normal file
|
@ -0,0 +1,233 @@
|
||||||
|
defmodule Ash.Dsl do
|
||||||
|
@using_schema [
|
||||||
|
single_extension_kinds: [
|
||||||
|
type: {:list, :atom},
|
||||||
|
default: [],
|
||||||
|
doc:
|
||||||
|
"The extension kinds that are allowed to have a single value. For example: `[:data_layer]`"
|
||||||
|
],
|
||||||
|
many_extension_kinds: [
|
||||||
|
type: {:list, :atom},
|
||||||
|
default: [],
|
||||||
|
doc:
|
||||||
|
"The extension kinds that can have multiple values. e.g `[notifiers: [Notifier1, Notifier2]]`"
|
||||||
|
],
|
||||||
|
untyped_extensions?: [
|
||||||
|
type: :boolean,
|
||||||
|
default: true,
|
||||||
|
doc: "Whether or not to support an `extensions` key which contains untyped extensions"
|
||||||
|
],
|
||||||
|
default_extensions: [
|
||||||
|
type: :keyword_list,
|
||||||
|
default: [],
|
||||||
|
doc: """
|
||||||
|
The extensions that are included by default. e.g `[data_layer: Default, notifiers: [Notifier1]]`
|
||||||
|
Default values for single extension kinds are overwritten if specified by the implementor, while many extension
|
||||||
|
kinds are appended to if specified by the implementor.
|
||||||
|
"""
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
@moduledoc """
|
||||||
|
The primary entry point for adding a DSL to a module.
|
||||||
|
|
||||||
|
To add a DSL to a module, add `use Ash.Dsl, ...options`. The options supported with `use Ash.Dsl` are:
|
||||||
|
|
||||||
|
#{Ash.OptionsHelpers.docs(@using_schema)}
|
||||||
|
|
||||||
|
See the callbacks defined in this module to augment the behavior/compilation of the module getting a Dsl.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@type opts :: Keyword.t()
|
||||||
|
@doc """
|
||||||
|
Validate/add options. Those options will be passed to `handle_opts` and `handle_before_compile`
|
||||||
|
"""
|
||||||
|
@callback init(opts) :: {:ok, opts} | {:error, String.t() | Ash.error()}
|
||||||
|
@doc """
|
||||||
|
Handle options in the context of the module. Must return a `quote` block.
|
||||||
|
|
||||||
|
If you want to persist anything in the DSL persistence layer,
|
||||||
|
use `@persist {:key, value}`. It can be called multiple times to
|
||||||
|
persist multiple times.
|
||||||
|
"""
|
||||||
|
@callback handle_opts(Keyword.t()) :: Macro.t()
|
||||||
|
@doc """
|
||||||
|
Handle options in the context of the module, after all extensions have been processed. Must return a `quote` block.
|
||||||
|
"""
|
||||||
|
@callback handle_before_compile(Keyword.t()) :: Macro.t()
|
||||||
|
|
||||||
|
defmacro __using__(opts) do
|
||||||
|
opts = Ash.OptionsHelpers.validate!(opts, @using_schema)
|
||||||
|
|
||||||
|
their_opt_schema =
|
||||||
|
Enum.map(opts[:single_extension_kinds], fn extension_kind ->
|
||||||
|
{extension_kind, type: :atom, default: opts[:default_extensions][extension_kind]}
|
||||||
|
end) ++
|
||||||
|
Enum.map(opts[:many_extension_kinds], fn extension_kind ->
|
||||||
|
{extension_kind, type: {:list, :atom}, default: []}
|
||||||
|
end)
|
||||||
|
|
||||||
|
their_opt_schema =
|
||||||
|
if opts[:untyped_extensions?] do
|
||||||
|
Keyword.put(their_opt_schema, :extensions, type: {:list, :atom})
|
||||||
|
else
|
||||||
|
their_opt_schema
|
||||||
|
end
|
||||||
|
|
||||||
|
quote bind_quoted: [
|
||||||
|
their_opt_schema: their_opt_schema,
|
||||||
|
parent_opts: opts,
|
||||||
|
parent: __CALLER__.module
|
||||||
|
] do
|
||||||
|
@dialyzer {:nowarn_function, handle_opts: 1, handle_before_compile: 1}
|
||||||
|
|
||||||
|
def init(opts), do: {:ok, opts}
|
||||||
|
|
||||||
|
def handle_opts(opts) do
|
||||||
|
quote do
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_before_compile(opts) do
|
||||||
|
quote do
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defoverridable init: 1, handle_opts: 1, handle_before_compile: 1
|
||||||
|
|
||||||
|
defmacro __using__(opts) do
|
||||||
|
parent = unquote(parent)
|
||||||
|
parent_opts = unquote(parent_opts)
|
||||||
|
their_opt_schema = unquote(their_opt_schema)
|
||||||
|
|
||||||
|
{opts, extensions} =
|
||||||
|
parent_opts[:default_extensions]
|
||||||
|
|> Enum.reduce(opts, fn {key, defaults}, opts ->
|
||||||
|
Keyword.update(opts, key, defaults, fn current_value ->
|
||||||
|
cond do
|
||||||
|
key in parent_opts[:single_extension_kinds] ->
|
||||||
|
current_value || defaults
|
||||||
|
|
||||||
|
key in parent_opts[:many_extension_kinds] || key == :extensions ->
|
||||||
|
List.wrap(current_value) ++ List.wrap(defaults)
|
||||||
|
|
||||||
|
true ->
|
||||||
|
opts
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|> Ash.Dsl.expand_modules(parent_opts, __CALLER__)
|
||||||
|
|
||||||
|
opts =
|
||||||
|
opts
|
||||||
|
|> Ash.OptionsHelpers.validate!(their_opt_schema)
|
||||||
|
|> init()
|
||||||
|
|> Ash.Dsl.unwrap()
|
||||||
|
|
||||||
|
body =
|
||||||
|
quote do
|
||||||
|
parent = unquote(parent)
|
||||||
|
opts = unquote(opts)
|
||||||
|
parent_opts = unquote(parent_opts)
|
||||||
|
their_opt_schema = unquote(their_opt_schema)
|
||||||
|
extensions = unquote(extensions)
|
||||||
|
|
||||||
|
@opts opts
|
||||||
|
@before_compile Ash.Dsl
|
||||||
|
@ash_is parent
|
||||||
|
@ash_parent parent
|
||||||
|
|
||||||
|
Module.register_attribute(__MODULE__, :persist, accumulate: true)
|
||||||
|
|
||||||
|
opts
|
||||||
|
|> @ash_parent.handle_opts()
|
||||||
|
|> Code.eval_quoted([], __ENV__)
|
||||||
|
|
||||||
|
for single_extension_kind <- parent_opts[:single_extension_kinds] do
|
||||||
|
@persist {single_extension_kind, opts[single_extension_kind]}
|
||||||
|
Module.put_attribute(__MODULE__, single_extension_kind, opts[single_extension_kind])
|
||||||
|
end
|
||||||
|
|
||||||
|
for many_extension_kind <- parent_opts[:many_extension_kinds] do
|
||||||
|
@persist {many_extension_kind, opts[many_extension_kind] || []}
|
||||||
|
Module.put_attribute(
|
||||||
|
__MODULE__,
|
||||||
|
many_extension_kind,
|
||||||
|
opts[many_extension_kind] || []
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
preparations = Ash.Dsl.Extension.prepare(extensions)
|
||||||
|
[body | preparations]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def unwrap({:ok, value}), do: value
|
||||||
|
def unwrap({:error, error}), do: raise(error)
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def expand_modules(opts, their_opt_schema, env) do
|
||||||
|
Enum.reduce(opts, {[], []}, fn {key, value}, {opts, extensions} ->
|
||||||
|
cond do
|
||||||
|
key in their_opt_schema[:single_extension_kinds] ->
|
||||||
|
mod = Macro.expand(value, env)
|
||||||
|
|
||||||
|
extensions =
|
||||||
|
if Ash.implements_behaviour?(mod, Ash.Dsl.Extension) do
|
||||||
|
[mod | extensions]
|
||||||
|
else
|
||||||
|
extensions
|
||||||
|
end
|
||||||
|
|
||||||
|
{Keyword.put(opts, key, mod), extensions}
|
||||||
|
|
||||||
|
key in their_opt_schema[:many_extension_kinds] || key == :extensions ->
|
||||||
|
mods = value |> List.wrap() |> Enum.map(&Macro.expand(&1, env))
|
||||||
|
|
||||||
|
extensions =
|
||||||
|
extensions ++ Enum.filter(mods, &Ash.implements_behaviour?(&1, Ash.Dsl.Extension))
|
||||||
|
|
||||||
|
{Keyword.put(opts, key, mods), extensions}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
{key, value}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
|
||||||
|
defmacro __before_compile__(_env) do
|
||||||
|
quote unquote: false do
|
||||||
|
@type t :: __MODULE__
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
def ash_dsl_config do
|
||||||
|
unquote(ash_dsl_config)
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_load do
|
||||||
|
Ash.Dsl.Extension.load()
|
||||||
|
end
|
||||||
|
|
||||||
|
@opts
|
||||||
|
|> @ash_parent.handle_before_compile()
|
||||||
|
|> Code.eval_quoted([], __ENV__)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def is?(module, type) do
|
||||||
|
Ash.try_compile(module)
|
||||||
|
|
||||||
|
type in List.wrap(module.module_info(:attributes)[:ash_is])
|
||||||
|
rescue
|
||||||
|
_ ->
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
|
@ -7,70 +7,44 @@ defmodule Ash.Resource do
|
||||||
|
|
||||||
alias Ash.Dsl.Extension
|
alias Ash.Dsl.Extension
|
||||||
|
|
||||||
defmacro __using__(opts) do
|
use Ash.Dsl,
|
||||||
data_layer = Macro.expand(opts[:data_layer], __CALLER__)
|
single_extension_kinds: [:data_layer],
|
||||||
embedded? = data_layer == :embedded
|
many_extension_kinds: [
|
||||||
|
:authorizers,
|
||||||
|
:notifiers
|
||||||
|
],
|
||||||
|
default_extensions: [
|
||||||
|
data_layer: Ash.DataLayer.Simple,
|
||||||
|
extensions: [Ash.Resource.Dsl]
|
||||||
|
]
|
||||||
|
|
||||||
data_layer =
|
def init(opts) do
|
||||||
if embedded? do
|
if opts[:data_layer] == :embedded do
|
||||||
Ash.DataLayer.Simple
|
{:ok,
|
||||||
|
opts
|
||||||
|
|> Keyword.put(:data_layer, Ash.DataLayer.Simple)
|
||||||
|
|> Keyword.put(:embedded?, true)}
|
||||||
else
|
else
|
||||||
data_layer || Ash.DataLayer.Simple
|
{:ok, opts}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
opts = Keyword.put(opts, :data_layer, data_layer)
|
def handle_opts(opts) do
|
||||||
|
quote bind_quoted: [embedded?: opts[:embedded?]] do
|
||||||
authorizers =
|
|
||||||
opts[:authorizers]
|
|
||||||
|> List.wrap()
|
|
||||||
|> Enum.map(&Macro.expand(&1, __CALLER__))
|
|
||||||
|
|
||||||
notifiers =
|
|
||||||
opts[:notifiers]
|
|
||||||
|> List.wrap()
|
|
||||||
|> Enum.map(&Macro.expand(&1, __CALLER__))
|
|
||||||
|
|
||||||
extensions =
|
|
||||||
if Ash.implements_behaviour?(data_layer, Ash.Dsl.Extension) do
|
|
||||||
[data_layer, Ash.Resource.Dsl]
|
|
||||||
else
|
|
||||||
[Ash.Resource.Dsl]
|
|
||||||
end
|
|
||||||
|
|
||||||
authorizer_extensions =
|
|
||||||
Enum.filter(authorizers, &Ash.implements_behaviour?(&1, Ash.Dsl.Extension))
|
|
||||||
|
|
||||||
notifier_extensions =
|
|
||||||
Enum.filter(notifiers, &Ash.implements_behaviour?(&1, Ash.Dsl.Extension))
|
|
||||||
|
|
||||||
extensions =
|
|
||||||
Enum.concat([
|
|
||||||
extensions,
|
|
||||||
opts[:extensions] || [],
|
|
||||||
authorizer_extensions,
|
|
||||||
notifier_extensions
|
|
||||||
])
|
|
||||||
|
|
||||||
body =
|
|
||||||
quote bind_quoted: [opts: opts, embedded?: embedded?] do
|
|
||||||
@before_compile Ash.Resource
|
|
||||||
|
|
||||||
@authorizers opts[:authorizers] || []
|
|
||||||
@notifiers opts[:notifiers] || []
|
|
||||||
@data_layer opts[:data_layer] || Ash.DataLayer.Simple
|
|
||||||
@extensions (opts[:extensions] || []) ++
|
|
||||||
List.wrap(opts[:data_layer] || Ash.DataLayer.Simple) ++
|
|
||||||
(opts[:authorizers] || [])
|
|
||||||
@embedded embedded?
|
|
||||||
|
|
||||||
if embedded? do
|
if embedded? do
|
||||||
|
@persist {:embedded?, true}
|
||||||
|
|
||||||
Ash.Resource.define_embeddable_type()
|
Ash.Resource.define_embeddable_type()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
preparations = Extension.prepare(extensions)
|
def handle_before_compile(_opts) do
|
||||||
|
quote do
|
||||||
|
require Ash.Schema
|
||||||
|
|
||||||
[body | preparations]
|
Ash.Schema.define_schema()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@embedded_resource_array_constraints [
|
@embedded_resource_array_constraints [
|
||||||
|
@ -560,44 +534,6 @@ defmodule Ash.Resource do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
|
|
||||||
defmacro __before_compile__(_env) do
|
|
||||||
quote unquote: false do
|
|
||||||
@doc false
|
|
||||||
alias Ash.Dsl.Extension
|
|
||||||
|
|
||||||
@type t :: %__MODULE__{}
|
|
||||||
|
|
||||||
Module.register_attribute(__MODULE__, :is_ash_resource, persist: true, accumulate: false)
|
|
||||||
@is_ash_resource true
|
|
||||||
|
|
||||||
@on_load :on_load
|
|
||||||
|
|
||||||
ash_dsl_config =
|
|
||||||
Macro.escape(
|
|
||||||
Extension.set_state(
|
|
||||||
notifiers: @notifiers,
|
|
||||||
authorizers: @authorizers,
|
|
||||||
data_layer: @data_layer,
|
|
||||||
embedded?: @embedded
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@doc false
|
|
||||||
def ash_dsl_config do
|
|
||||||
unquote(ash_dsl_config)
|
|
||||||
end
|
|
||||||
|
|
||||||
def on_load do
|
|
||||||
Extension.load()
|
|
||||||
end
|
|
||||||
|
|
||||||
require Ash.Schema
|
|
||||||
|
|
||||||
Ash.Schema.define_schema()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec extensions(Ash.resource()) :: [module]
|
@spec extensions(Ash.resource()) :: [module]
|
||||||
def extensions(resource) do
|
def extensions(resource) do
|
||||||
Extension.get_persisted(resource, :extensions)
|
Extension.get_persisted(resource, :extensions)
|
||||||
|
@ -653,12 +589,7 @@ defmodule Ash.Resource do
|
||||||
@doc "Whether or not a given module is a resource module"
|
@doc "Whether or not a given module is a resource module"
|
||||||
@spec resource?(module) :: boolean
|
@spec resource?(module) :: boolean
|
||||||
def resource?(module) when is_atom(module) do
|
def resource?(module) when is_atom(module) do
|
||||||
Ash.try_compile(module)
|
Ash.Dsl.is?(module, __MODULE__)
|
||||||
|
|
||||||
module.module_info(:attributes)[:is_ash_resource] == [true]
|
|
||||||
rescue
|
|
||||||
_ ->
|
|
||||||
false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def resource?(_), do: false
|
def resource?(_), do: false
|
||||||
|
|
3
mix.exs
3
mix.exs
|
@ -114,6 +114,9 @@ defmodule Ash.MixProject do
|
||||||
Ash.Dsl.Section,
|
Ash.Dsl.Section,
|
||||||
Ash.Dsl.Transformer
|
Ash.Dsl.Transformer
|
||||||
],
|
],
|
||||||
|
"dsl tooling": [
|
||||||
|
Ash.Dsl
|
||||||
|
],
|
||||||
"resource dsl transformers": ~r/Ash.Resource.Transformers/,
|
"resource dsl transformers": ~r/Ash.Resource.Transformers/,
|
||||||
"api dsl transformers": ~r/Ash.Api.Transformers/,
|
"api dsl transformers": ~r/Ash.Api.Transformers/,
|
||||||
"filter operators": ~r/Ash.Query.Operator/,
|
"filter operators": ~r/Ash.Query.Operator/,
|
||||||
|
|
Loading…
Reference in a new issue