defmodule Zig.Options do
@moduledoc false
# this module provides common mechanisms for parsing options.
@type context :: %{
module: module(),
file: Path.t(),
line: pos_integer(),
keystack: [atom()],
cleanup: bool,
otp_app: atom
}
# context operations
def initialize_context(caller, otp_app) do
caller
|> Map.take(~w[module file line]a)
|> Map.merge(%{keystack: [], cleanup: true, otp_app: otp_app})
end
def push_key(context, key) when is_atom(key) or is_binary(key) or is_integer(key),
do: Map.update!(context, :keystack, &[key | &1])
# normalization and validation
def normalize_kw(opts, key, default \\ nil, callback, context) do
Keyword.update(opts, key, default, &callback.(&1, push_key(context, key)))
end
def normalize_path(opts, key, context) do
Keyword.update(opts, key, nil, &Zig._normalize_path(&1, Path.dirname(context.file)))
rescue
_ ->
raise_with("must be a path", opts[key], push_key(context, key))
end
def boolean_normalizer([{key, value}]) when is_atom(key) and is_boolean(value),
do: fn
{^key}, _context ->
{:ok, value}
{_}, _context ->
:error
value, _context when is_boolean(value) ->
value
other, context ->
raise_with("must be boolean", other, context)
end
def struct_normalizer(module), do: &module.new/2
def normalize(opts, key, fun, context) do
Enum.map(opts, fn
{^key, value} ->
{key, fun.(value, push_key(context, key))}
{_other, _} = kv ->
kv
atom when is_atom(atom) ->
case fun.({atom}, push_key(context, key)) do
{:ok, value} -> {key, value}
:error -> atom
end
end)
rescue
_ in FunctionClauseError ->
raise_with("must be a list of `{atom, term}` or `atom`", opts, context)
end
def scrub_non_keyword(opts, context) do
Enum.map(opts, fn
{key, _} = kv when is_atom(key) ->
kv
other ->
raise_with(
"found an invalid term in the options list",
other,
context
)
end)
end
def validate(opts, key, :boolean, context) do
do_validate(opts, key, &is_boolean/1, "must be a boolean", context)
end
def validate(opts, key, :atom, context) do
do_validate(opts, key, &is_atom/1, "must be an atom", context)
end
def validate(opts, key, {:atom, substitute}, context) do
do_validate(opts, key, &is_atom/1, "must be #{substitute}", context)
end
def validate(opts, key, fun, context) when is_function(fun, 1) do
do_validate(opts, key, fun, "", context)
end
defp do_validate(opts, key, fun, message, context) do
case fetch_and_run(opts, key, fun) do
true ->
opts
false ->
raise_with(message, opts[key], push_key(context, key))
:ok ->
opts
{:error, substitute_message} ->
raise_with(substitute_message, push_key(context, key))
{:error, substitute_message, content} ->
raise_with(substitute_message, content, push_key(context, key))
end
end
def fetch_and_run(opts, key, fun) do
case Keyword.fetch(opts, key) do
{:ok, value} ->
fun.(value)
:error ->
:ok
end
end
@spec raise_with(String.t(), term, term) :: no_return
def raise_with(message, content \\ nil, context) do
message =
case content do
{:tag, label, content} ->
"#{message}, got: `#{inspect(content)}` for #{label}"
nil ->
message
content ->
"#{message}, got: `#{inspect(content)}`"
end
raise CompileError,
description: make_message(context.keystack, message),
file: context.file,
line: context.line
end
defp make_message([], stem), do: stem
defp make_message(keystack, stem) do
keystack_str =
keystack
|> List.wrap()
|> Enum.reverse()
|> Enum.map_join(" > ", &to_string/1)
"option `#{keystack_str}` #{stem}"
end
def list_of(members) do
Enum.map_join(members, ", ", &"`#{inspect(&1)}`")
end
end