Packages

spark

2.3.12
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 info_generator.ex
Raw

lib/spark/info_generator.ex

# SPDX-FileCopyrightText: 2022 spark contributors <https://github.com/ash-project/spark/graphs.contributors>
#
# SPDX-License-Identifier: MIT
defmodule Spark.InfoGenerator do
@moduledoc """
Used to dynamically generate configuration functions for Spark extensions
based on their DSL.
## Usage
```elixir
defmodule MyConfig do
use Spark.InfoGenerator, extension: MyDslExtension, sections: [:my_section]
end
```
"""
@type options :: [{:extension, module} | {:sections, [atom]}]
@doc false
@spec __using__(options) :: Macro.t()
defmacro __using__(opts) do
extension = Keyword.fetch!(opts, :extension) |> Macro.expand(__CALLER__)
sections = Keyword.get(opts, :sections, [])
quote do
require Spark.InfoGenerator
require unquote(extension)
Spark.InfoGenerator.generate_config_functions(
unquote(extension),
unquote(sections)
)
Spark.InfoGenerator.generate_options_functions(
unquote(extension),
unquote(sections)
)
Spark.InfoGenerator.generate_entity_functions(
unquote(extension),
unquote(sections)
)
end
end
@doc """
Given an extension and a list of DSL sections, generate an options function
which returns a map of all configured options for a resource (including
defaults).
"""
@spec generate_options_functions(module, [atom]) :: Macro.t()
defmacro generate_options_functions(extension, sections) do
for {path, options} <- extension_sections_to_option_list(extension, sections) do
function_name = :"#{Enum.join(path, "_")}_options"
quote location: :keep do
@doc """
#{unquote(Enum.join(path, "."))} DSL options
Returns a map containing the and any configured or default values.
"""
@spec unquote(function_name)(dsl_or_extended :: module | map) :: %{required(atom) => any}
def unquote(function_name)(dsl_or_extended) do
import Spark.Dsl.Extension, only: [get_opt: 4]
unquote(Macro.escape(options))
|> Stream.map(fn option ->
value =
dsl_or_extended
|> get_opt(option.path, option.name, Map.get(option, :default))
{option.name, value}
end)
|> Stream.reject(&is_nil(elem(&1, 1)))
|> Map.new()
end
defoverridable [{unquote(function_name), 1}]
end
end
end
@doc """
Given an extension and a list of DSL sections, generate an entities function
which returns a list of entities.
"""
@spec generate_entity_functions(module, [atom]) :: Macro.t()
defmacro generate_entity_functions(extension, sections) do
entity_paths =
extension.sections()
|> Stream.filter(&(&1.name in sections))
|> Stream.flat_map(&explode_section([], &1))
|> Stream.filter(fn {_, section} -> section.patchable? || Enum.any?(section.entities) end)
|> Stream.map(&elem(&1, 0))
for path <- entity_paths do
function_name = path |> Enum.join("_") |> String.to_atom()
quote location: :keep do
@doc """
#{unquote(Enum.join(path, "."))} DSL entities
"""
@spec unquote(function_name)(dsl_or_extended :: module | map) :: [struct]
def unquote(function_name)(dsl_or_extended) do
import Spark.Dsl.Extension, only: [get_entities: 2]
get_entities(dsl_or_extended, unquote(path))
end
defoverridable [{unquote(function_name), 1}]
end
end
end
@doc """
Given an extension and a list of DSL sections generate individual config
functions for each option.
"""
@spec generate_config_functions(module, [atom]) :: Macro.t()
defmacro generate_config_functions(extension, sections) do
for {_, options} <- extension_sections_to_option_list(extension, sections) do
for option <- options do
generate_config_function(option)
end
end
end
defp explode_section(path, %{sections: [], name: name} = section),
do: [{path ++ [name], section}]
defp explode_section(path, %{sections: sections, name: name} = section) do
path = path ++ [name]
head = [{path, section}]
tail = Stream.flat_map(sections, &explode_section(path, &1))
Stream.concat(head, tail)
end
# sobelow_skip ["DOS.StringToAtom"]
defp extension_sections_to_option_list(extension, sections) do
extension.sections()
|> Stream.filter(&(&1.name in sections))
|> Stream.flat_map(&explode_section([], &1))
|> Stream.reject(fn {_, section} -> Enum.empty?(section.schema) end)
|> Stream.map(fn {path, section} ->
schema =
section.schema
|> Enum.map(fn {name, opts} ->
opts
|> Map.new()
|> Map.take(~w[type doc default]a)
|> Map.update!(:type, &spec_for_type(&1, opts))
|> Map.put(:pred?, name |> to_string() |> String.ends_with?("?"))
|> Map.put(:name, name)
|> Map.put(:path, path)
|> Map.put(
:function_name,
path
|> Enum.concat([name])
|> Enum.join("_")
|> String.trim_trailing("?")
|> String.to_atom()
)
end)
{path, schema}
end)
|> Map.new()
end
# sobelow_skip ["DOS.BinToAtom"]
defp generate_config_function(%{pred?: true} = option) do
function_name = :"#{option.function_name}?"
quote location: :keep do
@doc unquote(option.doc)
@spec unquote(function_name)(dsl_or_extended :: module | map) ::
unquote(option.type)
def unquote(function_name)(dsl_or_extended) do
import Spark.Dsl.Extension, only: [get_opt: 4]
get_opt(
dsl_or_extended,
unquote(option.path),
unquote(option.name),
unquote(Macro.escape(option.default))
)
end
defoverridable [{unquote(function_name), 1}]
end
end
# sobelow_skip ["DOS.BinToAtom"]
defp generate_config_function(option) do
option = Map.put_new(option, :default, nil)
quote location: :keep do
@doc unquote(Map.get(option, :doc, false))
@spec unquote(option.function_name)(dsl_or_extended :: module | map) ::
{:ok, unquote(option.type)} | :error
if unquote(is_nil(option.default)) do
def unquote(option.function_name)(dsl_or_extended) do
import Spark.Dsl.Extension, only: [fetch_opt: 3]
case fetch_opt(
dsl_or_extended,
unquote(option.path),
unquote(option.name)
) do
:error -> :error
{:ok, value} -> {:ok, value}
end
end
else
def unquote(option.function_name)(dsl_or_extended) do
import Spark.Dsl.Extension, only: [fetch_opt: 3]
case fetch_opt(
dsl_or_extended,
unquote(option.path),
unquote(option.name)
) do
:error -> {:ok, unquote(Macro.escape(option.default))}
{:ok, value} -> {:ok, value}
end
end
end
@doc unquote(Map.get(option, :doc, false))
@spec unquote(:"#{option.function_name}!")(dsl_or_extended :: module | map) ::
unquote(option.type) | no_return
if unquote(is_nil(option.default)) do
# sobelow_skip ["DOS.BinToAtom"]
def unquote(:"#{option.function_name}!")(dsl_or_extended) do
import Spark.Dsl.Extension, only: [fetch_opt: 3, get_persisted: 2]
case fetch_opt(
dsl_or_extended,
unquote(option.path),
unquote(option.name)
) do
:error ->
on = get_persisted(dsl_or_extended, :module)
raise "No configuration for `#{unquote(option.name)}` present on #{inspect(on)}"
{:ok, value} ->
value
end
end
else
# sobelow_skip ["DOS.BinToAtom"]
def unquote(:"#{option.function_name}!")(dsl_or_extended) do
import Spark.Dsl.Extension, only: [fetch_opt: 3, get_persisted: 2]
case fetch_opt(
dsl_or_extended,
unquote(option.path),
unquote(option.name)
) do
:error ->
unquote(Macro.escape(option.default))
{:ok, value} ->
value
end
end
end
defoverridable [
{unquote(option.function_name), 1},
{unquote(:"#{option.function_name}!"), 1}
]
end
end
def spec_for_type(:keyword_list, opts) do
if opts[:keys] do
value_type =
opts[:keys]
|> Enum.map(fn {key, spec} ->
key_spec =
case key do
:* -> spec_for_type(:atom, [])
other -> other
end
{key_spec, spec_for_type(spec[:type], spec)}
end)
|> Enum.uniq()
|> Enum.reduce(nil, fn
type, nil ->
type
type, other_type ->
{:|, [], [type, other_type]}
end)
[value_type]
else
quote do
keyword()
end
end
end
# TODO: this should be combined with `Spark.Options.Docs.type_to_spec
def spec_for_type({:literal, value}, _opts) when is_atom(value) or is_integer(value) do
value
end
def spec_for_type({:literal, value}, _opts) when is_tuple(value) do
value
|> Tuple.to_list()
|> Enum.map(&spec_for_type({:literal, &1}, []))
|> List.to_tuple()
end
def spec_for_type({:literal, value}, _opts) when is_list(value) do
Enum.map(value, &spec_for_type({:literal, &1}, []))
end
def spec_for_type({:literal, _value}, _opts) do
{:any, [], Elixir}
end
def spec_for_type({:behaviour, _module}, _opts), do: {:module, [], Elixir}
def spec_for_type({:spark, _}, _opts), do: {:module, [], Elixir}
def spec_for_type({:spark_function_behaviour, behaviour, {_, arity}}, _opts),
do:
spec_for_type(
{:or,
[
{:behaviour, behaviour},
{{:behaviour, behaviour}, {:keyword, [], Elixir}},
{:fun, arity}
]},
[]
)
def spec_for_type({:fun, arity}, _opts) do
args = List.duplicate({:any, [], Elixir}, arity)
[{:->, [], [args, {:any, [], Elixir}]}]
end
def spec_for_type({:or, [type]}, _opts), do: spec_for_type(type, [])
def spec_for_type({:or, [next | remaining]}, _opts),
do: {:|, [], [spec_for_type(next, []), spec_for_type({:or, remaining}, [])]}
def spec_for_type({:in, %Range{first: first, last: last}}, _opts)
when is_integer(first) and is_integer(last),
do: {:.., [], [first, last]}
def spec_for_type({:in, %Range{first: first, last: last}}, _opts),
do:
{{:., [], [{:__aliases__, [], [:Range]}, :t]}, [],
[spec_for_type(first, []), spec_for_type(last, [])]}
def spec_for_type({:in, values}, _opts) do
values
|> Enum.map(&spec_for_type({:literal, &1}, []))
|> case do
[] ->
{:any, [], Elixir}
specs ->
Enum.reduce(specs, nil, fn
spec, nil ->
spec
spec, other_specs ->
{:|, [], [spec, other_specs]}
end)
end
end
def spec_for_type({type, subtype}, _opts) when type in [:list, :wrap_list],
do: [spec_for_type(subtype, [])]
def spec_for_type({:custom, _, _, _}, _opts), do: spec_for_type(:any, [])
def spec_for_type({:tuple, subtypes}, _opts) do
subtypes
|> Enum.map(&spec_for_type(&1, []))
|> List.to_tuple()
end
def spec_for_type(:quoted, _opts) do
{:any, [], Elixir}
end
def spec_for_type(:string, _opts),
do: {{:., [], [{:__aliases__, [alias: false], [:String]}, :t]}, [], []}
def spec_for_type(:literal, _opts) do
{:any, [], Elixir}
end
def spec_for_type(terminal, _opts)
when terminal in ~w[any map atom string boolean integer non_neg_integer pos_integer float timeout pid reference mfa]a,
do: {terminal, [], Elixir}
def spec_for_type(atom, _opts) when is_atom(atom), do: atom
def spec_for_type(number, _opts) when is_number(number), do: number
def spec_for_type(string, _opts) when is_binary(string), do: spec_for_type(:string, [])
def spec_for_type({:one_of, values}, opts), do: spec_for_type({:in, values}, opts)
def spec_for_type({mod, arg}, _opts) when is_atom(mod) and is_list(arg),
do: {{:module, [], Elixir}, {:list, [], Elixir}}
def spec_for_type({:map, key_type, value_type}, _opts) do
{:%{}, [], [{{:optional, [], [spec_for_type(key_type, [])]}, spec_for_type(value_type, [])}]}
end
def spec_for_type(:map, opts) do
spec_for_type({:map, :atom, :any}, opts)
end
def spec_for_type(tuple, _opts) when is_tuple(tuple) do
{:tuple, [], []}
end
def spec_for_type([], _), do: []
def spec_for_type([type], opts), do: [spec_for_type(type, opts)]
end