defmodule Membrane.Core.OptionsSpecs do @moduledoc false use Bunch alias Bunch.{KVEnum, Markdown} alias Membrane.Pad alias Membrane.Time @default_types_params %{ atom: [spec: quote_expr(atom)], boolean: [spec: quote_expr(boolean)], string: [spec: quote_expr(String.t())], keyword: [spec: quote_expr(keyword)], struct: [spec: quote_expr(struct)], caps: [spec: quote_expr(struct)], time: [spec: quote_expr(Time.t()), inspector: &Time.to_code_str/1] } @spec options_doc() :: String.t() def options_doc do """ Options are defined by a keyword list, where each key is an option name and is described by another keyword list with following fields: * `type:` atom, used for parsing * `spec:` typespec for value in struct. If ommitted, for types: `#{inspect(Map.keys(@default_types_params))}` the default typespec is provided, for others typespec is set to `t:any/0` * `default:` default value for option. If not present, value for this option will have to be provided each time options struct is created * `inspector:` function converting fields' value to a string. Used when creating documentation instead of `inspect/1` * `description:` string describing an option. It will be used for generating the docs """ end @spec def_options(module(), nil | Keyword.t(), :element | :bin) :: Macro.t() def def_options(module, options, child) do {opt_typespecs, escaped_opts} = parse_opts(options) typedoc = generate_opts_doc(escaped_opts) quote do @typedoc """ Struct containing options for `#{inspect(__MODULE__)}` """ @type t :: %unquote(module){unquote_splicing(opt_typespecs)} @membrane_options_moduledoc """ ## #{unquote(child) |> to_string |> String.capitalize()} options Passed via struct `t:#{inspect(__MODULE__)}.t/0` #{unquote(typedoc)} """ @doc """ Returns description of options available for this module """ @spec options() :: keyword def options(), do: unquote(escaped_opts) @enforce_keys unquote(escaped_opts) |> Enum.reject(fn {k, v} -> v |> Keyword.has_key?(:default) end) |> Keyword.keys() defstruct unquote(escaped_opts) |> Enum.map(fn {k, v} -> {k, v[:default]} end) end end @spec def_pad_options(Pad.name_t(), nil | Keyword.t()) :: {Macro.t(), Macro.t()} def def_pad_options(_pad_name, nil) do no_code = quote do end {nil, no_code} end def def_pad_options(pad_name, options) do {opt_typespecs, escaped_opts} = parse_opts(options) pad_opts_type_name = "#{pad_name}_pad_opts_t" |> String.to_atom() type_definiton = quote do @typedoc """ Options for pad `#{inspect(unquote(pad_name))}` """ @type unquote(Macro.var(pad_opts_type_name, nil)) :: unquote(opt_typespecs) end {escaped_opts, type_definiton} end @spec generate_opts_doc(escaped_opts :: Keyword.t()) :: Macro.t() def generate_opts_doc(escaped_opts) do escaped_opts |> Enum.map(&generate_opt_doc/1) |> Enum.reduce(fn opt_doc, acc -> quote do """ #{unquote(acc)} #{unquote(opt_doc)} """ end end) end defp generate_opt_doc({opt_name, opt_definition}) do header = "`#{Atom.to_string(opt_name)}`" spec = """ ``` #{Keyword.fetch!(opt_definition, :spec)} ``` """ default_value_doc = if Keyword.has_key?(opt_definition, :default) do inspector = opt_definition |> Keyword.get( :inspector, @default_types_params[opt_definition[:type]][:inspector] || quote(do: &inspect/1) ) quote do "Default value: `#{unquote(inspector).(unquote(opt_definition)[:default])}`" end else quote_expr("***Required***") end description = Keyword.get(opt_definition, :description, "") quote do description = unquote(description) |> String.trim() doc_parts = [unquote_splicing([spec, default_value_doc]), description] """ - #{unquote(header)} \n #{doc_parts |> Enum.map_join(" \n", &Markdown.indent/1)} """ end end defp parse_opts(opts) when is_list(opts) do opts = KVEnum.map_values(opts, fn definition -> default_spec = @default_types_params[definition[:type]][:spec] || quote_expr(any) Keyword.put_new(definition, :spec, default_spec) end) opts_typespecs = KVEnum.map_values(opts, & &1[:spec]) escaped_opts = KVEnum.map_values(opts, fn definition -> Keyword.update!(definition, :spec, &Macro.to_string/1) end) {opts_typespecs, escaped_opts} end end