defmodule Maru.Params.Builder do
alias Maru.Params.{Runtime, TypeError}
defmacro __using__(_) do
quote do
import unquote(__MODULE__)
@params []
end
end
for {method, required} <- [{:optional, false}, {:requires, true}] do
defmacro unquote(method)(name, do: block) do
args = Macro.escape(%{__name__: name, __type__: Map, __required__: unquote(required)})
quote do
params = unquote(__MODULE__).pop_params(__ENV__)
unquote(block)
nested_params = unquote(__MODULE__).pop_params(__ENV__)
unquote(__MODULE__).put_params(params, __ENV__)
unquote(args)
|> Map.put(:children, nested_params)
|> build_param()
|> push_param(__ENV__)
end
end
defmacro unquote(method)(name, type) do
type = expand_alias(type, __CALLER__)
args = Macro.escape(%{__name__: name, __type__: type, __required__: unquote(required)})
quote do
unquote(args) |> build_param() |> push_param(__ENV__)
end
end
defmacro unquote(method)(name, type, do: block) do
type = expand_alias(type, __CALLER__)
args = Macro.escape(%{__name__: name, __type__: type, __required__: unquote(required)})
quote do
params = unquote(__MODULE__).pop_params(__ENV__)
unquote(block)
nested_params = unquote(__MODULE__).pop_params(__ENV__)
unquote(__MODULE__).put_params(params, __ENV__)
unquote(args)
|> Map.put(:children, nested_params)
|> build_param()
|> push_param(__ENV__)
end
end
defmacro unquote(method)(name, type, options) do
type = expand_alias(type, __CALLER__)
options =
case options do
[_ | _] -> options |> expand_alias(__CALLER__) |> Macro.escape()
{{:., _, _}, _, _} -> options
end
args = Macro.escape(%{__name__: name, __type__: type, __required__: unquote(required)})
quote do
unquote(args)
|> Map.merge(Map.new(unquote(options)))
|> build_param()
|> push_param(__ENV__)
end
end
defmacro unquote(method)(name, type, options, do: block) do
type = expand_alias(type, __CALLER__)
options = options |> expand_alias(__CALLER__) |> Map.new()
args =
%{__name__: name, __type__: type, __required__: unquote(required)}
|> Map.merge(options)
|> Macro.escape()
quote do
params = unquote(__MODULE__).pop_params(__ENV__)
unquote(block)
nested_params = unquote(__MODULE__).pop_params(__ENV__)
unquote(__MODULE__).put_params(params, __ENV__)
unquote(args)
|> Map.put(:children, nested_params)
|> build_param()
|> push_param(__ENV__)
end
end
end
def build_param(args) do
accumulator = %{
args: Map.put_new(args, :children, []),
info: [],
runtime:
quote do
%Runtime{}
end
}
[:name, :type, :blank_func, :children]
|> Enum.reduce(accumulator, &do_build_param/2)
|> Map.take([:runtime, :info])
end
defp do_build_param(:name, %{args: args, info: info, runtime: runtime}) do
name = Map.fetch!(args, :__name__)
source = Map.get(args, :source, name)
%{
args: args,
info: Keyword.put(info, :name, name),
runtime:
quote do
%{unquote(runtime) | name: unquote(name), source: unquote(source)}
end
}
end
defp do_build_param(:type, %{args: args, info: info, runtime: runtime}) do
parsers = args |> Map.get(:__type__) |> do_build_type()
with_children? = args |> Map.get(:children) |> length() > 0
nested =
parsers
|> List.last()
|> case do
{:module, Maru.Params.Types.Map} when with_children? -> :map
{:module, Maru.Params.Types.List} -> :list_of_map
{:list, _} -> :list_of_single
_ -> nil
end
func = do_build_parser(parsers, args)
%{
args: args,
info: info,
runtime:
quote do
%{unquote(runtime) | parser_func: unquote(func), nested: unquote(nested)}
end
}
end
defp do_build_param(:blank_func, %{args: args, info: info, runtime: runtime}) do
has_default? = args |> Map.has_key?(:default)
required? = args |> Map.fetch!(:__required__)
name = args |> Map.fetch!(:__name__)
keep_blank? = args |> Map.get(:keep_blank, false)
unpassed =
{has_default?, required?}
|> case do
{false, true} -> {:error, :parse, "required #{name}"}
{false, false} -> :ignore
{true, _} -> {:ok, args[:default]}
end
|> Macro.escape()
func =
if keep_blank? do
quote do
fn
{value, true} -> {:ok, value}
{_, false} -> unquote(unpassed)
end
end
else
quote do
fn {_, _} -> unquote(unpassed) end
end
end
%{
args: args,
info: info,
runtime:
quote do
%{unquote(runtime) | blank_func: unquote(func)}
end
}
end
defp do_build_param(:children, %{args: args, info: info, runtime: runtime}) do
children_runtime = args |> Map.get(:children) |> Enum.map(&Map.get(&1, :runtime))
%{
args: args,
info: info,
runtime:
quote do
%{unquote(runtime) | children: unquote(children_runtime)}
end
}
end
defp do_build_type({:fn, _, _} = func) do
[{:func, func}]
end
defp do_build_type({:&, _, _} = func) do
[{:func, func}]
end
defp do_build_type({:|>, _, [left, right]}) do
do_build_type(left) ++ do_build_type(right)
end
defp do_build_type({{:., _, [Access, :get]}, _, [List, nested]}) do
do_build_type(List) ++ [{:list, do_build_type(nested)}]
end
defp do_build_type(type) do
module = Module.concat(Maru.Params.Types, type)
module.__info__(:functions)
[{:module, module}]
rescue
UndefinedFunctionError ->
raise TypeError, type: type, reason: "Undefined Type"
end
def do_build_parser(parsers, args) do
value = quote do: value
options = quote do: options
block =
Enum.reduce(parsers, value, fn
{:list, nested}, ast ->
nested_ast = do_build_parser(nested, args)
quote do
case unquote(ast) do
{:ok, value} ->
value
|> Enum.map(fn item -> {:ok, item} end)
|> Enum.map(fn ok_item -> unquote(nested_ast).(ok_item, unquote(options)) end)
|> then(fn value -> {:ok, value} end)
error ->
error
end
end
{:func, func}, ast ->
quote do
case unquote(ast) do
{:ok, value} -> unquote(func).(value)
error -> error
end
end
{:module, module}, ast ->
parser_args =
args
|> Map.take(module.parser_arguments())
|> Macro.escape()
validator_args =
args
|> Map.take(module.validator_arguments())
|> Enum.map(&List.wrap/1)
quote do
case unquote(ast) do
{:ok, value} ->
options =
unquote(parser_args)
|> Map.get(:options, [])
|> Keyword.merge(unquote(options))
args =
Enum.reduce(
unquote(validator_args),
unquote(module).parse(
unquote(ast),
Map.put(unquote(parser_args), :options, options)
),
fn
validator_arg, {:ok, parsed} ->
unquote(module).validate(parsed, validator_arg)
_validator_arg, error ->
error
end
)
error ->
error
end
end
end)
quote do
fn unquote(value), unquote(options) ->
unquote(block)
end
end
end
def push_param(param, %Macro.Env{module: module}) do
params = Module.get_attribute(module, :params)
Module.put_attribute(module, :params, params ++ List.wrap(param))
end
def put_params(params, %Macro.Env{module: module}) do
Module.put_attribute(module, :params, params)
end
def pop_params(%Macro.Env{module: module}) do
params = Module.get_attribute(module, :params)
Module.put_attribute(module, :params, [])
params
end
def expand_alias(ast, caller) do
Macro.prewalk(ast, fn
{:__aliases__, _, _} = module -> Macro.expand(module, caller)
other -> other
end)
end
end