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)).

Search Document