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, fn path_iodata -> case IO.iodata_to_binary(path_iodata) do "./" <> rest -> Path.join(File.cwd!(), rest) path -> path end end) rescue _ in ArgumentError -> raise_with("`#{key}` option must be a path", opts[key], context) 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 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