Current section
Files
Jump to
Current section
Files
lib/errors/validation_error.ex
defmodule Funx.Errors.ValidationError do
@moduledoc """
Represents a validation error in the Funx library.
A `ValidationError` wraps one or more domain-level validation messages. It is typically used with `Either.Left` to indicate that a value failed validation and should not proceed in a computation. It can also be raised directly, as it implements the `Exception` behaviour.
This module provides functions to construct, merge, and convert validation errors, enabling structured, composable error handling across pipelines and validation chains.
## Functions
* `new/1` – Creates a `ValidationError` from a single error string or a list of error strings.
* `empty/0` – Returns an empty `ValidationError`.
* `merge/2` – Combines two `ValidationError` structs into one.
* `from_tagged/1` – Converts a tagged error tuple (`{:error, errors}`) into a `ValidationError`.
This module also implements the `Exception`, `String.Chars`, and `Funx.Summarizable` protocols, supporting both human-readable output and structured reporting.
### Usage in validation
You can validate a value using a list of validator functions. Each validator returns an `Either.Right` if
the check passes, or an `Either.Left` with an error message if it fails. If any validation fails,
all errors are aggregated and returned in a single `Left`.
In contexts where an error must halt execution, `ValidationError` can be raised directly using `raise/1`.
## Examples
You can also use a `ValidationError` to hold errors:
```elixir
alias Funx.Errors.ValidationError
validate_positive = fn x ->
Funx.Monad.Either.lift_predicate(x, &(&1 > 0), fn v -> "Value must be positive: " <> to_string(v) end)
|> Funx.Monad.Either.map_left(&ValidationError.new/1)
end
validate_even = fn x ->
Funx.Monad.Either.lift_predicate(x, &(rem(&1, 2) == 0), fn v -> "Value must be even: " <> to_string(v) end)
|> Funx.Monad.Either.map_left(&ValidationError.new/1)
end
Funx.Monad.Either.validate(-3, [validate_positive, validate_even])
#=> %Funx.Monad.Either.Left{
# left: %ValidationError{
# errors: ["Value must be positive: -3", "Value must be even: -3"]
# }
# }
```
"""
import Funx.Macros, only: [eq_for: 2, ord_for: 2]
defstruct errors: [], __exception__: true
eq_for(Funx.Errors.ValidationError, :errors)
ord_for(Funx.Errors.ValidationError, :errors)
@behaviour Exception
@type t :: %__MODULE__{errors: [String.t()]}
@doc """
Creates a `ValidationError` from a single string or list of strings.
## Examples
iex> Funx.Errors.ValidationError.new("must be positive")
%Funx.Errors.ValidationError{errors: ["must be positive"]}
iex> Funx.Errors.ValidationError.new(["must be positive", "must be even"])
%Funx.Errors.ValidationError{errors: ["must be positive", "must be even"]}
"""
@spec new([String.t()]) :: t()
def new(errors) when is_list(errors), do: %__MODULE__{errors: errors}
@spec new(String.t()) :: t()
def new(error), do: %__MODULE__{errors: [error]}
@doc """
Returns an empty `ValidationError`.
## Examples
iex> Funx.Errors.ValidationError.empty()
%Funx.Errors.ValidationError{errors: []}
"""
@spec empty() :: t()
def empty, do: %__MODULE__{errors: []}
@doc """
Merges two `ValidationError` structs into one by concatenating their error lists.
## Examples
iex> e1 = Funx.Errors.ValidationError.new("must be positive")
iex> e2 = Funx.Errors.ValidationError.new("must be even")
iex> Funx.Errors.ValidationError.merge(e1, e2)
%Funx.Errors.ValidationError{errors: ["must be positive", "must be even"]}
"""
@spec merge(t(), t()) :: t()
def merge(%__MODULE__{errors: e1}, %__MODULE__{errors: e2}),
do: %__MODULE__{errors: e1 ++ e2}
@doc """
Converts a tagged error tuple into a `ValidationError`.
## Examples
iex> Funx.Errors.ValidationError.from_tagged({:error, ["must be positive"]})
%Funx.Errors.ValidationError{errors: ["must be positive"]}
"""
@spec from_tagged({:error, [String.t()]}) :: t()
def from_tagged({:error, errors}) when is_list(errors), do: new(errors)
@impl Exception
def exception(args) when is_list(args), do: struct(__MODULE__, args)
@impl Exception
def exception(message) when is_binary(message),
do: %__MODULE__{errors: [message]}
@impl Exception
def message(%__MODULE__{errors: errors}) do
Enum.map_join(errors, ", ", &to_string/1)
end
end
defimpl Funx.Appendable, for: Funx.Errors.ValidationError do
alias Funx.Errors.ValidationError
def coerce(%ValidationError{errors: e}), do: ValidationError.new(e)
def append(%ValidationError{} = acc, %ValidationError{} = other) do
ValidationError.merge(acc, other)
end
end
defimpl String.Chars, for: Funx.Errors.ValidationError do
alias Funx.Errors.ValidationError
def to_string(%ValidationError{errors: errors}) do
"ValidationError(#{Enum.map_join(errors, ", ", &Kernel.to_string/1)})"
end
end
defimpl Funx.Summarizable, for: Funx.Errors.ValidationError do
def summarize(%{errors: value}), do: {:validation_error, Funx.Summarizable.summarize(value)}
end