dataprep/validator
dataprep/validator — fallible checks on a single value.
Reach for this module when the operation may reject: rules
that produce typed errors when the input doesn’t satisfy them.
Compose with both (accumulate) / guard (short-circuit) /
label (attach field name) / all / alt.
For total transformations ("trim", "lowercase", "replace")
use dataprep/prep. The two compose cleanly — see
doc/architecture.md for the
decision table, the canonical Prep → Validator pipeline recipe,
and a worked end-to-end example.
Types
Validator(a, e) checks a value and either returns it unchanged or produces errors. Key invariant: if v(x) returns Valid(y), then x == y.
pub type Validator(a, e) =
fn(a) -> validated.Validated(a, e)
Values
pub fn all(
validators: List(fn(a) -> validated.Validated(a, e)),
) -> fn(a) -> validated.Validated(a, e)
Run all validators on the same input. Accumulate all errors.
Valid(a) is the identity element of accumulation, so
all([]) returns a validator that accepts every input
without producing any errors. This is a deliberate monoid law
(see test/dataprep/laws_test.gleam) and lets callers build
validator lists incrementally — for example via
list.filter(all_validators, by_feature_flag) — without a
special case when the resulting list happens to be empty.
If you want an explicit pass-through validator, prefer
validator.predicate(fn(_) { True }, _) so the intent is
visible at the call site instead of relying on the empty-list
identity.
pub fn alt(
first v1: fn(a) -> validated.Validated(a, e),
second v2: fn(a) -> validated.Validated(a, e),
) -> fn(a) -> validated.Validated(a, e)
Try alternatives in order. Use when the input can satisfy different formats (e.g. UUID or slug).
Evaluation: v1 is tried first. If Valid, v2 is never called (short-circuit). If v1 fails, v2 is tried. If both fail, errors from both branches are accumulated.
The accumulated errors can be noisy for end-user display.
Use map_error to tag each branch before alt, then
post-process the error list before presenting to users.
pub fn both(
first v1: fn(a) -> validated.Validated(a, e),
second v2: fn(a) -> validated.Validated(a, e),
) -> fn(a) -> validated.Validated(a, e)
Run both validators on the same input. Accumulate all errors. On success, return the (unchanged) input.
pub fn check(
f: fn(a) -> Result(Nil, e),
) -> fn(a) -> validated.Validated(a, e)
Create a validator from a function that returns Ok(Nil) on success or Error(e) on failure. Allows value-dependent error construction.
pub fn each(
v: fn(a) -> validated.Validated(a, e),
) -> fn(List(a)) -> validated.Validated(List(a), e)
Validate each element of a list with the given validator. All errors from all elements are accumulated. Returns Valid with the unchanged list on success.
Issue #21: returns a Validator(List(a), e) so it composes
directly with all, both, alt, and guard over the same
parent list — e.g. validator.all([length_check, validator.each(item_v)])
validates “this list as a whole” AND “each item” without an
adapter. The Validator invariant (input value preserved on
Valid) holds because validated.traverse does not mutate the
input — it threads each element through v whose own invariant
preserves the value.
For index-aware validation, use validated.traverse_indexed
with validator.label to attach position info.
pub fn guard(
pre pre: fn(a) -> validated.Validated(a, e),
main main: fn(a) -> validated.Validated(a, e),
) -> fn(a) -> validated.Validated(a, e)
Short-circuit prerequisite. Use when main is expensive or semantically depends on pre passing (e.g. “non-empty” before “regex match”).
Evaluation: pre runs first. If Valid, main runs on the same input. If pre fails, main is never called and only pre’s errors are returned. Errors are NOT accumulated across pre and main.
pub fn label(
v: fn(a) -> validated.Validated(a, e1),
ctx: ctx,
wrap: fn(ctx, e1) -> e2,
) -> fn(a) -> validated.Validated(a, e2)
Attach structured context to all errors produced by a validator.
Shorthand for map_error(v, fn(e) { wrap(ctx, e) }).
Apply at module or field boundaries (once per field), not at every individual rule. Deeply nested labels produce unreadable error structures.
Example:
check_name |> validator.label(“name”, FieldError) // wraps every error e as FieldError(“name”, e)
pub fn map_error(
v: fn(a) -> validated.Validated(a, e1),
f: fn(e1) -> e2,
) -> fn(a) -> validated.Validated(a, e2)
Transform the error type of a validator.
pub fn optional(
v: fn(a) -> validated.Validated(a, e),
) -> fn(option.Option(a)) -> validated.Validated(
option.Option(a),
e,
)
Make a validator optional: if the value is None, it is always Valid(None). If Some(a), run the inner validator and wrap the result back in Some.
Issue #21: returns a Validator(Option(a), e) so it composes
with all, both, alt, and guard over the same optional
parent value (e.g. enforce “if present, satisfies X” alongside
other Optional-level rules).
pub fn predicate(
condition: fn(a) -> Bool,
error: e,
) -> fn(a) -> validated.Validated(a, e)
Convenience for the common case of a boolean test with a static error.