Packages
spark
0.3.3
2.7.2
2.7.1
2.7.0
2.6.1
2.6.0
2.5.0
2.4.1
2.4.0
2.3.14
2.3.13
2.3.12
2.3.11
2.3.10
2.3.9
2.3.8
2.3.7
2.3.6
2.3.5
2.3.4
2.3.3
2.3.2
2.3.1
2.3.0
2.2.69
2.2.68
2.2.67
2.2.66
2.2.65
2.2.64
2.2.63
2.2.62
2.2.61
2.2.60
2.2.59
2.2.58
2.2.57
2.2.56
2.2.55
2.2.54
2.2.53
2.2.52
2.2.51
2.2.50
2.2.49
2.2.48
2.2.47
2.2.46
2.2.45
2.2.44
2.2.43
2.2.42
2.2.41
2.2.40
2.2.39
2.2.38
2.2.37
2.2.36
2.2.35
2.2.34
2.2.33
2.2.32
2.2.31
2.2.30
2.2.29
2.2.28
2.2.27
2.2.26
2.2.25
2.2.24
2.2.23
2.2.22
2.2.21
2.2.20
2.2.19
2.2.18
2.2.17
2.2.16
2.2.15
2.2.14
2.2.13
2.2.12
2.2.11
2.2.10
2.2.9
2.2.8
2.2.7
2.2.6
2.2.5
2.2.4
2.2.3
2.2.2
2.2.1
2.2.0
2.1.24
2.1.23
2.1.22
2.1.21
2.1.20
2.1.19
2.1.18
2.1.17
2.1.16
2.1.15
2.1.14
2.1.13
2.1.12
2.1.11
2.1.10
2.1.9
2.1.8
2.1.7
2.1.6
2.1.5
2.1.4
2.1.3
2.1.2
2.1.1
2.1.0
2.0.1
2.0.0
1.1.55
1.1.54
1.1.53
1.1.52
1.1.51
1.1.50
1.1.49
1.1.48
1.1.47
1.1.46
1.1.45
1.1.44
1.1.43
1.1.42
1.1.41
1.1.40
1.1.39
1.1.38
1.1.37
1.1.36
1.1.35
1.1.34
1.1.32
1.1.31
1.1.30
1.1.29
1.1.28
1.1.27
1.1.26
1.1.25
1.1.24
1.1.22
1.1.21
1.1.20
1.1.19
1.1.18
1.1.17
1.1.16
1.1.15
1.1.14
retired
1.1.13
1.1.12
1.1.11
1.1.10
1.1.9
1.1.8
1.1.7
1.1.6
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.0.9
1.0.8
1.0.7
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.18
0.2.17
0.2.16
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.29
0.1.28
0.1.27
0.1.26
0.1.25
0.1.24
0.1.23
0.1.22
0.1.21
0.1.20
0.1.19
0.1.18
0.1.17
0.1.16
0.1.15
0.1.14
0.1.13
0.1.12
0.1.11
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
Generic tooling for building DSLs
Current section
Files
Jump to
Current section
Files
lib/spark/dsl.ex
defmodule Spark.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.
"""
],
opt_schema: [
type: :keyword_list,
default: [],
doc: """
A schema for additional options to accept when calling `use YourSpark`
"""
]
]
@type entity :: %Spark.Dsl.Entity{}
@type section :: %Spark.Dsl.Section{}
@moduledoc """
The primary entry point for adding a DSL to a module.
To add a DSL to a module, add `use Spark.Dsl, ...options`. The options supported with `use Spark.Dsl` are:
#{Spark.OptionsHelpers.docs(@using_schema)}
See the callbacks defined in this module to augment the behavior/compilation of the module getting a Dsl.
## Schemas/Data Types
Spark DSLs use a superset of `NimbleOptions` for the `schema` that makes up sections/entities of the DSL.
For more information, see `Spark.OptionsHelpers`.
"""
@type opts :: Keyword.t()
@type t :: map()
@doc """
Validate/add options. Those options will be passed to `handle_opts` and `handle_before_compile`
"""
@callback init(opts) :: {:ok, opts} | {:error, String.t() | term}
@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 = Spark.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
their_opt_schema = Keyword.put(their_opt_schema, :otp_app, type: :atom)
their_opt_schema = Keyword.merge(opts[:opt_schema] || [], their_opt_schema)
quote bind_quoted: [
their_opt_schema: their_opt_schema,
parent_opts: opts,
parent: __CALLER__.module
],
generated: true do
require Spark.Dsl.Extension
@dialyzer {:nowarn_function, handle_opts: 1, handle_before_compile: 1}
Module.register_attribute(__MODULE__, :spark_dsl, persist: true)
Module.register_attribute(__MODULE__, :spark_default_extensions, persist: true)
Module.register_attribute(__MODULE__, :spark_extension_kinds, persist: true)
@spark_dsl true
@spark_default_extensions parent_opts[:default_extensions]
|> Keyword.values()
|> List.flatten()
@spark_extension_kinds List.wrap(parent_opts[:many_extension_kinds]) ++
List.wrap(parent_opts[:single_extension_kinds])
def init(opts), do: {:ok, opts}
def default_extensions, do: @spark_default_extensions
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)
require Spark.Dsl.Extension
{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 ->
current_value
end
end)
end)
|> Spark.Dsl.expand_modules(parent_opts, __CALLER__)
body =
quote generated: true do
opts =
unquote(opts)
|> Spark.OptionsHelpers.validate!(unquote(their_opt_schema))
|> unquote(__MODULE__).init()
|> Spark.Dsl.unwrap()
parent = unquote(parent)
parent_opts = unquote(parent_opts)
their_opt_schema = unquote(their_opt_schema)
@opts opts
@before_compile Spark.Dsl
@after_compile __MODULE__
@spark_is parent
@spark_parent parent
def spark_is, do: @spark_is
defmacro __after_compile__(_, _) do
quote do
# This is here because dialyzer complains
# this is really dumb but it works so 🤷♂️
if @after_compile_transformers do
transformers_to_run =
@extensions
|> Enum.flat_map(& &1.transformers())
|> Spark.Dsl.Transformer.sort()
|> Enum.filter(& &1.after_compile?())
@extensions
|> Enum.flat_map(& &1.verifiers())
|> Enum.each(fn verifier ->
case verifier.verify(@spark_dsl_config) do
:ok ->
:ok
{:warn, warnings} ->
warnings
|> List.wrap()
|> Enum.each(&IO.warn(&1, Macro.Env.stacktrace(__ENV__)))
{:error, error} ->
if Exception.exception?(error) do
raise error
else
raise "Verification error from #{inspect(verifier)}: #{inspect(error)}"
end
end
end)
__MODULE__
|> Spark.Dsl.Extension.run_transformers(
transformers_to_run,
@spark_dsl_config,
__ENV__
)
end
end
end
Module.register_attribute(__MODULE__, :persist, accumulate: true)
opts
|> @spark_parent.handle_opts()
|> Code.eval_quoted([], __ENV__)
if opts[:otp_app] do
@persist {:otp_app, opts[:otp_app]}
end
@persist {:module, __MODULE__}
@persist {:file, __ENV__.file}
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 = Spark.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 Spark.implements_behaviour?(mod, Spark.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, &Spark.implements_behaviour?(&1, Spark.Dsl.Extension))
{Keyword.put(opts, key, mods), extensions}
true ->
{Keyword.put(opts, key, value), extensions}
end
end)
end
@after_verify_supported Version.match?(System.version(), ">= 1.14.0")
defmacro __before_compile__(env) do
parent = Module.get_attribute(env.module, :spark_parent)
opts = Module.get_attribute(env.module, :opts)
parent_code = parent.handle_before_compile(opts)
# This `to_string() == "true"` bit is to appease dialyzer
# It is very dumb but it works so 🤷♂️
verify_code =
if to_string(@after_verify_supported) == "true" do
quote generated: true do
@after_compile_transformers false
@after_verify {__MODULE__, :__verify_ash_dsl__}
@doc false
def __verify_ash_dsl__(_module) do
transformers_to_run =
@extensions
|> Enum.flat_map(& &1.transformers())
|> Spark.Dsl.Transformer.sort()
|> Enum.filter(& &1.after_compile?())
@extensions
|> Enum.flat_map(& &1.verifiers())
|> Enum.each(fn verifier ->
case verifier.verify(@spark_dsl_config) do
:ok ->
:ok
{:warn, warnings} ->
warnings
|> List.wrap()
|> Enum.each(&IO.warn(&1, Macro.Env.stacktrace(__ENV__)))
{:error, error} ->
if Exception.exception?(error) do
raise error
else
raise "Verification error from #{inspect(verifier)}: #{inspect(error)}"
end
end
end)
__MODULE__
|> Spark.Dsl.Extension.run_transformers(
transformers_to_run,
@spark_dsl_config,
__ENV__
)
end
end
else
quote do
@after_compile_transformers true
end
end
code =
quote generated: true, bind_quoted: [dsl: __MODULE__] do
require Spark.Dsl.Extension
Module.register_attribute(__MODULE__, :spark_is, persist: true)
Module.put_attribute(__MODULE__, :spark_is, @spark_is)
Spark.Dsl.Extension.set_state(@persist)
for {block, bindings} <- @spark_dsl_config[:eval] || [] do
Code.eval_quoted(block, bindings, __ENV__)
end
def __spark_placeholder__, do: nil
def spark_dsl_config do
@spark_dsl_config
end
if @moduledoc do
@moduledoc """
#{@moduledoc}
#{Spark.Dsl.Extension.explain(@extensions, @spark_dsl_config)}
"""
else
@moduledoc Spark.Dsl.Extension.explain(@extensions, @spark_dsl_config)
end
end
[code, parent_code, verify_code]
end
def is?(module, type) when is_atom(module) do
module.spark_is() == type
rescue
_ -> false
end
def is?(_module, _type), do: false
end