Packages

spark

2.3.10
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 docs.ex
Raw

lib/spark/options/docs.ex

# SPDX-FileCopyrightText: 2022 spark contributors <https://github.com/ash-project/spark/graphs.contributors>
#
# SPDX-License-Identifier: MIT
defmodule Spark.Options.Docs do
@moduledoc false
def generate(schema, options) when is_list(schema) and is_list(options) do
nest_level = Keyword.get(options, :nest_level, 0)
{docs, sections, _level} = build_docs(schema, {[], [], nest_level})
to_string([Enum.reverse(docs), Enum.reverse(sections)])
end
# If the schema is a function, we want to not show anything (it's a recursive
# function) and "back up" one level since when we got here we already
# increased the level by one.
defp build_docs(fun, {docs, sections, level}) when is_function(fun) do
{docs, sections, level - 1}
end
defp build_docs(schema, {docs, sections, level} = acc) do
if schema[:*] do
build_docs(schema[:*][:keys], acc)
else
Enum.reduce(schema || [], {docs, sections, level}, &maybe_option_doc/2)
end
end
defp build_docs_with_subsection(subsection, schema, {docs, sections, level}) do
subsection = String.trim_trailing(subsection, "\n") <> "\n\n"
{item_docs, sections, _level} = build_docs(schema, {[], sections, 0})
item_section = [subsection | Enum.reverse(item_docs)]
{docs, [item_section | sections], level}
end
defp maybe_option_doc({key, schema}, acc) do
if schema[:doc] == false do
acc
else
option_doc({key, schema}, acc)
end
end
defp option_doc({key, schema}, {docs, sections, level}) do
type_str = if type_str = get_type_str(schema), do: " #{type_str}"
desc_indent = String.duplicate(" ", level + 1)
description =
get_required_str(schema)
|> get_doc_str(schema)
|> get_default_str(schema)
|> case do
nil -> ""
parts -> indent_doc(" - " <> parts, desc_indent)
end
doc = [String.duplicate(" ", level), "* `#{inspect(key)}`#{type_str}", description, "\n\n"]
docs = [IO.iodata_to_binary(doc) | docs]
cond do
schema[:keys] && schema[:subsection] ->
build_docs_with_subsection(schema[:subsection], schema[:keys], {docs, sections, level})
schema[:keys] ->
{docs, sections, _level} = build_docs(schema[:keys], {docs, sections, level + 1})
{docs, sections, level}
true ->
{docs, sections, level}
end
end
defp space_concat(left, nil), do: left
defp space_concat(nil, right), do: right
defp space_concat(left, right), do: left <> " " <> right
defp get_required_str(schema) do
if schema[:required], do: "Required."
end
defp get_doc_str(prev_str, schema) do
space_concat(prev_str, schema[:doc] && String.trim(schema[:doc]))
end
defp get_default_str(prev_str, schema) do
if Keyword.has_key?(schema, :default) do
default_str = "The default value is `#{inspect(schema[:default])}`."
# If the documentation contains multiple lines,
# the default must be in a trailing line.
if prev_str && String.contains?(prev_str, ["\r\n", "\n\n"]) do
prev_str <> "\n\n" <> default_str
else
space_concat(prev_str, default_str)
end
else
prev_str
end
end
defp get_type_str(schema) do
str =
case Keyword.fetch(schema, :type_doc) do
{:ok, false} -> nil
{:ok, type_doc} when is_binary(type_doc) -> type_doc
:error -> get_raw_type_str(schema[:type])
end
if str do
"(#{str})"
end
end
# Only shows types when they are concise.
defp get_raw_type_str(nil), do: nil
defp get_raw_type_str({:custom, _mod, _fun, _args}), do: nil
defp get_raw_type_str(:mfa), do: nil
defp get_raw_type_str(:mod_arg), do: nil
defp get_raw_type_str(:module), do: "`t:module/0`"
defp get_raw_type_str({:or, _values}), do: nil
defp get_raw_type_str({:in, _}), do: nil
defp get_raw_type_str({:one_of, _}), do: nil
defp get_raw_type_str({:fun, arity}) when is_integer(arity), do: "function of arity #{arity}"
defp get_raw_type_str(:any), do: "`t:term/0`"
defp get_raw_type_str(:reference), do: "`t:reference/0`"
defp get_raw_type_str(:pid), do: "`t:pid/0`"
defp get_raw_type_str(:timeout), do: "`t:timeout/0`"
defp get_raw_type_str(:boolean), do: "`t:boolean/0`"
defp get_raw_type_str(:atom), do: "`t:atom/0`"
defp get_raw_type_str(:integer), do: "`t:integer/0`"
defp get_raw_type_str(:regex_as_mfa), do: "`t:Regex.t/0`"
defp get_raw_type_str(:non_neg_integer), do: "`t:non_neg_integer/0`"
defp get_raw_type_str(:pos_integer), do: "`t:pos_integer/0`"
defp get_raw_type_str(:float), do: "`t:float/0`"
defp get_raw_type_str(:number), do: "`t:number/0`"
defp get_raw_type_str(:string), do: "`t:String.t/0`"
defp get_raw_type_str(:keyword_list), do: "`t:keyword/0`"
defp get_raw_type_str(:non_empty_keyword_list), do: "non-empty `t:keyword/0`"
defp get_raw_type_str({:map, _keys}), do: "`t:map/0`"
defp get_raw_type_str({:keyword_list, _keys}), do: "`t:keyword/0`"
defp get_raw_type_str({:non_empty_keyword_list, _keys}), do: "non-empty `t:keyword/0`"
defp get_raw_type_str(:map), do: "`t:map/0`"
defp get_raw_type_str({:tagged_tuple, tag, tuple_type}) do
get_raw_type_str({:tuple, [{:literal, tag}, tuple_type]})
end
defp get_raw_type_str({:spark_behaviour, module}),
do: "module that adopts `#{inspect(module)}` or a module and options"
defp get_raw_type_str({:spark_behaviour, module, _}),
do: "module that adopts `#{inspect(module)}` or a module and options"
defp get_raw_type_str({:spark_function_behaviour, module, {_, integer}}),
do:
"module that adopts `#{inspect(module)}`, a module and options, or a function of arity #{integer}"
defp get_raw_type_str({:spark_function_behaviour, module, _, {_, integer}}),
do:
"module that adopts `#{inspect(module)}`, a module and options, or a function of arity #{integer}"
defp get_raw_type_str({:behaviour, module}), do: "module that adopts `#{inspect(module)}`"
defp get_raw_type_str({:protocol, module}),
do: "value that implements the `#{inspect(module)}` protocol"
defp get_raw_type_str({:impl, module}),
do: "module for which the `#{inspect(module)}` protocol is implemented"
defp get_raw_type_str({:struct, struct_type}), do: "struct of type `#{inspect(struct_type)}`"
defp get_raw_type_str(:struct), do: "struct"
defp get_raw_type_str({:spark, module}), do: "`#{inspect(module)}`"
defp get_raw_type_str({:mfa_or_fun, arity}), do: "mfa or function of arity #{arity}"
defp get_raw_type_str({:spark_type, module, _}), do: "an `#{inspect(module)}`"
defp get_raw_type_str({:spark_type, module, _, _}), do: "an `#{inspect(module)}`"
defp get_raw_type_str(:fun), do: "function"
defp get_raw_type_str({:fun, _}), do: "function"
defp get_raw_type_str({:fun, _, _}), do: "function"
defp get_raw_type_str({:wrap_list, subtype}) do
if subtype_str = get_raw_type_str(subtype), do: "one or a list of #{subtype_str}"
end
defp get_raw_type_str(:literal), do: nil
defp get_raw_type_str({:literal, value}), do: "`#{inspect(value)}`"
defp get_raw_type_str(:quoted), do: "a quoted value"
defp get_raw_type_str({:list, subtype}) do
if subtype_str = get_raw_type_str(subtype), do: "list of #{subtype_str}"
end
defp get_raw_type_str({:map, key_type, value_type}) do
with key_type_str when is_binary(key_type_str) <- get_raw_type_str(key_type),
value_type_str when is_binary(value_type_str) <- get_raw_type_str(value_type) do
"map of #{key_type_str} keys and #{value_type_str} values"
end
end
defp get_raw_type_str({:tuple, value_types}) do
value_types =
value_types
|> Enum.map_join(", ", &get_raw_type_str/1)
"tuple of #{value_types} values"
end
def dsl_docs_type({:behaviour, _mod}), do: "module"
def dsl_docs_type({:protocol, protocol}), do: "an `#{inspect(protocol)}` value"
def dsl_docs_type({:impl, protocol}), do: "a module implementing `#{inspect(protocol)}`"
def dsl_docs_type({:spark, mod}), do: dsl_docs_type({:behaviour, mod})
def dsl_docs_type({:spark_behaviour, mod}), do: dsl_docs_type({:behaviour, mod})
def dsl_docs_type({:spark_behaviour, mod, _builtins}),
do: dsl_docs_type({:behaviour, inspect(mod)})
def dsl_docs_type(:module), do: "module"
def dsl_docs_type({:spark_function_behaviour, mod, {_, arity}}) do
dsl_docs_type({:or, [{:fun, arity}, {:spark_behaviour, mod}]})
end
def dsl_docs_type({:spark_function_behaviour, mod, _builtins, {_, arity}}) do
dsl_docs_type({:or, [{:fun, arity}, {:spark_behaviour, mod}]})
end
def dsl_docs_type({:custom, _, _, _}), do: "any"
def dsl_docs_type(:any), do: "any"
def dsl_docs_type(:keyword_list), do: "keyword"
def dsl_docs_type(:non_empty_keyword_list), do: "keyword"
def dsl_docs_type({keyword_type, [*: value]})
when keyword_type in [:keyword_list, :non_empty_keyword_list] and is_list(value),
do: "keyword(#{dsl_docs_type(value[:type])})"
def dsl_docs_type({keyword_type, schema})
when keyword_type in [:keyword_list, :non_empty_keyword_list] and is_list(schema) do
options =
Enum.map_join(schema, ", ", fn {key, value} ->
"#{key}: #{dsl_docs_type(value[:type])}"
end)
"[#{options}]"
end
def dsl_docs_type(:atom), do: "atom"
def dsl_docs_type(:string), do: "String.t"
def dsl_docs_type(:boolean), do: "boolean"
def dsl_docs_type(:integer), do: "integer"
def dsl_docs_type(:non_neg_integer), do: "non_neg_integer"
def dsl_docs_type(:pos_integer), do: "pos_integer"
def dsl_docs_type(:float), do: "float"
def dsl_docs_type(:number), do: "number"
def dsl_docs_type(:timeout), do: "timeout"
def dsl_docs_type(:pid), do: "pid"
def dsl_docs_type(:mfa), do: "mfa"
def dsl_docs_type(:mod_arg), do: dsl_docs_type({:tuple, [:module, {:list, :any}]})
def dsl_docs_type({:tuple, list}) when is_list(list) do
tuple = Enum.map_join(list, ", ", &dsl_docs_type/1)
"{#{tuple}}"
end
def dsl_docs_type(:map), do: "map"
def dsl_docs_type({:map, key_type, value_type}),
do: "%{optional(#{dsl_docs_type(key_type)}) => #{dsl_docs_type(value_type)}}"
def dsl_docs_type({:map, schema}) when is_list(schema) do
options =
Enum.map_join(schema, ", ", fn {key, value} ->
"#{if value[:required], do: "required", else: "optional"}(:#{key}) => #{dsl_docs_type(value[:type])}"
end)
"%{#{options}}"
end
def dsl_docs_type(:struct), do: "struct"
def dsl_docs_type({:struct, struct}), do: "#{inspect(struct)}"
def dsl_docs_type(nil), do: "nil"
def dsl_docs_type({:fun, 0}), do: "(-> any)"
def dsl_docs_type({:fun, arity}) when is_integer(arity) do
args =
Enum.map_join(0..(arity - 1), ", ", fn _ -> "any" end)
"(#{args} -> any)"
end
def dsl_docs_type({:fun, args}) when is_list(args) do
args =
Enum.map_join(args, ", ", &dsl_docs_type/1)
"(#{args} -> any)"
end
def dsl_docs_type({:fun, args, return}) when is_list(args) do
args =
Enum.map_join(args, ", ", &dsl_docs_type/1)
"(#{args} -> #{dsl_docs_type(return)})"
end
def dsl_docs_type({:one_of, values}), do: dsl_docs_type({:in, values})
def dsl_docs_type({:in, values}), do: Enum.map_join(values, " | ", &inspect/1)
def dsl_docs_type({:or, subtypes}),
do: subtypes |> Enum.map(&dsl_docs_type/1) |> Enum.uniq() |> Enum.join(" | ")
def dsl_docs_type({:list, subtype}), do: "list(#{dsl_docs_type(subtype)})"
def dsl_docs_type(:quoted), do: "any"
def dsl_docs_type({:wrap_list, subtype}) do
str = dsl_docs_type(subtype)
"#{str} | list(#{str})"
end
def dsl_docs_type({:list_of, subtype}), do: dsl_docs_type({:list, subtype})
def dsl_docs_type({:mfa_or_fun, arity}), do: dsl_docs_type({:or, [{:fun, arity}, :mfa]})
def dsl_docs_type(:literal), do: "any"
def dsl_docs_type({:literal, value}), do: inspect(value)
def dsl_docs_type({:tagged_tuple, tag, type}),
do: dsl_docs_type({:tuple, [{:literal, tag}, type]})
def dsl_docs_type({:spark_type, type, _}), do: dsl_docs_type({:behaviour, type})
def dsl_docs_type({:spark_type, type, _, _}), do: dsl_docs_type({:behaviour, type})
defp indent_doc(text, indent) do
[head | tail] = String.split(text, ["\r\n", "\n"])
tail =
Enum.map(tail, fn
"" -> "\n"
str -> [?\n, indent, str]
end)
[head | tail]
end
def schema_specs(schema, or_nil? \\ false) do
Enum.map(schema, fn {key, opt_schema} ->
typespec =
Keyword.get_lazy(opt_schema, :type_spec, fn -> type_to_spec(opt_schema[:type]) end)
if or_nil? && !opt_schema[:required] && is_nil(opt_schema[:default]) do
quote do: {unquote(key), unquote(typespec) | nil}
else
quote do: {unquote(key), unquote(typespec)}
end
end)
end
def schema_to_spec(schema) do
schema
|> schema_specs()
|> unionize_quoted()
end
defp type_to_spec(type) do
case type do
:any ->
quote(do: term())
:integer ->
quote(do: integer())
:non_neg_integer ->
quote(do: non_neg_integer())
:pos_integer ->
quote(do: pos_integer())
:atom ->
quote(do: atom())
:float ->
quote(do: float())
:boolean ->
quote(do: boolean())
:pid ->
quote(do: pid())
:reference ->
quote(do: reference())
:timeout ->
quote(do: timeout())
:string ->
quote(do: String.t())
:mfa ->
quote(do: {module(), atom(), [term()]})
:mod_arg ->
quote(do: {module(), [term()]})
:keyword_list ->
quote(do: keyword())
{:keyword_list, _keys} ->
quote(do: keyword())
:non_empty_keyword_list ->
quote(do: keyword())
{:non_empty_keyword_list, _keys} ->
quote(do: keyword())
nil ->
quote(do: nil)
:regex_as_mfa ->
quote(do: Regex.t())
:map ->
quote(do: map())
{:map, key_type, value_type} ->
quote(
do: %{optional(unquote(type_to_spec(key_type))) => unquote(type_to_spec(value_type))}
)
:fun ->
quote do: (... -> term())
{:fun, arity} when is_integer(arity) ->
function_spec(arity)
{:fun, arg_types} ->
function_spec(arg_types)
{:fun, arg_types, return_type} ->
function_spec(arg_types, return_type)
{:in, %Range{first: first, last: last} = range} ->
if Map.get(range, :step) in [nil, 1] do
quote(do: unquote(first)..unquote(last))
else
quote(do: term())
end
{one_of, choices} when one_of in [:in, :one_of] ->
if !Enum.empty?(choices) && Enum.all?(choices, &is_atom(&1)) do
unionize_quoted(choices)
else
quote(do: term())
end
{:custom, _mod, _fun, _args} ->
quote(do: term())
{:list, subtype} ->
quote(do: [unquote(type_to_spec(subtype))])
{:or, subtypes} ->
subtypes |> Enum.map(&type_to_spec/1) |> unionize_quoted()
{:struct, _struct_name} ->
quote(do: struct())
:struct ->
quote(do: struct())
{:tuple, tuple_types} ->
case Enum.map(tuple_types, &type_to_spec/1) do
[type1, type2] -> quote(do: {unquote(type1), unquote(type2)})
types -> quote do: {unquote_splicing(types)}
end
{:tagged_tuple, tag, subtype} ->
quote do: {unquote(tag), unquote(type_to_spec(subtype))}
{tag, _} when tag in [:behaviour, :spark_behaviour, :protocol, :spark, :impl] ->
quote(do: module())
{:spark_function_behaviour, _, _} ->
quote(do: module() | {module(), Keyword.t()})
{:spark_function_behaviour, _, _, _} ->
quote(do: module() | {module(), Keyword.t()})
{:spark_type, _, _} ->
quote(do: module)
{:spark_type, _, _, _} ->
quote(do: module)
{:mfa_or_fun, arity} ->
quote do: unquote(type_to_spec(:mfa)) | unquote(type_to_spec({:fun, arity}))
{:struct, module} ->
quote do: unquote(module).t()
{:wrap_list, type} ->
quote do: unquote(type_to_spec(type)) | [unquote(type_to_spec(type))]
:literal ->
quote do: term()
{:literal, atom} when is_atom(atom) ->
atom
{:literal, _} ->
quote do: term()
:quoted ->
quote do: Macro.t()
end
end
defp function_spec(arity) when is_integer(arity) do
args = List.duplicate(quote(do: term()), arity)
quote do
unquote_splicing(args) -> term()
end
end
defp function_spec(arg_types) when is_list(arg_types) do
arg_types = arg_types |> Enum.map(&type_to_spec/1)
quote do
unquote_splicing(arg_types) -> term()
end
end
defp function_spec(arg_types, return_type) when is_list(arg_types) do
arg_types = arg_types |> Enum.map(&type_to_spec/1)
return_type = type_to_spec(return_type)
quote do
unquote_splicing(arg_types) -> unquote(return_type)
end
end
defp unionize_quoted(specs) do
specs
|> Enum.reverse()
|> Enum.uniq_by(&remove_meta/1)
|> Enum.reduce(&quote(do: unquote(&1) | unquote(&2)))
end
defp remove_meta(code) do
Macro.prewalk(code, fn
{a, _b, c} ->
{a, [], c}
other ->
other
end)
end
end