Current section

Files

Jump to
argx lib core matcher.ex
Raw

lib/core/matcher.ex

defmodule Argx.Matcher do
@moduledoc false
import Argx.Error
alias Argx.Const
alias Argx.Matcher.Helper
@init_errors []
@should_drop_flag Const.should_drop_flag()
@check_types Const.check_types()
@value_key Const.value_key()
def match(from) do
fn args, configs, curr_m ->
with traverse_via_depth_first <- traverse(from, args, configs),
{root, path} <- {[], []},
{errors, new_args} <- traverse_via_depth_first.(root, @init_errors, path, curr_m),
args <- Helper.keep_checkbox_radio(new_args, configs),
new_errors <- Helper.collect_errors(args, configs, errors) do
{new_errors, new_args}
end
end
end
###
def traverse(_from, [] = _args, [] = _configs) do
fn root, errors, _path, _curr_m ->
{errors, Enum.reverse(root)}
end
end
def traverse(from, [{arg_name, _arg_value} = arg | args_rest], [config | configs_rest]) do
fn root, errors, path, curr_m ->
with arg <- Helper.pre_process_args(arg, config, curr_m),
errors <- Helper.collect_errors(arg, config, path, errors),
drill_down <- drill_down(from, arg, config),
new_path <- Helper.append_path(path, arg_name),
{errors, root} <- drill_down.(root, errors, @should_drop_flag, new_path, curr_m),
traverse <- traverse(from, args_rest, configs_rest) do
traverse.(root, errors, path, curr_m)
end
end
end
###
defp drill_down(
_from,
arg,
{_arg_name2, %Argx.Config{type: type, nested: nil}}
)
when type in [:list, :map] do
fn root, errors, _should_drop_flag, _path, _curr_m ->
{errors, [arg | root]}
end
end
defp drill_down(
from,
{arg_name, arg_value},
{_arg_name2, %Argx.Config{type: :list, nested: nested_configs}}
)
when is_list(arg_value) do
fn root, errors, _should_drop_flag, path, curr_m ->
with value_type <- Helper.get_value_type_by_configs(nested_configs, @value_key),
worker <- traverse_by_list(from, arg_value, nested_configs) do
worker.(root, errors, path, curr_m, arg_name, value_type)
end
end
end
defp drill_down(
from,
{arg_name, arg_value},
{_arg_name2, %Argx.Config{type: :map, nested: nested_configs}}
)
when is_map(arg_value) do
fn root, errors, _should_drop_flag, path, curr_m ->
with worker <- traverse_by_map(from, arg_value, nested_configs) do
worker.(root, errors, path, curr_m, arg_name, :map)
end
end
end
defp drill_down(from, {_arg_name, arg_value} = arg, _config) do
fn root, errors, should_drop_flag, _path, _curr_m ->
with :argx <- from,
true <- should_drop_flag == arg_value do
{errors, root}
else
_ -> {errors, [arg | root]}
end
end
end
### Reenter Traverse Procedure
defp traverse_by_map(from, list, configs) do
fn root, errors, path, curr_m, parent, value_type ->
with worker <- reenter(from, list, configs),
{new_errors, map} <- worker.(path, curr_m, nil),
errors <- merge_errors(errors, new_errors, @check_types),
map <- Helper.return_child(map, value_type),
root <- Keyword.put(root, parent, map) do
{errors, root}
end
end
end
defp traverse_by_list(from, list, configs) do
fn root, errors, path, curr_m, parent, value_type ->
with list <- Helper.build_list(list, value_type, @value_key),
new_list <- [],
line_num <- 1,
worker <- do_traverse_by_list(from, list, configs),
{errors, list} <-
worker.(new_list, errors, path, curr_m, parent, line_num),
list <- Helper.return_child(list, value_type),
root <- Keyword.put(root, parent, list) do
{errors, root}
end
end
end
defp do_traverse_by_list(_from, [] = _list, %{} = configs) when map_size(configs) > 0 do
fn new_list, errors, path, _curr_m, _parent, _line_num ->
with lacked_keys <- Helper.get_required_key(configs, @value_key),
new_errors <- reduce_errors(errors, lacked_keys, path, &Helper.join_path/2, :lacked) do
{new_errors, new_list}
end
end
end
defp do_traverse_by_list(_from, [] = _list, _configs) do
fn new_list, errors, _path, _curr_m, _parent, _line_num ->
{errors, new_list}
end
end
defp do_traverse_by_list(from, [args | rest_args] = _list, configs) do
fn new_list, errors, path, curr_m, parent, line_num ->
with %{} <- args,
reenter <- reenter(from, args, configs),
result <- reenter.(path, curr_m, line_num),
continue <- continue(from, rest_args, configs) do
continue.(new_list, errors, path, curr_m, parent, line_num, result)
else
_ ->
{errors, _, _} = reduce_errors(errors, nil, path, &Helper.join_path/2, :error_type)
{errors, new_list}
end
end
end
###
defp reenter(from, args, configs) do
fn path, curr_m, line_num ->
with {args, configs} <- Helper.pre_args_configs(args, configs),
path <- Helper.append_path(path, line_num),
traverse <- traverse(from, args, configs),
new_list <- [] do
traverse.(new_list, @init_errors, path, curr_m)
end
end
end
defp continue(from, [_ | _] = rest, configs) do
fn new_list, errors, path, curr_m, parent, line_num, result ->
with {new_errors, args} <- result,
line_num <- line_num + 1,
errors <- merge_errors(errors, new_errors, @check_types),
list <- Helper.reduce_list(args, new_list),
worker <- do_traverse_by_list(from, rest, configs) do
worker.(list, errors, path, curr_m, parent, line_num)
end
end
end
defp continue(_from, [] = _rest, _configs) do
fn new_list, errors, _path, _curr_m, _parent, _line_num, result ->
with {new_errors, args} <- result,
errors <- merge_errors(errors, new_errors, @check_types),
list <- Helper.reduce_list(args, new_list) do
{errors, list}
end
end
end
end
defmodule Argx.Matcher.Helper do
@moduledoc false
alias Argx.{Checker, Converter, Const, Defaulter, Util}
@should_drop_flag Const.should_drop_flag()
@value_key Const.value_key()
@path_sep ":"
def pre_process_args(arg, config, curr_m) do
arg
|> Defaulter.set_default(config, curr_m)
|> Converter.convert(config)
end
def get_required_key(configs, value_key) do
configs
|> to_keyword()
|> Enum.filter(fn {field, config} ->
config.optional == false and field != value_key
end)
|> Keyword.keys()
end
def get_value_type_by_configs(configs, value_key) do
with [key | []] <- :maps.keys(configs),
true <- key == value_key do
:value
else
_ -> :list
end
end
def build_list(list, :value, value_key) when is_list(list) do
Enum.map(list, fn term ->
%{value_key => term}
end)
end
def build_list(list, :list, _value_key), do: list
def build_list(other, _value_type, _value_key), do: other
def return_child([_ | _] = list, :value) do
list
|> Enum.map(fn term ->
%{_: value} = term
value
end)
|> Enum.reverse()
end
def return_child([_ | _] = list, :list), do: Enum.reverse(list)
def return_child([_ | _] = map, :map), do: to_map(map)
def return_child(other_list, _value_type), do: other_list
def reduce_list(args, new_list) when is_list(args) and is_list(new_list) do
[to_map(args) | new_list]
end
def reduce_list(_other_args, new_list) when is_list(new_list), do: new_list
### Path
def join_path(key, path, sep \\ @path_sep, value_key \\ @value_key)
def join_path(key, [] = _path, _sep, _value_key) when is_atom(key), do: key
def join_path(nil = _key, [path | []], _sep, _value_key), do: path
def join_path(nil = _key, [_ | _] = path, sep, _value_key), do: path |> Enum.join(sep)
def join_path(key, [_ | _] = path, sep, _value_key) do
path |> append_path(key) |> Enum.join(sep)
end
def append_path(path, term, value_key \\ @value_key)
def append_path(path, nil = _term, _value_key) when is_list(path), do: path
def append_path(path, term, value_key) when is_list(path) and term == value_key, do: path
def append_path(path, term, _value_key) when is_list(path) and is_atom(term), do: path ++ [term]
def append_path(path, num, _value_key) when is_list(path) and is_integer(num) do
path ++ ["#{num}"]
end
### Preprocess
def pre_args_configs(args, configs) when is_map(args) or is_map(configs) do
args
|> Util.to_atom_key()
|> to_keyword()
|> pre_args_configs(to_keyword(configs))
end
def pre_args_configs(args, configs) when is_list(args) and is_list(configs) do
args =
configs
|> Keyword.keys()
|> Enum.map(fn key ->
value = Keyword.get(args, key, @should_drop_flag)
{key, value}
end)
{args, configs}
end
def sort_by_keys(keyword, keys, should_drop_flag) when is_list(keyword) do
keyword |> to_map() |> sort_by_keys(keys, should_drop_flag)
end
def sort_by_keys(%{} = map, keys, should_drop_flag) do
keys
|> Enum.reduce([], fn key, keyword ->
value = :maps.get(key, map, should_drop_flag)
(value && [{key, value} | keyword]) || keyword
end)
|> Enum.reverse()
end
def to_map(%{} = term), do: term
def to_map(term) when is_list(term), do: Map.new(term)
def to_map(other), do: other
def to_keyword(%{} = term), do: Keyword.new(term)
def to_keyword(term) when is_list(term), do: term
def to_keyword(other), do: other
###
def get_type(%{} = _term), do: :map
def get_type(term) when is_bitstring(term), do: :string
def get_type(term) when is_integer(term), do: :integer
def get_type(term) when is_float(term), do: :float
def get_type(term) when is_list(term) do
term
|> Keyword.keyword?()
|> if(
do: :keyword,
else: :list
)
end
def get_type(_other), do: :unknown
###
def keep_checkbox_radio(args, configs), do: do_keep_checkbox_radio(args, configs, {[], []})
defp do_keep_checkbox_radio([], _configs, new_args), do: new_args
defp do_keep_checkbox_radio(
[{arg_name, _arg_value} = arg | arg_rest],
configs,
{checkboxes, radios}
) do
%{checkbox: checkbox, radio: radio} = Keyword.get(configs, arg_name)
new_args =
case {checkbox, radio} do
{true, nil} -> {[arg | checkboxes], radios}
{nil, true} -> {checkboxes, [arg | radios]}
{nil, nil} -> {checkboxes, radios}
end
do_keep_checkbox_radio(arg_rest, configs, new_args)
end
###
def collect_errors(arg, config, path, errors) do
arg
|> Checker.lacked(config, path, errors, &join_path/2)
|> Checker.error_type(path, &join_path/2)
|> Checker.out_of_range(path, &join_path/2)
end
def collect_errors({checkbox_args, radio_args}, configs, errors) do
errors
|> Checker.check_checkbox(checkbox_args, configs)
|> Checker.check_radio(radio_args, configs)
end
### Proxy
def prune_names(term), do: Util.prune_names(term)
end