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 also(
first v1: fn(a) -> validated.Validated(a, e),
second v2: fn(a) -> validated.Validated(a, e),
) -> fn(a) -> validated.Validated(a, e)
Pipe-friendly alias of validator.both/2 for chains of three or
more error-accumulating checks. Reads as “this check also has to
pass” at every step, instead of both (which implies two things).
Identical semantics — picks the same error-accumulation behaviour
— so the choice is purely stylistic.
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 and_then(
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”).
Reads as “run main and then pre first” — same idiom as
result.try / option.then. If pre is 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. Pair with validator.both / also
when error accumulation is what you actually want.
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.
When chaining three or more error-accumulating checks, the pipe
a |> validator.both(b) |> validator.both(c) works but reads
awkwardly past two steps — “both” implies two things, not “and
one more”. validator.also/2 is an alias of this function with
the same semantics; pick whichever name fits the call site
better, or use validator.all([a, b, c]) for the list form
once the chain grows beyond a handful of checks.
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)
Deprecated: Use `validator.and_then` instead. `guard` collides with `bool.guard` in stdlib, which has the opposite branching intuition.
Deprecated alias for and_then/2. The name collides with
bool.guard from gleam_stdlib (which is the “shortcut on
condition” idiom), so first-time readers expect the opposite
branching. and_then/2 matches the Result / Option idiom
callers already know from stdlib and removes the friction.
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.
pub fn required(
error: e,
) -> fn(String) -> validated.Validated(String, e)
Reject the empty string. Convenience for the most common form-
validation case: “this field must be non-empty”. Equivalent to
predicate(fn(s) { s != "" }, error) but reads at the call site
as the intent (“required”) rather than the implementation
(“predicate that the string is not the empty string”). (#62)
Pairs naturally with prep.trim() upstream when the desired
posture is “required after trimming whitespace”:
let normalised = prep.trim() |> prep.run(" ")
// normalised == ""
case validator.required("project is required")(normalised) {
validated.Valid(_) -> ...
validated.Invalid(_) -> // rejected
}
Scoped to String because “required for Option(a)” and
“required for a list” are different shapes (Option → flip
optional/1; list → predicate(fn(xs) { xs != [] }, error)).