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)
|
||||
|> Enum.any?(&(&1 == behaviour))
|
||||
rescue
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
|
||||
def uuid do
|
||||
|
|
|
@ -365,41 +365,16 @@ defmodule Ash.Api do
|
|||
|
||||
alias Ash.Dsl.Extension
|
||||
|
||||
defmacro __using__(opts) do
|
||||
extensions = [Ash.Api.Dsl | opts[:extensions] || []]
|
||||
use Ash.Dsl, default_extensions: [extensions: [Ash.Api.Dsl]]
|
||||
|
||||
body =
|
||||
def handle_opts(_) do
|
||||
quote do
|
||||
@before_compile Ash.Api
|
||||
@behaviour Ash.Api
|
||||
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
|
||||
|
||||
def handle_before_compile(_) do
|
||||
quote do
|
||||
use Ash.Api.Interface
|
||||
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
|
||||
|
||||
defmacro __using__(opts) do
|
||||
data_layer = Macro.expand(opts[:data_layer], __CALLER__)
|
||||
embedded? = data_layer == :embedded
|
||||
use Ash.Dsl,
|
||||
single_extension_kinds: [:data_layer],
|
||||
many_extension_kinds: [
|
||||
:authorizers,
|
||||
:notifiers
|
||||
],
|
||||
default_extensions: [
|
||||
data_layer: Ash.DataLayer.Simple,
|
||||
extensions: [Ash.Resource.Dsl]
|
||||
]
|
||||
|
||||
data_layer =
|
||||
if embedded? do
|
||||
Ash.DataLayer.Simple
|
||||
def init(opts) do
|
||||
if opts[:data_layer] == :embedded do
|
||||
{:ok,
|
||||
opts
|
||||
|> Keyword.put(:data_layer, Ash.DataLayer.Simple)
|
||||
|> Keyword.put(:embedded?, true)}
|
||||
else
|
||||
data_layer || Ash.DataLayer.Simple
|
||||
{:ok, opts}
|
||||
end
|
||||
end
|
||||
|
||||
opts = Keyword.put(opts, :data_layer, data_layer)
|
||||
|
||||
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?
|
||||
|
||||
def handle_opts(opts) do
|
||||
quote bind_quoted: [embedded?: opts[:embedded?]] do
|
||||
if embedded? do
|
||||
@persist {:embedded?, true}
|
||||
|
||||
Ash.Resource.define_embeddable_type()
|
||||
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
|
||||
|
||||
@embedded_resource_array_constraints [
|
||||
|
@ -560,44 +534,6 @@ defmodule Ash.Resource do
|
|||
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]
|
||||
def extensions(resource) do
|
||||
Extension.get_persisted(resource, :extensions)
|
||||
|
@ -653,12 +589,7 @@ defmodule Ash.Resource do
|
|||
@doc "Whether or not a given module is a resource module"
|
||||
@spec resource?(module) :: boolean
|
||||
def resource?(module) when is_atom(module) do
|
||||
Ash.try_compile(module)
|
||||
|
||||
module.module_info(:attributes)[:is_ash_resource] == [true]
|
||||
rescue
|
||||
_ ->
|
||||
false
|
||||
Ash.Dsl.is?(module, __MODULE__)
|
||||
end
|
||||
|
||||
def resource?(_), do: false
|
||||
|
|
3
mix.exs
3
mix.exs
|
@ -114,6 +114,9 @@ defmodule Ash.MixProject do
|
|||
Ash.Dsl.Section,
|
||||
Ash.Dsl.Transformer
|
||||
],
|
||||
"dsl tooling": [
|
||||
Ash.Dsl
|
||||
],
|
||||
"resource dsl transformers": ~r/Ash.Resource.Transformers/,
|
||||
"api dsl transformers": ~r/Ash.Api.Transformers/,
|
||||
"filter operators": ~r/Ash.Query.Operator/,
|
||||
|
|
Loading…
Reference in a new issue