defmodule DryValidation.Validator do
@moduledoc """
Contains the schema validation logic.
"""
alias DryValidation.Types
@doc """
Validates a schema agains an input map.
"""
def validate(schema, input) do
{:ok, pid} = start_agent()
Enum.each(schema, &walk(&1, input, [], pid))
result = get_all(pid)
stop_agent(pid)
format_result(result)
end
@doc false
defp walk(%{rule: :required, name: name, type: nil}, input, level, pid) do
value = Map.get(input, name)
if value,
do: put_result(pid, level, %{name => value}),
else: put_error(pid, level, %{name => "Is missing"})
end
defp walk(%{rule: :required, name: name, type: type}, input, level, pid) do
value = Map.get(input, name)
if value,
do: validate_and_put_value(type, name, value, level, pid),
else: put_error(pid, level, %{name => "Is missing"})
end
defp walk(%{rule: :optional, name: name, type: type}, input, level, pid) do
value = Map.get(input, name)
if value, do: validate_and_put_value(type, name, value, level, pid)
end
defp walk(%{rule: :map_list, name: name, optional: true} = rule, input, level, pid) do
value = Map.get(input, name)
if value do
walk(%{rule | optional: false}, input, level, pid)
end
end
defp walk(%{rule: :map_list, name: name, inner: inner, optional: false}, input, level, pid) do
value = Map.get(input, name)
if value do
if is_list(value) do
result =
Enum.map(value, fn v ->
{:ok, nested_pid} = start_agent()
Enum.each(inner, &walk(&1, v, [], nested_pid))
result = get_all(nested_pid)
stop_agent(nested_pid)
result
end)
errors? = Enum.any?(result, fn item -> item.errors != %{} end)
if errors? do
final =
result
|> Enum.with_index()
|> Enum.map(fn {item, index} -> [index, item.errors] end)
put_error(pid, level, %{name => final})
else
final = Enum.map(result, fn item -> item.result end)
put_result(pid, level, %{name => final})
end
else
put_error(pid, level, %{name => "#{inspect(value)} is not a List"})
end
else
put_error(pid, level, %{name => "Is missing"})
end
end
defp walk(%{rule: :map, name: name, inner: inner, optional: false}, input, level, pid) do
value = Map.get(input, name)
if value do
Enum.each(inner, &walk(&1, value, level ++ [name], pid))
else
put_error(pid, level, %{name => "Is missing"})
end
end
defp walk(%{rule: :map, name: name, inner: inner, optional: true}, input, level, pid) do
value = Map.get(input, name)
if value do
Enum.each(inner, &walk(&1, value, level ++ [name], pid))
end
end
@doc false
def validate_and_put_value(Types.List, name, value, level, pid) do
validate_and_put_value(%Types.List{}, name, value, level, pid)
end
def validate_and_put_value(%Types.List{type: type} = list, name, value, level, pid) do
case Types.List.call(list, value) do
{:ok, new_value} ->
put_result(pid, level, %{name => new_value})
{:error, :not_a_list} ->
put_error(pid, level, %{name => "#{inspect(value)} is not a List"})
{:error, invalid_values} ->
put_error(pid, level, %{
name => "#{inspect(invalid_values)} are not of type #{inspect(type)}"
})
{:error, invalid_values, error_message} ->
put_error(pid, level, %{
name => "#{inspect(invalid_values)} #{error_message}"
})
end
end
def validate_and_put_value(%Types.Func{type: nil} = func, name, value, level, pid) do
value = Types.Func.cast(func, value)
if Types.Func.call(func, value) do
put_result(pid, level, %{name => value})
else
put_error(pid, level, %{name => "#{inspect(value)} #{func.error_message}"})
end
end
def validate_and_put_value(%Types.Func{type: type} = func, name, value, level, pid) do
value = Types.Func.cast(func, value)
if type.valid?(value) do
if Types.Func.call(func, value) do
put_result(pid, level, %{name => value})
else
put_error(pid, level, %{name => "#{inspect(value)} #{func.error_message}"})
end
else
put_error(pid, level, %{
name => "#{inspect(value)} is not a valid type; Expected type is #{inspect(type)}"
})
end
end
def validate_and_put_value(nil, name, value, level, pid) do
put_result(pid, level, %{name => value})
end
def validate_and_put_value(type, name, value, level, pid) do
value = type.cast(value)
if type.valid?(value) do
put_result(pid, level, %{name => value})
else
put_error(pid, level, %{
name => "#{inspect(value)} is not a valid type; Expected type is #{inspect(type)}"
})
end
end
@doc false
def format_result(%{result: result, errors: errors}) when map_size(errors) == 0 do
{:ok, result}
end
@doc false
def format_result(%{errors: errors}) do
{:error, errors}
end
@doc false
defp put_result(pid, level, map) do
Agent.update(
pid,
fn state ->
update_in(state, [:result] ++ level, fn result -> Map.merge(result || %{}, map) end)
end
)
end
@doc false
defp put_error(pid, level, map) do
Agent.update(
pid,
fn state ->
update_in(state, [:errors] ++ level, fn result -> Map.merge(result || %{}, map) end)
end
)
end
@doc false
defp get_all(pid) do
Agent.get(pid, & &1)
end
@doc false
defp start_agent do
Agent.start_link(fn ->
%{result: %{}, errors: %{}}
end)
end
@doc false
defp stop_agent(pid) do
Agent.stop(pid)
end
end