defmodule Xema.Validator do
@moduledoc """
This module contains all validators to check data against a schema.
"""
use Xema.Format
import Xema.Utils
alias Xema.{Behaviour, Ref, Schema}
@compile {
:inline,
do_validate: 3,
get_type: 1,
struct?: 1,
struct?: 2,
type?: 2,
types: 2,
validate_by: 4,
fail?: 2
}
@type result :: :ok | {:error, map}
@types [
:atom,
:struct,
:boolean,
:float,
:integer,
:keyword,
:list,
:map,
nil,
:number,
:string,
:tuple
]
@doc """
A callback for custom validators. For an example see:
[Custom validators](examples.html#custom-validator)
"""
@callback validate(any) :: :ok | result
@doc """
Validates `data` against the given `schema`.
"""
@spec validate(Behaviour.t() | Schema.t(), any) :: result
def validate(schema, data), do: validate(schema, data, [])
@doc false
@spec validate(Behaviour.t() | Schema.t(), any, keyword) :: result
def validate(%Schema{} = schema, data, opts),
do: do_validate(schema, data, opts)
def validate(%{schema: schema} = xema, data, opts),
do:
do_validate(
schema,
data,
opts
|> Keyword.put_new(:root, xema)
|> Keyword.put_new(:master, xema)
)
@spec do_validate(Behaviour.t() | Schema.t(), any, keyword) :: result
defp do_validate(%Schema{type: true}, _, _), do: :ok
defp do_validate(%Schema{type: false}, _, _), do: {:error, %{type: false}}
defp do_validate(%Schema{type: types} = schema, value, opts) when is_list(types) do
with {:ok, type} <- types(schema, value),
:ok <- validate_by(:default, schema, value, opts),
:ok <- validate_by(type, schema, value, opts),
:ok <- custom_validator(schema, value),
do: :ok
end
defp do_validate(%Schema{type: :any, ref: nil} = schema, value, opts) do
with type <- get_type(value),
:ok <- validate_by(:default, schema, value, opts),
:ok <- validate_by(type, schema, value, opts),
:ok <- custom_validator(schema, value),
do: :ok
end
defp do_validate(%Schema{type: :any, ref: ref}, value, opts), do: Ref.validate(ref, value, opts)
defp do_validate(%Schema{type: type} = schema, value, opts) do
with :ok <- type(schema, value),
:ok <- validate_by(:default, schema, value, opts),
:ok <- validate_by(type, schema, value, opts),
:ok <- custom_validator(schema, value),
do: :ok
end
defp validate_by(:default, schema, value, opts) do
with :ok <- enum(schema, value),
:ok <- not_(schema, value, opts),
:ok <- all_of(schema, value, opts),
:ok <- any_of(schema, value, opts),
:ok <- one_of(schema, value, opts),
:ok <- const(schema, value),
:ok <- if_then_else(schema, value, opts),
do: :ok
end
defp validate_by(:string, schema, value, _opts) do
with :ok <- min_length(schema, value),
:ok <- max_length(schema, value),
:ok <- pattern(schema, value),
:ok <- format(schema, value),
do: :ok
end
defp validate_by(:tuple, schema, value, opts),
do: validate_by(:list, schema, value, opts)
defp validate_by(:list, schema, value, opts) do
case fail?(opts, :finally) do
true ->
collect([
min_items(schema, value),
max_items(schema, value),
unique(schema, value),
items(schema, value, opts),
contains(schema, value, opts)
])
false ->
with :ok <- min_items(schema, value),
:ok <- max_items(schema, value),
:ok <- unique(schema, value),
:ok <- items(schema, value, opts),
:ok <- contains(schema, value, opts),
do: :ok
end
end
defp validate_by(:struct, schema, value, opts) do
with :ok <- module(schema, value),
:ok <- validate_by(:map, schema, value, opts),
do: :ok
end
defp validate_by(:map, schema, value, opts) do
case fail?(opts, :finally) do
false ->
with :ok <- size(schema, value),
:ok <- keys(schema, value),
:ok <- required(schema, value),
:ok <- property_names(schema, value, opts),
:ok <- dependencies(schema, value, opts),
:ok <- all_properties(schema, value, opts),
do: :ok
true ->
collect([
size(schema, value),
keys(schema, value),
required(schema, value),
property_names(schema, value, opts),
dependencies(schema, value, opts),
all_properties(schema, value, opts)
])
end
end
defp validate_by(:keyword, schema, value, opts) do
case fail?(opts, :finally) do
true ->
map = Enum.into(value, %{})
collect([
dependencies(schema, value, opts),
size(schema, value),
required(schema, map),
property_names(schema, map, opts),
all_properties(schema, map, opts)
])
false ->
with :ok <- dependencies(schema, value, opts),
:ok <- size(schema, value),
value <- Enum.into(value, %{}),
:ok <- required(schema, value),
:ok <- property_names(schema, value, opts),
:ok <- all_properties(schema, value, opts),
do: :ok
end
end
defp validate_by(:integer, schema, value, opts),
do: validate_by(:number, schema, value, opts)
defp validate_by(:float, schema, value, opts),
do: validate_by(:number, schema, value, opts)
defp validate_by(:number, schema, value, opts) do
with :ok <- minimum(schema, value),
:ok <- maximum(schema, value),
:ok <- exclusive_maximum(schema, value),
:ok <- exclusive_minimum(schema, value),
:ok <- multiple_of(schema, value),
:ok <- validate_by(:default, schema, value, opts),
do: :ok
end
defp validate_by(:boolean, _schema, _value, _opts), do: :ok
defp validate_by(nil, _schema, _value, _opts), do: :ok
defp validate_by(:atom, _schema, _value, _opts), do: :ok
#
# Schema type handling
#
defp get_type([]), do: :list
defp get_type(value),
do: Enum.find(@types, fn type -> type?(type, value) end)
@spec type(Schema.t() | atom, any) :: result
defp type(%{type: type}, value) do
case type?(type, value) do
true -> :ok
false -> {:error, %{type: type, value: value}}
end
end
@spec type?(atom, any) :: boolean
defp type?(:any, _value), do: true
defp type?(:atom, value), do: is_atom(value)
defp type?(:boolean, value), do: is_boolean(value)
defp type?(:string, value), do: is_binary(value)
defp type?(:tuple, value), do: is_tuple(value)
defp type?(:keyword, value), do: Keyword.keyword?(value)
defp type?(:number, value), do: is_number(value)
defp type?(:integer, value), do: is_integer(value) || like_integer(value)
defp type?(:float, value), do: is_float(value)
defp type?(:map, value), do: is_map(value)
defp type?(:list, value), do: is_list(value)
defp type?(:struct, value), do: is_map(value) && struct?(value)
defp type?(nil, nil), do: true
defp type?(_, _), do: false
defp like_integer(value), do: is_float(value) && trunc(value) - value == 0
@spec struct?(any) :: boolean
defp struct?(%_{}), do: true
defp struct?(_), do: false
@spec struct?(any, atom) :: boolean
defp struct?(%module{}, module), do: true
defp struct?(_, _), do: false
@spec types(Schema.t(), any) :: {:ok, atom} | {:error, map}
defp types(%{type: list}, value) do
case Enum.find(list, :not_found, fn type -> type?(type, value) end) do
:not_found -> {:error, %{type: list, value: value}}
found -> {:ok, found}
end
end
#
# Validators
#
@spec const(Schema.t(), any) :: result
defp const(%{const: nil}, _value), do: :ok
defp const(%{const: :__nil__}, nil), do: :ok
defp const(%{const: :__nil__}, value),
do: {:error, %{const: nil, value: value}}
defp const(%{const: const}, const), do: :ok
defp const(%{const: const}, value) when is_number(const) do
case const == value do
true -> :ok
false -> {:error, %{const: const, value: value}}
end
end
defp const(%{const: const}, value),
do: {:error, %{const: const, value: value}}
@spec if_then_else(Behaviour.t() | Schema.t(), any, keyword) :: result
defp if_then_else(%{if: nil}, _value, _opts), do: :ok
defp if_then_else(%{then: nil, else: nil}, _value, _opts), do: :ok
defp if_then_else(%{if: schema_if, then: schema_then, else: schema_else}, value, opts) do
case Xema.valid?(schema_if, value) do
true ->
if_then_else(:then, schema_then, value, opts)
false ->
if_then_else(:else, schema_else, value, opts)
end
end
@spec if_then_else(atom, Schema.t() | nil, any) :: result
defp if_then_else(_key, nil, _value, _opts), do: :ok
defp if_then_else(key, schema, value, opts) do
case do_validate(schema, value, opts) do
:ok -> :ok
{:error, reason} -> {:error, Map.new([{key, reason}])}
end
end
@spec property_names(Behaviour.t() | Schema.t(), map, keyword) :: result
defp property_names(%{property_names: nil}, _map, _opts), do: :ok
defp property_names(%{property_names: schema}, map, opts) do
map
|> Map.keys()
|> Enum.reduce([], fn
key, acc when is_binary(key) ->
case do_validate(schema, key, opts) do
:ok -> acc
{:error, reason} -> [{key, reason} | acc]
end
key, acc when is_atom(key) ->
case do_validate(schema, Atom.to_string(key), opts) do
:ok -> acc
{:error, reason} -> [{key, reason} | acc]
end
_, acc ->
acc
end)
|> case do
[] -> :ok
errors -> {:error, %{value: Map.keys(map), property_names: Enum.reverse(errors)}}
end
end
@spec enum(Schema.t(), any) :: result
defp enum(%{enum: nil}, _element), do: :ok
defp enum(%{enum: enum}, value) when is_integer(value) do
case Enum.member?(enum, value) || Enum.member?(enum, value * 1.0) do
true -> :ok
false -> {:error, %{enum: enum, value: value}}
end
end
defp enum(%{enum: enum}, value) when is_float(value) do
case Enum.member?(enum, value) do
true ->
:ok
false ->
case zero_terminated_float?(value) && Enum.member?(enum, trunc(value)) do
true -> :ok
false -> {:error, %{enum: enum, value: value}}
end
end
end
defp enum(%{enum: enum}, value) do
case Enum.member?(enum, value) do
true -> :ok
false -> {:error, %{enum: enum, value: value}}
end
end
defp zero_terminated_float?(value) when is_float(value), do: Float.round(value) == value
@spec module(Schema.t(), any) :: result
defp module(%{module: nil}, _val), do: :ok
defp module(%{module: module}, val) do
case struct?(val, module) do
true -> :ok
false -> {:error, %{module: module, value: val}}
end
end
@spec not_(Schema.t(), any, keyword) :: result
defp not_(%{not: nil}, _value, _opts), do: :ok
defp not_(%{not: schema}, value, opts) do
case do_validate(schema, value, opts) do
:ok -> {:error, %{not: :ok, value: value}}
_ -> :ok
end
end
@spec any_of(Schema.t(), any, keyword) :: result
defp any_of(%{any_of: nil}, _value, _opts), do: :ok
defp any_of(%{any_of: schemas}, value, opts) do
case do_any_of(schemas, value, opts) do
:ok ->
:ok
{:error, errors} ->
{:error, %{any_of: Enum.reverse(errors), value: value}}
end
end
@spec do_any_of(list, any, keyword, [map]) :: :ok | {:error, list(map)}
defp do_any_of(schemas, value, opts, errors \\ [])
defp do_any_of([], _value, _opts, errors), do: {:error, errors}
defp do_any_of([schema | schemas], value, opts, errors) do
with {:error, error} <- do_validate(schema, value, opts) do
do_any_of(schemas, value, opts, [error | errors])
end
end
@spec all_of(Schema.t(), any, keyword) :: result
defp all_of(%{all_of: nil}, _value, _opts), do: :ok
defp all_of(%{all_of: schemas}, value, opts) do
with {:error, errors} <- do_all_of(schemas, value, opts) do
{:error, %{all_of: errors, value: value}}
end
end
@spec do_all_of(list, any, keyword, [map]) :: result
defp do_all_of(schemas, value, opts, errors \\ [])
defp do_all_of([], _value, _opts, []), do: :ok
defp do_all_of([], _value, _opts, errors),
do: {:error, Enum.reverse(errors)}
defp do_all_of([schema | schemas], value, opts, errors) do
case do_validate(schema, value, opts) do
:ok ->
do_all_of(schemas, value, opts, errors)
{:error, error} ->
do_all_of(schemas, value, opts, [error | errors])
end
end
@spec one_of(Schema.t(), any, keyword) :: result
defp one_of(%{one_of: nil}, _value, _opts), do: :ok
defp one_of(%{one_of: schemas}, value, opts) do
case do_one_of(schemas, value, opts) do
%{success: [], errors: errors} ->
{:error, %{one_of: {:error, Enum.reverse(errors)}, value: value}}
%{success: [_]} ->
:ok
%{success: success} ->
{:error, %{one_of: {:ok, Enum.reverse(success)}, value: value}}
end
end
@spec do_one_of(list, any, keyword) :: %{errors: [map], success: [map]}
defp do_one_of(schemas, value, opts),
do:
schemas
|> Enum.with_index()
|> Enum.reduce(
%{errors: [], success: []},
fn {schema, index}, %{errors: errors, success: success} ->
case do_validate(schema, value, opts) do
:ok ->
%{errors: errors, success: [index | success]}
{:error, error} ->
%{errors: [error | errors], success: success}
end
end
)
@spec exclusive_maximum(Schema.t(), any) :: result
defp exclusive_maximum(%{exclusive_maximum: nil}, _value), do: :ok
defp exclusive_maximum(%{exclusive_maximum: max}, _value)
when is_boolean(max),
do: :ok
defp exclusive_maximum(%{exclusive_maximum: max}, value)
when value < max,
do: :ok
defp exclusive_maximum(%{exclusive_maximum: max}, value),
do: {:error, %{exclusive_maximum: max, value: value}}
@spec exclusive_minimum(Schema.t(), any) :: result
defp exclusive_minimum(%{exclusive_minimum: nil}, _value), do: :ok
defp exclusive_minimum(%{exclusive_minimum: min}, _value)
when is_boolean(min),
do: :ok
defp exclusive_minimum(%{exclusive_minimum: min}, value)
when value > min,
do: :ok
defp exclusive_minimum(%{exclusive_minimum: min}, value),
do: {:error, %{value: value, exclusive_minimum: min}}
@spec minimum(Schema.t(), any) :: result
defp minimum(%{minimum: nil}, _value), do: :ok
defp minimum(
%{minimum: minimum, exclusive_minimum: exclusive_minimum},
value
)
when is_number(value),
do: minimum(minimum, exclusive_minimum, value)
@spec minimum(number, boolean, number) :: result
defp minimum(minimum, _exclusive, value) when value > minimum, do: :ok
defp minimum(minimum, nil, value) when value == minimum, do: :ok
defp minimum(minimum, false, value) when value == minimum, do: :ok
defp minimum(minimum, nil, value),
do: {:error, %{value: value, minimum: minimum}}
defp minimum(minimum, exclusive, value),
do: {:error, %{value: value, minimum: minimum, exclusive_minimum: exclusive}}
@spec maximum(Schema.t(), any) :: result
defp maximum(%{maximum: nil}, _value), do: :ok
defp maximum(
%{maximum: maximum, exclusive_maximum: exclusive_maximum},
value
),
do: maximum(maximum, exclusive_maximum, value)
@spec maximum(number, boolean, number) :: result
defp maximum(maximum, _exclusive, value) when value < maximum, do: :ok
defp maximum(maximum, nil, value) when value == maximum, do: :ok
defp maximum(maximum, false, value) when value == maximum, do: :ok
defp maximum(maximum, nil, value),
do: {:error, %{value: value, maximum: maximum}}
defp maximum(maximum, exclusive, value),
do: {:error, %{value: value, maximum: maximum, exclusive_maximum: exclusive}}
@spec multiple_of(Schema.t(), number) :: result
defp multiple_of(%{multiple_of: nil} = _keywords, _value), do: :ok
defp multiple_of(%{multiple_of: multiple_of}, value) when is_number(value) do
x = value / multiple_of
case x - Float.floor(x) do
0.0 -> :ok
_ -> {:error, %{value: value, multiple_of: multiple_of}}
end
end
@spec min_length(Schema.t(), String.t()) :: result
defp min_length(%{min_length: nil}, _), do: :ok
defp min_length(%{min_length: min}, value) do
len = String.length(value)
case len >= min do
true -> :ok
false -> {:error, %{value: value, min_length: min}}
end
end
@spec max_length(Schema.t(), String.t()) :: result
defp max_length(%{max_length: nil}, _), do: :ok
defp max_length(%{max_length: max}, value) do
len = String.length(value)
case len <= max do
true -> :ok
false -> {:error, %{value: value, max_length: max}}
end
end
@spec pattern(Schema.t(), String.t()) :: result
defp pattern(%{pattern: nil}, _string), do: :ok
defp pattern(%{pattern: pattern}, string) do
case Regex.match?(pattern, string) do
true -> :ok
false -> {:error, %{value: string, pattern: pattern}}
end
end
@spec min_items(Schema.t(), list | tuple) :: result
defp min_items(%{min_items: nil}, _), do: :ok
defp min_items(%{min_items: min}, val) do
case size(val) >= min do
true -> :ok
false -> {:error, %{value: val, min_items: min}}
end
end
@spec max_items(Schema.t(), list | tuple) :: result
defp max_items(%{max_items: nil}, _list), do: :ok
defp max_items(%{max_items: max}, val) do
case size(val) <= max do
true -> :ok
false -> {:error, %{value: val, max_items: max}}
end
end
@spec unique(Schema.t(), list | tuple) :: result
defp unique(%{unique_items: nil}, _list), do: :ok
defp unique(%{unique_items: false}, _list), do: :ok
defp unique(%{unique_items: true}, list) when is_list(list) do
case unique?(list) do
true -> :ok
false -> {:error, %{value: list, unique_items: true}}
end
end
defp unique(%{unique_items: true}, tuple) when is_tuple(tuple) do
tuple
|> Tuple.to_list()
|> unique?()
|> case do
true -> :ok
false -> {:error, %{value: tuple, unique_items: true}}
end
end
@spec unique?(list, map) :: boolean
defp unique?(list, set \\ %{})
defp unique?([], _), do: true
defp unique?([h | t], set) do
case set do
%{^h => true} -> false
_ -> unique?(t, Map.put(set, h, true))
end
end
@spec contains(Schema.t(), any, keyword) :: result
defp contains(%{contains: nil}, _, _), do: :ok
defp contains(%{contains: _} = schema, tuple, opts) when is_tuple(tuple) do
with {:error, reason} <- contains(schema, Tuple.to_list(tuple), opts) do
{:error, %{reason | value: tuple}}
end
end
defp contains(%{contains: schema}, list, opts) when is_list(list) do
errors =
list
|> Enum.with_index()
|> Enum.reduce([], fn {value, index}, acc ->
case do_validate(schema, value, opts) do
:ok -> acc
{:error, reason} -> [{index, reason} | acc]
end
end)
case length(errors) < length(list) do
true -> :ok
false -> {:error, %{value: list, contains: Enum.reverse(errors)}}
end
end
@spec items(Schema.t(), list | tuple, keyword) :: result
defp items(%{items: nil}, _list, _opts), do: :ok
defp items(schema, tuple, opts) when is_tuple(tuple) do
items(schema, Tuple.to_list(tuple), opts)
end
defp items(%{items: items} = schema, list, opts) when is_list(items) do
items_tuple(
items,
Map.get(schema, :additional_items, true),
Enum.with_index(list),
[],
opts
)
end
defp items(%{items: items}, list, opts) do
items_list(items, Enum.with_index(list), [], opts)
end
@spec items_list(Schema.t(), [{any, integer}], list, keyword) :: result
defp items_list(%{type: false}, [], _, _), do: :ok
defp items_list(%{type: false}, _, _, _), do: {:error, %{type: false}}
defp items_list(%{type: true}, _, _, _), do: :ok
defp items_list(_schema, [], [], _opts), do: :ok
defp items_list(_schema, [], errors, _opts),
do: {:error, %{items: Enum.into(errors, %{})}}
defp items_list(schema, [{item, index} | list], errors, opts) do
case do_validate(schema, item, opts) do
:ok ->
items_list(schema, list, errors, opts)
{:error, reason} ->
case fail?(opts, :immediately) do
true -> items_list(schema, [], [{index, reason} | errors], opts)
false -> items_list(schema, list, [{index, reason} | errors], opts)
end
end
end
@spec items_tuple(list, nil | boolean | Schema.t(), [{any, integer}], list, keyword) :: result
defp items_tuple(_schemas, _additonal_items, [], [], _opts), do: :ok
defp items_tuple(_schemas, _additonal_items, [], errors, _opts),
do: {:error, %{items: Enum.into(errors, %{})}}
defp items_tuple([], false, [{_, index} | list], errors, opts) do
items_tuple(
[],
false,
list,
[{index, %{additional_items: false}} | errors],
opts
)
end
defp items_tuple([], additional_items, _list, [], _opts)
when additional_items in [nil, true],
do: :ok
defp items_tuple([], additional_items, _list, errors, _opts)
when additional_items in [nil, true],
do: {:error, %{items: Enum.into(errors, %{})}}
defp items_tuple([], schema, [{item, index} | list], errors, opts) do
case do_validate(schema, item, opts) do
:ok ->
items_tuple([], schema, list, errors, opts)
{:error, reason} ->
case fail?(opts, :immediately) do
true -> items_tuple([], schema, [], [{index, reason} | errors], opts)
false -> items_tuple([], schema, list, [{index, reason} | errors], opts)
end
end
end
defp items_tuple(
[schema | schemas],
additional_items,
[{item, index} | list],
errors,
opts
) do
case do_validate(schema, item, opts) do
:ok ->
items_tuple(schemas, additional_items, list, errors, opts)
{:error, reason} ->
case fail?(opts, :immediately) do
true -> items_tuple(schemas, additional_items, [], [{index, reason} | errors], opts)
false -> items_tuple(schemas, additional_items, list, [{index, reason} | errors], opts)
end
end
end
@spec keys(Schema.t(), any) :: result
defp keys(%{keys: nil}, _value), do: :ok
defp keys(%{keys: :atoms}, map) do
case map |> Map.keys() |> Enum.all?(&is_atom/1) do
true -> :ok
false -> {:error, %{keys: :atoms, value: map}}
end
end
defp keys(%{keys: :strings}, map) do
case map |> Map.keys() |> Enum.all?(&is_binary/1) do
true -> :ok
false -> {:error, %{keys: :strings, value: map}}
end
end
@spec all_properties(Schema.t(), map, keyword) :: :ok | {:error, map}
defp all_properties(schema, value, opts) do
case fail?(opts, :immediately) do
true ->
with :ok <- patterns(schema, value, opts),
:ok <- properties(schema, value, opts),
:ok <- additionals(schema, value, opts),
do: :ok
false ->
[
patterns(schema, value, opts),
properties(schema, value, opts),
additionals(schema, value, opts)
]
|> collect()
|> case do
:ok ->
:ok
{:error, reasons} when is_list(reasons) ->
properties =
Enum.reduce(reasons, %{}, fn %{properties: properties}, acc ->
Map.merge(acc, properties)
end)
{:error, %{properties: properties}}
{:error, _} = error ->
error
end
end
end
@spec properties(Schema.t(), map, keyword) :: :ok | {:error, map}
defp properties(%{properties: nil}, _map, _opts), do: :ok
defp properties(%{properties: props}, map, opts),
do: do_properties(Map.to_list(props), map, %{}, opts)
@spec do_properties(list, map, map, keyword) :: result
defp do_properties([], _map, errors, _opts) when errors == %{}, do: :ok
defp do_properties([], _map, errors, _opts), do: {:error, %{properties: errors}}
defp do_properties([{prop, schema} | props], map, errors, opts) do
with {:ok, value} <- Map.fetch(map, prop),
:ok <- do_validate(schema, value, opts) do
do_properties(props, map, errors, opts)
else
# The property is not in the map. The required properties are checked at
# another location.
:error ->
do_properties(props, map, errors, opts)
{:error, reason} ->
updated_errors = Map.put(errors, prop, reason)
case fail?(opts, :immediately) do
true -> do_properties([], map, updated_errors, opts)
false -> do_properties(props, map, updated_errors, opts)
end
end
end
@spec required(Schema.t(), map) :: result
defp required(%{required: nil}, _map), do: :ok
defp required(%{required: required}, map) do
case Enum.filter(required, fn key -> !Map.has_key?(map, key) end) do
[] ->
:ok
missing ->
{
:error,
%{required: missing}
}
end
end
@spec size(Schema.t(), map | keyword) :: result
defp size(%{min_properties: nil, max_properties: nil}, _value), do: :ok
defp size(%{min_properties: min, max_properties: max}, value) when is_map(value) do
do_size(length(Map.keys(value)), min, max, value)
end
defp size(%{min_properties: min, max_properties: max}, value) when is_list(value) do
do_size(length(value), min, max, value)
end
@spec do_size(number, number, number, map | keyword) :: result
defp do_size(len, min, _max, value) when not is_nil(min) and len < min do
{:error, %{min_properties: min, value: value}}
end
defp do_size(len, _min, max, value) when not is_nil(max) and len > max do
{:error, %{max_properties: max, value: value}}
end
defp do_size(_len, _min, _max, _map), do: :ok
@spec patterns(Schema.t(), map, keyword) :: :ok | {:error, map}
defp patterns(%{pattern_properties: nil}, _map, _opts), do: :ok
defp patterns(%{pattern_properties: patterns}, map, opts) do
props =
for {pattern, schema} <- Map.to_list(patterns),
key <- Map.keys(map),
key_match?(pattern, key),
do: {key, schema}
do_properties(props, map, %{}, opts)
end
@spec key_match?(Regex.t(), String.t() | atom) :: boolean
defp key_match?(regex, atom) when is_atom(atom) do
key_match?(regex, to_string(atom))
end
defp key_match?(regex, string), do: Regex.match?(regex, string)
@spec additionals(Schema.t(), map, keyword) :: result
defp additionals(%{additional_properties: true}, _map, _opts), do: :ok
defp additionals(%{additional_properties: nil}, _map, _opts), do: :ok
defp additionals(schema, map, opts) do
map = map |> delete_properties(schema) |> delete_patterns(schema)
do_additionals(schema, map, opts)
end
defp do_additionals(%{additional_properties: false}, map, _opts) do
case Map.equal?(map, %{}) do
true ->
:ok
false ->
{
:error,
%{
properties:
map
|> Map.keys()
|> Enum.into(%{}, fn x ->
{x, %{additional_properties: false}}
end)
}
}
end
end
defp do_additionals(%{additional_properties: schema}, map, opts)
when is_map(schema) do
result =
Enum.reduce(map, %{}, fn {key, value}, acc ->
case do_validate(schema, value, opts) do
:ok -> acc
{:error, reason} -> Map.put(acc, key, reason)
end
end)
case result == %{} do
true -> :ok
false -> {:error, %{properties: result}}
end
end
@spec dependencies(Schema.t(), map, keyword) :: result
defp dependencies(%{dependencies: nil}, _map, _opts), do: :ok
defp dependencies(%{dependencies: dependencies}, map, opts) do
dependencies
|> Enum.filter(fn {key, _} -> has_key?(map, key) end)
|> do_dependencies(map, opts)
end
@spec do_dependencies(list, map, keyword) :: result
defp do_dependencies([], _map, _opts), do: :ok
defp do_dependencies([{key, list} | tail], map, opts) when is_list(list) do
with :ok <- do_dependencies_list(key, list, map, opts) do
do_dependencies(tail, map, opts)
end
end
defp do_dependencies([{key, schema} | tail], map, opts) do
case do_validate(schema, map, opts) do
:ok ->
do_dependencies(tail, map, opts)
{:error, reason} ->
{:error, %{dependencies: %{key => reason}}}
end
end
@spec do_dependencies_list(String.t() | atom, list, map, keyword) :: result
defp do_dependencies_list(_key, [], _map, _opts), do: :ok
defp do_dependencies_list(key, [dependency | dependencies], map, opts) do
case has_key?(map, dependency) do
true ->
do_dependencies_list(key, dependencies, map, opts)
false ->
{:error, %{dependencies: %{key => dependency}}}
end
end
# Semantic validation of strings.
@spec format(Schema.t(), any) :: result
defp format(%{format: nil}, _str), do: :ok
defp format(%{format: fmt}, str) when Format.supports(fmt) do
case Format.is?(fmt, str) do
true -> :ok
false -> {:error, %{format: fmt, value: str}}
end
end
defp format(_, _str), do: :ok
# Custom validator
@spec custom_validator(Schema.t(), any) :: result
defp custom_validator(%{validator: validator}, value)
when is_function(validator, 1) do
with {:error, reason} <- validator.(value) do
{:error, %{validator: reason, value: value}}
end
end
defp custom_validator(%{validator: {module, validator}}, value) do
with {:error, reason} <- apply(module, validator, [value]) do
{:error, %{validator: reason, value: value}}
end
end
defp custom_validator(%{validator: behaviour}, value)
when not is_nil(behaviour) and is_atom(behaviour) do
with {:error, reason} <- behaviour.validate(value) do
{:error, %{validator: reason, value: value}}
end
end
defp custom_validator(_, _), do: :ok
defp delete_properties(map, %{properties: nil}), do: map
defp delete_properties(map, %{properties: properties}) do
map
|> Enum.filter(fn {key, _value} -> !Map.has_key?(properties, key) end)
|> Enum.into(%{})
end
defp delete_patterns(map, %{pattern_properties: nil}), do: map
defp delete_patterns(map, %{pattern_properties: patterns}) do
patterns = Map.keys(patterns)
map
|> Enum.filter(fn {key, _value} -> !match_any?(patterns, key) end)
|> Enum.into(%{})
end
defp match_any?(patterns, pattern) when is_atom(pattern) do
match_any?(patterns, Atom.to_string(pattern))
end
defp match_any?(patterns, pattern) when is_binary(pattern) do
Enum.any?(patterns, fn regex -> Regex.match?(regex, pattern) end)
end
defp collect(results) when is_list(results) do
results
|> Enum.reduce(:ok, fn
:ok, :ok ->
:ok
{:error, reason}, :ok ->
{:error, [reason]}
:ok, {:error, _} = error ->
error
{:error, reason}, {:error, reasons} ->
{:error, [reason | reasons]}
end)
|> collect()
end
defp collect(:ok), do: :ok
defp collect({:error, [reason]}), do: {:error, reason}
defp collect(errors), do: errors
defp fail?(opts, cmp), do: Keyword.get(opts, :fail, :early) == cmp
end