Packages

spark

1.1.7
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 options_helpers.ex
Raw

lib/spark/options_helpers.ex

defmodule Spark.OptionsHelpers do
@moduledoc """
Helpers for working with our superset of nimble options.
## Additional Types
Spark provides the following additional option types for use when building DSLs or validating options.
- `{:one_of, values}` - maps to `{:in, values}` for backwards compatibility
- `{:tagged_tuple, tag, inner_type}` - maps to `{tag, type}`
- `{:spark_behaviour, behaviour}` - expects a module that implements the given behaviour, and can be specified with options, i.e `mod` or `{mod, [opt: :val]}`
- `{:spark_behaviour, behaviour, builtin_module}` - Same as the above, but also accepts a `builtin_module`. The builtin_module is used to provide additional options for the elixir_sense plugin.
- `{:spark_function_behaviour, behaviour, {function_mod, arity}}` - expects a module that implements the given behaviour, and can be specified with options, i.e `mod` or `{mod, [opt: :val]}`, that also has a special module that supports being provided an anonymous function or MFA as the `:fun` option.
- `{:spark_function_behaviour, behaviour, builtin_module, {function_mod, arity}}` - Same as the above, but also accepts a `builtin_module`. The builtin_module is used to provide additional options for the elixir_sense plugin.
- `{:behaviour, behaviour}` - expects a module that implements a given behaviour.
- `{:spark, dsl_module}` - expects a module that is a `Spark.Dsl`
- `{:mfa_or_fun, arity}` - expects a function or MFA of a corresponding arity.
- `{:spark_type, module, builtin_function}` - a behaviour that defines `builtin_function/0` that returns a list of atoms that map to built in variations of that thing.
- `{:spark_type, module, builtin_function, templates}` - same as the above, but includes additional templates for elixir_sense autocomplete
- `:literal` -> any literal value. Maps to `:any`, but is used for documentation.
- `{:literal, value}` -> exactly the value specified.
- `:quoted` -> retains the quoted value of the code provided to the option
- `{:wrap_list, type}` -> Allows a single value or a list of values.
"""
@typedoc """
Spark provides additional types on top of those provided by [NimbleOptions](https://hex.pm/packages/nimble_options).
"""
@type type ::
nimble_types
| {:one_of, [type]}
| {:tagged_tuple, tag :: type, inner_type :: type}
| {:spark_behaviour, module}
| {:spark_behaviour, module, module}
| {:spark_function_behaviour, module, {module, integer}}
| {:spark_function_behaviour, module, module, {module, integer}}
| {:behaviour, module}
| {:spark, module}
| {:mfa_or_fun, non_neg_integer()}
| {:spark_type, module, builtin_function :: atom}
| {:spark_type, module, builtin_function :: atom, templates :: [String.t()]}
| {:struct, module}
| {:wrap_list, type}
| :literal
| {:literal, any}
| :quoted
@typedoc """
The types as specified by the [NimbleOptions docs](https://hexdocs.pm/nimble_options/NimbleOptions.html#module-types).
"""
@type nimble_types ::
:any
| :keyword_list
| :non_empty_keyword_list
| :map
| {:map, key_type :: type, value_type :: type}
| :atom
| :string
| :boolean
| :integer
| :non_neg_integer
| :pos_integer
| :float
| :timeout
| :pid
| :reference
| :mfa
| :mod_arg
| {:fun, arity :: non_neg_integer}
| {:in, [type] | Range.t()}
| {:custom, module, function :: atom, args :: [any]}
| {:or, [type]}
| {:list, type | {:keyword_list, type} | {:non_empty_keyword_list, type}}
| {:tuple, [type | {:keyword_list, type} | {:non_empty_keyword_list, type}]}
@typedoc """
The schema type, as defined by the [NimbleOptions docs](https://hexdocs.pm/nimble_options/NimbleOptions.html#module-schema-options).
"""
@type nimble_schema :: [
{:type, type | :any}
| {:required, boolean}
| {:default, :any}
| {:keys, Keyword.t({:type, type})}
| {:deprecated, String.t()}
| {:doc, String.t()}
| {:subsection, String.t()}
| {:type_doc, false | String.t()}
| {:rename_to, atom}
]
@typedoc """
The actual schema type, as used in sections and entities.
"""
@type schema :: Keyword.t(nimble_schema)
@doc """
Merges two schemas, and sets the `subsection` option on all options on the right
side.
"""
def merge_schemas(left, right, section \\ nil) do
new_right =
Enum.map(right, fn {key, value} ->
{key, Keyword.put(value, :subsection, section)}
end)
Keyword.merge(left, new_right)
end
@doc """
Sanitizes the option schema and validates with NimbleOptions.
"""
def validate(opts, schema) do
NimbleOptions.validate(opts, sanitize_schema(schema))
end
@doc """
Sanitizes the option schema and validates with NimbleOptions.
"""
def validate!(opts, schema) do
NimbleOptions.validate!(opts, sanitize_schema(schema))
end
@doc """
Creates markdown documentation for a given schema.
"""
def docs(schema) do
schema
|> Enum.reject(fn {_key, opts} ->
opts[:hide]
end)
|> sanitize_schema()
|> Enum.map(fn {key, opts} ->
{key, update_key_docs(opts)}
end)
|> NimbleOptions.docs()
end
@doc false
def update_key_docs(opts) do
opts
|> Keyword.put_new(:doc, "")
|> Keyword.update!(:doc, &String.replace(&1, "\n\n", " \n"))
|> document_values()
end
defp document_values(opts) do
case opts[:type] do
{in_type, values} when in_type in [:in, :one_of] ->
values = Enum.map_join(values, ", ", &inspect/1)
Keyword.update!(
opts,
:doc,
&"#{&1} Valid values are #{values}"
)
{list_type, {in_type, values}}
when in_type in [:in, :one_of] and list_type in [:list, :wrap_list] ->
values = Enum.map_join(values, ", ", &inspect/1)
Keyword.update!(
opts,
:doc,
&"#{&1} Valid values are #{values}"
)
_ ->
opts
end
end
@non_nimble_options [:hide, :as, :snippet, :links]
defp sanitize_schema(schema) do
Enum.map(schema, fn {key, opts} ->
opts =
if opts[:type] do
Keyword.update!(opts, :type, &sanitize_type(&1, key))
else
opts
end
opts =
if opts[:keys] && Keyword.keyword?(opts[:keys]) do
Keyword.update!(opts, :keys, &sanitize_schema(&1))
else
opts
end
{key, Keyword.drop(opts, @non_nimble_options)}
end)
end
defp sanitize_type(type, key) when is_list(type) do
Enum.map(type, &sanitize_type(&1, key))
end
defp sanitize_type(type, key) do
case type do
{:one_of, values} ->
{:in, sanitize_type(values, key)}
{:in, values} ->
{:in, sanitize_type(values, key)}
{:or, subtypes} ->
{:or, sanitize_type(subtypes, key)}
{:tagged_tuple, tag, type} ->
{:custom, __MODULE__, :tagged_tuple, [key, sanitize_type(type, key), tag]}
{:struct, struct} ->
{:custom, __MODULE__, :struct, [struct]}
{:list, values} ->
{:list, sanitize_type(values, key)}
{:wrap_list, types} ->
{:custom, __MODULE__, :wrap_list, [key, sanitize_type(types, key)]}
:quoted ->
:any
{:spark_behaviour, behaviour, _builtins} ->
{:custom, __MODULE__, :spark_behaviour, [behaviour]}
{:spark_behaviour, behaviour} ->
{:custom, __MODULE__, :spark_behaviour, [behaviour]}
{:spark_function_behaviour, behaviour, _builtins, {functional, arity}} ->
{:custom, __MODULE__, :spark_function_behaviour, [behaviour, {functional, arity}]}
{:spark_function_behaviour, behaviour, {functional, arity}} ->
{:custom, __MODULE__, :spark_function_behaviour, [behaviour, {functional, arity}]}
{:behaviour, _behaviour} ->
:atom
{:spark, _} ->
:atom
{:mfa_or_fun, arity} ->
{:custom, __MODULE__, :mfa_or_fun, [arity]}
{:spark_type, _, _} ->
:any
{:spark_type, _, _, _} ->
:any
{:tuple, subtypes} ->
{:tuple, Enum.map(subtypes, &sanitize_type(&1, key))}
{:literal, value} ->
{:custom, __MODULE__, :literal_value, [value]}
:literal ->
:any
:module ->
:atom
type ->
type
end
end
def literal_value(value, value), do: {:ok, value}
def literal_value(_, value), do: {:error, "is not #{inspect(value)}"}
def wrap_list(value, key, type) when not is_list(value) do
wrap_list(List.wrap(value), key, type)
end
def wrap_list(values, key, type) do
case Spark.OptionsHelpers.validate(
values,
[
{key,
[
type: {:list, type}
]}
]
) do
{:ok, opts} ->
{:ok, opts[key]}
{:error, %NimbleOptions.ValidationError{message: message}} ->
{:error, message}
end
end
def tagged_tuple({tag, value}, key, type, tag) do
case Spark.OptionsHelpers.validate(
[{key, value}],
[
{key,
[
type: type
]}
]
) do
{:ok, opts} ->
{:ok, {tag, opts[key]}}
{:error, %NimbleOptions.ValidationError{message: message}} ->
{:error, message}
end
end
def tagged_tuple(other, _key, _type, tag) do
{:error,
"Expected a tagged tuple in the form of {#{inspect(tag)}, value}, got: #{inspect(other)}"}
end
def spark_behaviour({module, opts}, _behaviour)
when is_atom(module) and module not in [nil, true, false] do
if Keyword.keyword?(opts) do
# We can't check if it implements the behaviour here, unfortunately
# As it may not be immediately available
{:ok, {module, opts}}
else
{:error, "Expected opts to be a keyword, got: #{inspect(opts)}"}
end
end
def spark_behaviour(module, behaviour)
when is_atom(module) and module not in [nil, true, false] do
spark_behaviour({module, []}, behaviour)
end
def spark_behaviour(other, _) do
{:error, "expected a module and opts, got: #{inspect(other)}"}
end
def spark_function_behaviour({module, opts}, _behaviour, {_functional, _arity})
when is_atom(module) and module not in [nil, true, false] do
if Keyword.keyword?(opts) do
# We can't check if it implements the behaviour here, unfortunately
# As it may not be immediately available
{:ok, {module, opts}}
else
{:error, "Expected opts to be a keyword, got: #{inspect(opts)}"}
end
end
def spark_function_behaviour(module, behaviour, {functional, arity})
when is_atom(module) and module not in [nil, true, false] do
spark_function_behaviour({module, []}, behaviour, {functional, arity})
end
def spark_function_behaviour(function, _behaviour, {functional, arity})
when is_function(function, arity) do
{:ok, {functional, [fun: function]}}
end
def spark_function_behaviour(function, _behaviour, {_functional, arity})
when is_function(function) do
actual_arity = :erlang.fun_info(function)[:arity]
{:error,
"Expected a module and opts, or a #{arity} argument function. Got a #{actual_arity} argument function."}
end
def spark_function_behaviour(other, _, {_, arity}) do
{:error,
"Expected a module and opts, or a #{arity} argument function, got: #{inspect(other)}"}
end
def map(value) when is_map(value), do: {:ok, value}
def map(_), do: {:error, "must be a map"}
def struct(value, struct) when is_struct(value, struct), do: {:ok, value}
def struct(_, struct), do: {:error, "must be a struct instance of #{inspect(struct)}"}
@deprecated "Use {:list, :atom} instead"
def list_of_atoms(value) do
if is_list(value) and Enum.all?(value, &is_atom/1) do
{:ok, value}
else
{:error, "Expected a list of atoms"}
end
end
def module_and_opts({module, opts}) when is_atom(module) and module not in [nil, true, false] do
if Keyword.keyword?(opts) do
{:ok, {module, opts}}
else
{:error, "Expected the second element to be a keyword list, got: #{inspect(opts)}"}
end
end
def module_and_opts({other, _}) do
{:error, "Expected the first element to be a module, got: #{inspect(other)}"}
end
def module_and_opts(module) do
module_and_opts({module, []})
end
def mfa_or_fun(value, arity) when is_function(value, arity), do: {:ok, value}
def mfa_or_fun({module, function, args}, _arity)
when is_atom(module) and is_atom(function) and is_list(args),
do: {:ok, {module, function, args}}
def mfa_or_fun(value, _), do: {:ok, value}
def make_required!(options, field) do
Keyword.update!(options, field, &Keyword.put(&1, :required, true))
end
def make_optional!(options, field) do
Keyword.update!(options, field, &Keyword.delete(&1, :required))
end
def set_type!(options, field, type) do
Keyword.update!(options, field, &Keyword.put(&1, :type, type))
end
def set_default!(options, field, value) do
Keyword.update!(options, field, fn config ->
config
|> Keyword.put(:default, value)
|> Keyword.delete(:required)
end)
end
def append_doc!(options, field, to_append) do
Keyword.update!(options, field, fn opt_config ->
Keyword.update(opt_config, :doc, to_append, fn existing ->
existing <> " - " <> to_append
end)
end)
end
end