Current section
Files
Jump to
Current section
Files
lib/validator/not.ex
defmodule Funx.Validator.Not do
@moduledoc """
Validates that a given validator does not succeed.
`Not` provides logical negation for validation. It inverts the success and failure
of a single validator while preserving inapplicability semantics for optional
(`Prism`) foci.
This validator is useful for expressing constraints such as:
"value must not satisfy rule A".
Options
- `:validator` (required)
A single validator to negate. This may be:
- a validator module implementing `Funx.Validate.Behaviour`
- a `{Validator, opts}` tuple for optioned validators
- `:message` (optional)
Note: Not uses a zero-arity callback `(() -> String.t())` used to override the default error
message when the negated validator succeeds.
Semantics
- The inner validator is evaluated first.
- If the inner validator returns `Left`, `Not` succeeds and returns the original value.
- If the inner validator returns `Right`, `Not` fails with a `ValidationError`.
- `Nothing` values are preserved and never cause failure.
- `Just` values are validated by the inner validator, but the original input is
returned unchanged on success.
Examples
iex> Funx.Validator.Not.validate(0,
...> validator: Funx.Validator.Positive
...> )
%Funx.Monad.Either.Right{right: 0}
iex> Funx.Validator.Not.validate(10,
...> validator: Funx.Validator.Positive
...> )
%Funx.Monad.Either.Left{
left: %Funx.Errors.ValidationError{
errors: ["must not satisfy condition"]
}
}
iex> Funx.Validator.Not.validate(%Funx.Monad.Maybe.Nothing{},
...> validator: Funx.Validator.Positive
...> )
%Funx.Monad.Either.Right{right: %Funx.Monad.Maybe.Nothing{}}
iex> Funx.Validator.Not.validate(10,
...> validator: Funx.Validator.Positive,
...> message: fn -> "must not be positive" end
...> )
%Funx.Monad.Either.Left{
left: %Funx.Errors.ValidationError{
errors: ["must not be positive"]
}
}
"""
@behaviour Funx.Validate.Behaviour
alias Funx.Errors.ValidationError
alias Funx.Monad
alias Funx.Monad.Either
def validate(value, opts) when is_list(opts) do
validate(value, opts, %{})
end
@impl true
def validate(value, opts, env) do
validator = Keyword.fetch!(opts, :validator)
result = run(validator, value, env)
case result do
%Either.Right{right: %Monad.Maybe.Nothing{}} ->
result
other ->
other
|> Either.flip()
|> Monad.map(fn _ -> value end)
|> finalize(opts)
end
end
defp run({validator, opts}, value, env) do
validator.validate(value, opts, env)
end
defp run(validator, value, env) do
validator.validate(value, [], env)
end
defp finalize(%Either.Right{} = ok, _opts), do: ok
defp finalize(%Either.Left{}, opts) do
message =
case Keyword.get(opts, :message) do
nil -> "must not satisfy condition"
callback -> callback.()
end
Either.left(ValidationError.new(message))
end
end