Current section

Files

Jump to
funx lib validator each.ex
Raw

lib/validator/each.ex

defmodule Funx.Validator.Each do
@moduledoc """
Validates that every element in a list passes a given validator (or validators).
`Each` provides universal quantification over list elements. It applies one or more
validators to each element and collects all errors using applicative semantics.
## Options
Exactly one of the following must be provided:
- `:validator` - A single validator to apply to each element
- `:validators` - A list of validators; each element must pass all of them
Each validator may be:
- A validator module implementing `Funx.Validate.Behaviour`
- A `{Validator, opts}` tuple for optioned validators
- A validator function with arity 1, 2, or 3
## Semantics
- Uses `traverse_a` for applicative error collection (all failures reported)
- Empty lists pass validation (vacuous truth)
- `Nothing` passes through unchanged
- `Just(list)` unwraps and validates the list
## Examples
iex> Funx.Validator.Each.validate([1, 2, 3], validator: Funx.Validator.Positive)
%Funx.Monad.Either.Right{right: [1, 2, 3]}
iex> Funx.Validator.Each.validate([1, 2, 3], validators: [Funx.Validator.Positive, Funx.Validator.Integer])
%Funx.Monad.Either.Right{right: [1, 2, 3]}
"""
@behaviour Funx.Validate.Behaviour
alias Funx.Errors.ValidationError
alias Funx.Monad
alias Funx.Monad.Either
alias Funx.Monad.Maybe.{Just, Nothing}
def validate(value), do: validate(value, [])
def validate(value, opts), do: validate(value, opts, %{})
@impl true
def validate(%Nothing{}, _opts, _env), do: Either.right(%Nothing{})
def validate(%Just{value: list}, opts, env), do: validate(list, opts, env)
def validate(list, opts, _env) when is_list(list) do
validators = normalize_validators!(opts)
Either.traverse_a(list, fn item ->
validate_item(item, validators)
end)
end
def validate(_non_list, opts, _env) do
message = Funx.Validator.build_message(opts, nil, "must be a list")
Either.left(ValidationError.new(message))
end
defp normalize_validators!(opts) do
has_validator = Keyword.has_key?(opts, :validator)
has_validators = Keyword.has_key?(opts, :validators)
case {has_validator, has_validators} do
{false, false} ->
raise ArgumentError, "Each validator requires :validator or :validators option"
{true, true} ->
raise ArgumentError, "Each validator accepts :validator or :validators, not both"
{true, false} ->
[Keyword.fetch!(opts, :validator)]
{false, true} ->
Keyword.fetch!(opts, :validators)
end
end
defp validate_item(item, validators) do
Either.traverse_a(validators, fn validator ->
run(validator, item)
end)
|> Monad.map(fn _ -> item end)
end
defp run({validator, opts}, value) when is_atom(validator) do
validator.validate(value, opts, %{})
end
defp run(validator, value) when is_atom(validator) do
validator.validate(value, [], %{})
end
defp run(func, value) when is_function(func, 1), do: func.(value)
defp run(func, value) when is_function(func, 2), do: func.(value, [])
defp run(func, value) when is_function(func, 3), do: func.(value, [], %{})
end