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.

Search Document