Packages

spark

2.2.18
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
spark lib spark dsl.ex
Raw

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"
],
extension_kind_types: [
type: :keyword_list,
default: [],
doc:
"A keyword list of extension kinds and their types, e.g `[authorizers: {:list, {:behaviour, Ash.Authorizer}}]`"
],
extension_kind_docs: [
type: :keyword_list,
default: [],
doc:
"A keyword list of extension kinds and a short documentation snippet to be used when autocompleting that option"
],
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`
"""
],
opts_to_document: [
type: {:or, [{:literal, :all}, {:list, :atom}]},
default: :all,
doc: """
A list of `t:atom/0` or `:all`. Spark automatically detects options and documents them in `@moduledoc`.
You can instruct Spark to use only a subset of options, e.g. `opts_to_document: [:fragments]`.
"""
]
]
@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.Options.docs(@using_schema)}
See the callbacks defined in this module to augment the behavior/compilation of the module getting a Dsl.
## Schemas/Data Types
For more information, see `Spark.Options`.
"""
@type opts :: keyword()
@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 """
Validate/add options. Those options will be passed to `handle_opts` and `handle_before_compile`
"""
@callback explain(t(), opts) :: String.t() | nil
@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()) :: Macro.t()
@doc """
A callback that is called in the `after_verify` hook. Only runs on versions of Elixir >= 1.14.0
"""
@callback verify(module, keyword()) :: term
@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()) :: Macro.t()
defmacro __using__(opts) do
opts = Spark.Options.validate!(opts, @using_schema)
their_opt_schema =
Enum.map(opts[:single_extension_kinds], fn extension_kind ->
{extension_kind,
type: opts[:extension_kind_types][extension_kind] || {:behaviour, Spark.Dsl.Extension},
default: opts[:default_extensions][extension_kind],
doc:
opts[:extension_kind_docs][extension_kind] ||
"#{extension_kind} extensions to add to the `#{inspect(__MODULE__)}`"}
end) ++
Enum.map(opts[:many_extension_kinds], fn extension_kind ->
{extension_kind,
[
type:
opts[:extension_kind_types][extension_kind] ||
{:list, {:behaviour, Spark.Dsl.Extension}},
default: [],
doc:
opts[:extension_kind_docs][extension_kind] ||
"#{extension_kind} extensions to add to the `#{inspect(__MODULE__)}`"
]}
end)
their_opt_schema =
if opts[:untyped_extensions?] do
Keyword.put(their_opt_schema, :extensions,
type: {:list, {:behaviour, Spark.Dsl.Extension}},
doc: "A list of DSL extensions to add to the `#{inspect(__MODULE__)}`"
)
else
their_opt_schema
end
their_opt_schema =
Keyword.merge(their_opt_schema,
otp_app: [type: :atom, doc: "The otp_app to use for any application configurable options"],
fragments: [
type: {:list, :module},
doc:
"Fragments to include in the `#{inspect(__MODULE__)}`. See the fragments guide for more."
]
)
their_opt_schema = Keyword.merge(opts[:opt_schema] || [], their_opt_schema)
opts_to_document = opts[:opts_to_document]
quote bind_quoted: [
their_opt_schema: their_opt_schema,
opts_to_document: opts_to_document,
parent_opts: opts,
parent: __CALLER__.module
],
location: :keep,
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()
|> Enum.flat_map(fn module ->
if function_exported?(module, :add_extensions, 0),
do: [module | module.add_extensions()],
else: [module]
end)
|> Enum.sort()
|> Enum.uniq()
@spark_extension_kinds List.wrap(parent_opts[:many_extension_kinds]) ++
List.wrap(parent_opts[:single_extension_kinds])
@behaviour Spark.Dsl
@their_opt_schema their_opt_schema
@doc false
def opt_schema, do: @their_opt_schema
schema_to_document =
if is_list(opts_to_document) do
Enum.filter(@their_opt_schema, fn {opt, _} -> opt in opts_to_document end)
else
@their_opt_schema
end
cond do
@moduledoc == false or schema_to_document == [] ->
:ok
@moduledoc ->
@moduledoc """
#{@moduledoc}
### Options
#{Spark.Options.docs(schema_to_document)}
"""
true ->
@moduledoc """
### Options
#{Spark.Options.docs(schema_to_document)}
"""
end
@doc false
def init(opts), do: {:ok, opts}
@doc false
def explain(_, _), do: nil
@doc false
def verify(_, _), do: :ok
@doc false
def default_extensions, do: @spark_default_extensions
@doc false
def default_extension_kinds, do: List.wrap(unquote(parent_opts[:default_extensions]))
@doc false
def many_extension_kinds, do: List.wrap(unquote(parent_opts[:many_extension_kinds]))
@doc false
def single_extension_kinds, do: List.wrap(unquote(parent_opts[:single_extension_kinds]))
@doc false
def handle_opts(opts) do
quote do
end
end
@doc false
def handle_before_compile(opts) do
quote do
end
end
defmacro __using__(opts) do
opts =
if Macro.quoted_literal?(opts) do
opts
else
[]
end
parent = unquote(parent)
parent_opts = unquote(parent_opts)
their_opt_schema = unquote(their_opt_schema)
require Spark.Dsl.Extension
fragments =
if Keyword.keyword?(opts) do
opts[:fragments]
|> List.wrap()
|> Enum.map(&Spark.Dsl.Extension.do_expand(&1, __CALLER__))
else
[]
end
{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] ->
fragments_set =
Enum.filter(fragments, fn fragment ->
fragment.opts()
|> Spark.Dsl.expand_modules(parent_opts, __CALLER__)
|> elem(0)
|> Keyword.get(key)
end)
cond do
current_value && !Enum.empty?(fragments_set) ->
raise "#{key} is being set as an option, but is also set in fragments: #{Enum.map_join(fragments_set, ", ", &inspect/1)}"
Enum.count(fragments_set) > 1 ->
raise "#{key} is being set by multiple fragments: #{Enum.map_join(fragments_set, ", ", &inspect/1)}"
true ->
current_value || List.first(fragments_set) || defaults
end
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__)
fragment_extensions = Enum.flat_map(fragments, & &1.extensions())
opts =
Enum.reduce(fragments, opts, fn fragment, opts ->
fragment.opts()
|> Spark.Dsl.expand_modules(parent_opts, __CALLER__)
|> elem(0)
|> Enum.reduce(opts, fn {key, value}, opts ->
cond do
key in parent_opts[:single_extension_kinds] ->
Keyword.put(opts, key, value)
key in parent_opts[:many_extension_kinds] ->
Keyword.update(opts, key, value, fn current_extensions ->
Enum.uniq(current_extensions ++ List.wrap(value))
end)
true ->
opts
end
end)
end)
extensions =
extensions
|> Enum.concat(fragment_extensions)
|> Enum.flat_map(&[&1 | &1.add_extensions()])
|> Enum.uniq()
if :elixir_module.mode(__CALLER__.module) == :all do
Module.put_attribute(__CALLER__.module, :extensions, extensions)
end
body =
quote generated: true, location: :keep do
Module.register_attribute(__MODULE__, :extensions, persist: true)
@extensions unquote(extensions)
opts =
unquote(
opts
|> Keyword.take([:extensions] ++ @spark_extension_kinds)
|> Keyword.merge(
opts
|> Keyword.drop([:extensions] ++ @spark_extension_kinds)
|> Spark.Dsl.Extension.expand_alias_no_require(__CALLER__)
)
)
|> Spark.Options.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)
@their_opt_schema their_opt_schema
@opts opts
@before_compile Spark.Dsl
Module.register_attribute(__MODULE__, :spark_is, persist: true)
@spark_is parent
@spark_parent parent
@doc false
def spark_is, do: @spark_is
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
defoverridable init: 1,
handle_opts: 1,
handle_before_compile: 1,
explain: 2,
__using__: 1,
verify: 2
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
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)
verify_code =
quote generated: true do
@after_verify {__MODULE__, :__verify_spark_dsl__}
@doc false
def __verify_spark_dsl__(module) do
unquote(parent).verify(module, @opts)
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 is_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
code =
quote generated: true,
bind_quoted: [dsl: __MODULE__, parent: parent, fragments: opts[:fragments]] do
require Spark.Dsl.Extension
for extension <- @extensions do
for section <- extension.sections() do
if section.top_level? do
current_config =
Process.get(
{__MODULE__, :spark, [section.name]},
%{entities: [], opts: []}
)
opts =
case Spark.Options.validate(
current_config.opts,
Map.get(section, :schema, [])
) do
{:ok, opts} ->
opts
{:error, error} ->
raise Spark.Error.DslError,
module: __MODULE__,
message: error,
path: [section.name]
end
Process.put(
{__MODULE__, :spark, [section.name]},
%{
entities: current_config.entities,
opts: opts
}
)
end
end
end
Module.register_attribute(__MODULE__, :spark_is, persist: true)
Module.put_attribute(__MODULE__, :spark_is, @spark_is)
Spark.Dsl.Extension.set_state(@persist, fragments)
for {block, bindings} <- Enum.reverse(@spark_dsl_config[:eval] || []) do
Code.eval_quoted(block, bindings, __ENV__)
end
def __spark_placeholder__, do: nil
@doc false
for {path, %{entities: entities}} <- @spark_dsl_config do
def entities(unquote(path)), do: unquote(Macro.escape(entities || []))
end
def entities(_), do: []
@doc false
for {path, %{opts: opts}} <- @spark_dsl_config, is_list(path) do
for {key, value} <- opts do
def fetch_opt(unquote(path), unquote(key)) do
{:ok, unquote(Macro.escape(value))}
end
end
end
def fetch_opt(_, _), do: :error
@doc false
def spark_dsl_config do
@spark_dsl_config
end
@persisted Map.drop(@spark_dsl_config[:persist], [:env])
@doc false
for {key, value} <- @persisted do
def persisted(unquote(key), _), do: unquote(Macro.escape(value))
end
def persisted(_, default), do: default
@doc false
for {key, value} <- @persisted do
def persisted(unquote(key)), do: unquote(Macro.escape(value))
end
def persisted(_), do: nil
@doc false
def persisted do
@persisted
end
cond do
@moduledoc == false ->
:ok
@moduledoc ->
@moduledoc """
#{@moduledoc}
#{Spark.Dsl.Extension.explain(parent, @opts, @extensions, @spark_dsl_config)}
"""
true ->
@moduledoc Spark.Dsl.Extension.explain(parent, @opts, @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
def handle_fragments(dsl_config, fragments) do
fragments
|> List.wrap()
|> Enum.reduce(dsl_config, fn fragment, acc ->
config = Map.delete(fragment.spark_dsl_config(), :persist)
Map.merge(acc, config, fn
key, %{entities: entities, opts: opts}, %{entities: right_entities, opts: right_opts} ->
%{
entities: entities ++ right_entities,
opts: merge_with_warning(opts, right_opts, key, "fragment: #{fragment}")
}
end)
end)
end
@doc false
def merge_with_warning(left, right, path, overwriting_by \\ nil) do
Keyword.merge(left, right, fn
_, left, left ->
left
key, left, right ->
by =
if overwriting_by do
" by #{overwriting_by}"
else
""
end
IO.warn(
"#{Enum.join(path ++ [key], ".")} is being overwritten from #{inspect(left)} to #{inspect(right)}#{by}"
)
right
end)
end
end