defmodule Validate do
@moduledoc """
Validate, validate incoming requests in an easy to reason-about way.
"""
alias Validate.Validator.{Error, Arg}
@fns ~w[between cast characters date_string digits_between digits ends_with in ip lowercase max_digits max min_digits min not_ends_with not_in not_regex not_starts_with nullable regex required size starts_with type uppercase url uuid]
@doc """
Validates an input against a given list of rules
## Examples
iex> {:ok, _data} = Validate.validate("Jane", required: true, type: :string)
iex> {:error, _errors} = Validate.validate([], required: true, type: :list)
iex> {:ok, _data} = Validate.validate(%{"name" => "Jane"}, %{"name" => [required: true, type: :string]})
"""
def validate(input, rules) when is_list(rules) do
{input, errors} =
validate_single_input(%{
value: input,
valueName: nil,
rules: rules,
input: input,
path: []
})
cond do
Enum.count(errors) > 0 -> {:error, errors}
true -> {:ok, input}
end
end
def validate(input, rules) do
{input, errors} =
Enum.reduce(rules, {%{}, []}, fn {inputName, ruleList}, {filteredInput, finalErrors} ->
inputValue = Map.get(input, inputName)
{inputValue, inputErrors} =
validate_single_input(%{
value: inputValue,
valueName: inputName,
rules: ruleList,
input: input,
path: []
})
cond do
Enum.count(inputErrors) > 0 -> {filteredInput, finalErrors ++ inputErrors}
true -> {Map.put(filteredInput, inputName, inputValue), finalErrors}
end
end)
cond do
Enum.count(errors) > 0 -> {:error, errors}
true -> {:ok, input}
end
end
defp validate_single_input(opts) do
{_, value, errors} =
Enum.reduce(opts.rules, {:ok, opts.value, []}, fn {ruleName, ruleArg},
{continue, value, allErrors} ->
case continue do
:halt ->
{continue, value, allErrors}
_ ->
{code, data, errors} =
run_validator_rule(%{
rule: ruleName,
arg: ruleArg,
value: value,
valueName: opts.valueName,
rules: opts.rules,
input: opts.input,
path: opts.path
})
{code, data, allErrors ++ errors}
end
end)
{value, errors}
end
defp run_validator_rule(%{rule: :list, arg: rulesForList} = opts) do
opts.value
|> Enum.with_index()
|> Enum.reduce({:ok, [], []}, fn {item, i}, {_, value, allErrors} ->
{data, errors} =
validate_single_input(%{
value: item,
valueName: i,
rules: rulesForList,
input: opts.input,
path: opts.path ++ [opts.valueName]
})
cond do
Enum.count(errors) > 0 -> {:error, value, allErrors ++ errors}
true -> {:ok, value ++ [data], allErrors}
end
end)
end
defp run_validator_rule(%{rule: :map, arg: rulesForMap} = opts) do
rulesForMap
|> Enum.reduce({:ok, %{}, []}, fn {subKey, subRules}, {_, filteredValue, allErrors} ->
path = if opts.valueName != nil, do: [opts.valueName], else: []
path = opts.path ++ path
{data, errors} =
validate_single_input(%{
value: Map.get(opts.value, subKey),
valueName: subKey,
rules: subRules,
input: opts.input,
path: path
})
cond do
Enum.count(errors) > 0 -> {:error, filteredValue, allErrors ++ errors}
true -> {:ok, Map.put(filteredValue, subKey, data), allErrors}
end
end)
end
defp run_validator_rule(opts) do
handler = get_handler(opts)
result = handler.(%Arg{value: opts.value, arg: opts.arg, input: opts.input})
path = if opts.valueName != nil, do: [opts.valueName], else: []
path = opts.path ++ path
case result do
{code, reason} when code in [:error, :halt] ->
{code, opts.value, [%Error{path: path, rule: opts.rule, message: reason}]}
{:halt} ->
{:halt, opts.value, []}
{:ok, value} ->
{:ok, value, []}
end
end
defp get_handler(%{rule: :custom, arg: arg}), do: arg
defp get_handler(%{rule: :>}), do: &Validate.Rules.GreaterThan.validate/1
defp get_handler(%{rule: :>=}), do: &Validate.Rules.GreaterThanEqual.validate/1
defp get_handler(%{rule: :<}), do: &Validate.Rules.LessThan.validate/1
defp get_handler(%{rule: :<=}), do: &Validate.Rules.LessThanEqual.validate/1
defp get_handler(%{rule: :==}), do: &Validate.Rules.Equal.validate/1
defp get_handler(%{rule: rule}) do
rule_str = Atom.to_string(rule)
if rule_str in @fns do
module = "Elixir.Validate.Rules.#{Macro.camelize(rule_str)}" |> String.to_existing_atom()
&module.validate/1
else
raise "#{rule} validator does not exist"
end
end
end