Current section

Files

Jump to
funx lib validate.ex
Raw

lib/validate.ex

defmodule Funx.Validate do
@moduledoc """
Declarative validation DSL using optics and applicative error accumulation.
## Overview
The Validation DSL provides:
- Declarative syntax for validating nested structures
- Projection to fields using optics (Lens, Prism, Traversal)
- Applicative error accumulation (all validators run, all errors collected)
- Structure preservation (returns original value on success)
## Usage
use Funx.Validate
user_validation =
validate do
at :name, [Required, {MinLength, min: 3}]
at :email, [Required, Email]
end
Either.validate(%{name: "Alice", email: "alice@example.com"}, user_validation)
#=> %Right{right: %{name: "Alice", email: "alice@example.com"}}
## Laws
1. **Identity**: `validate do end` returns `Right(value)`
2. **Structure Preservation**: Successful validation returns original structure
3. **Applicative**: All validators run; all errors accumulate
## Architecture
The DSL compiles in two phases:
1. **Parser** - Converts DSL syntax into Step nodes
2. **Executor** - Converts Step nodes into executable validator function
"""
alias Funx.Errors.ValidationError
alias Funx.Monad.Either
alias Funx.Validate.Dsl.{Executor, Parser}
@type t() :: (term(), keyword() -> Either.t(ValidationError.t(), term()))
defmacro __using__(_opts) do
quote do
import Funx.Validate, only: [validate: 1, validate: 2]
end
end
@doc """
Defines a validation using the DSL.
Returns a validator function compatible with `Either.validate/2,3`.
## Syntax
### Root Validators
validate do
HasContactMethod
ValidTimezone
end
### Field Validation with `at`
validate do
at :name, Required
at :email, Email
end
By default, `at :key` uses `Prism.key(:key)` (optional field).
### Nested Field Validation (List Paths)
validate do
at [:user, :profile, :name], Required
at [:user, :profile, :age], Positive
end
List paths support struct modules for type-safe nested access:
validate do
at [User, :profile, Profile, :age], Positive
end
### Explicit Optics
validate do
# Prism: optional field
at Prism.key(:email), Email
# Lens: required field (raises KeyError if missing)
at Lens.key(:name), Required
end
### Validator Options
validate do
at :name, {MinLength, min: 3}
end
### Multiple Validators per Field
validate do
at :name, [Required, {MinLength, min: 3}]
end
Validators run sequentially left-to-right.
### Validation Modes
# Sequential mode (default): fail-fast, short-circuits on first error
validate mode: :sequential do
at :name, Required
at :email, Email
end
# Parallel mode: runs all validations, accumulates all errors
validate mode: :parallel do
at :name, Required
at :email, Email
end
## Examples
validate do
at :name, Required
at :email, [Required, Email]
at :age, Positive
end
"""
defmacro validate(opts \\ [], do: block) do
mode = Keyword.get(opts, :mode, :sequential)
compile_validation(block, mode, __CALLER__)
end
defp compile_validation(block, mode, caller_env) do
steps = Parser.parse_steps(block, caller_env)
Executor.execute_steps(steps, mode)
end
end