Skip to content

Built In Checks

Chris Michael edited this page Sep 29, 2026 · 6 revisions

Pudu

Built In Checks

The Rules modules cover the common cases so most validators never need a hand-written predicate:

Module Checks
Rules.Text notEmpty, empty, length, minimumLength, maximumLength, emailAddress, matches, creditCard, enumName
Rules.Number greaterThan, greaterThanOrEqualTo, lessThan, lessThanOrEqualTo, inclusiveBetween, exclusiveBetween, equal, notEqual, notEmpty, empty, isInEnum
Rules.Decimal precisionScale
Rules.Presence notNull, isNull, notEmpty
Rules.Collection notEmpty, empty, countBetween, forEach, forEachWhere, forEachWhereIndexed, and element chains (ruleForEach, must, withMessage, withCode, whereItems, withIndex, stopOnFirst, inRuleSet, buildEach)
Rules.Comparison equal, notEqual, equalToProperty, notEqualToProperty, oneOf
Rules.Ordering greaterThan, greaterThanOrEqualTo, lessThan, lessThanOrEqualTo, inclusiveBetween, exclusiveBetween, and the *Property cross-property forms
Rules.Child setValidator, setOptionalValidator, forEachValidator
Rules.Case forCase

Text.matches compiles its pattern up front and reports a typed error for a bad pattern instead of failing at validation time. Braces interpolate in Pudu string literals, so a quantifier such as {2} is written with escaped braces (\{2\}); an unescaped brace never reaches the regex engine. Decimal.precisionScale counts exact decimal digits: with precision 4 and scale 2, at most two digits may stand before the point, whether or not the value uses both fractional places.

module Ex03Checks

import Std.Io as Io
import PuduLangValidator.Rule as Rule
import PuduLangValidator.Rules.Collection as CollectionRule
import PuduLangValidator.Rules.Comparison as ComparisonRule
import PuduLangValidator.Rules.Decimal as DecimalRule
import PuduLangValidator.Rules.Number as NumberRule
import PuduLangValidator.Rules.Ordering as OrderingRule
import PuduLangValidator.Rules.Presence as PresenceRule
import PuduLangValidator.Rules.Text as TextRule
import PuduLangValidator.Validator as Validator

type Signup = {
  name: Str, email: Str, password: Str, confirm: Str,
  age: Int, price: Decimal, nickname: Option[Str],
  low: Int, high: Int, tags: Array[Str]
}

fn signupValidator() -> Validator.Validator[Signup] {
  let v0 = Validator.create()
  let v1 = Validator.add(v0, Rule.build(TextRule.minimumLength(Rule.ruleFor("name", |s: Signup| s.name), 2)))
  let v2 = Validator.add(v1, Rule.build(TextRule.emailAddress(Rule.ruleFor("email", |s: Signup| s.email))))
  let v3 = Validator.add(v2, Rule.build(NumberRule.inclusiveBetween(Rule.ruleFor("age", |s: Signup| s.age), 18, 120)))
  let v4 = Validator.add(v3, Rule.build(DecimalRule.precisionScale(Rule.ruleFor("price", |s: Signup| s.price), 6, 2, false)))
  let v5 = Validator.add(v4, Rule.build(PresenceRule.notNull(Rule.ruleFor("nickname", |s: Signup| s.nickname))))
  let v6 = Validator.add(v5, Rule.build(ComparisonRule.equalToProperty(
    Rule.ruleFor("confirm", |s: Signup| s.confirm), "password", |s: Signup| s.password)))
  let v7 = Validator.add(v6, Rule.build(OrderingRule.greaterThanProperty(
    Rule.ruleFor("high", |s: Signup| s.high), "low", |s: Signup| s.low)))
  let v8 = Validator.add(v7, Rule.build(CollectionRule.countBetween(Rule.ruleFor("tags", |s: Signup| s.tags), 1, 5)))
  v8
}

fn good() -> Signup {
  Signup{name: "Ada", email: "ada@example.com", password: "s3cret!", confirm: "s3cret!",
    age: 30, price: 19.99d, nickname: Some("ada"), low: 3, high: 9, tags: ["pudu"]}
}

export fn main() -> Int {
  let validator = signupValidator()
  let ok = Validator.validate(&validator, good())
  let bad = Validator.validate(&validator, Signup{..good(), email: "nope", age: 3, confirm: "other"})
  let line = "ok=" + show(ok.isValid) + " bad=" + show(bad.errors.length())
  match Io.writeLine(line) {
    case Ok(_) => if ok.isValid && bad.errors.length() == 3 { 0 } else { 2 }
    case Err(_) => 1
  }
}

Output:

ok=true bad=3

One validator across every family

The remaining checks fill the gaps the signup validator leaves: shaped text, optional presence, exact decimals, cross-property and set comparisons, and nested or optional children:

module Ex15ChecksExtra

import Std.Io as Io
import PuduLangValidator.Rule as Rule
import PuduLangValidator.Rules.Child as ChildRule
import PuduLangValidator.Rules.Case as CaseRule
import PuduLangValidator.Rules.Collection as CollectionRule
import PuduLangValidator.Rules.Comparison as ComparisonRule
import PuduLangValidator.Rules.Decimal as DecimalRule
import PuduLangValidator.Rules.Number as NumberRule
import PuduLangValidator.Rules.Ordering as OrderingRule
import PuduLangValidator.Rules.Presence as PresenceRule
import PuduLangValidator.Rules.Text as TextRule
import PuduLangValidator.Validator as Validator

type Address = { street: Str, zip: Str }

type Signup = {
  name: Str, email: Str, code: Str, card: Str, role: Str, title: Str,
  nickname: Option[Str], middle: Option[Str], price: Decimal,
  password: Str, confirm: Str, flavor: Str, low: Int, high: Int,
  address: Option[Address], contact: Option[Str], tags: Array[Str]
}

fn addressValidator() -> Validator.Validator[Address] {
  let street = Rule.build(TextRule.notEmpty(Rule.ruleFor("street", |address: Address| address.street)))
  Validator.add(Validator.create(), street)
}

fn contactValidator() -> Validator.Validator[Str] {
  Validator.add(Validator.create(), Rule.build(
    TextRule.notEmpty(Rule.ruleFor("value", |contact: Str| contact))))
}

fn extraValidator() -> Validator.Validator[Signup] {
  let v00 = Validator.create()
  let v01 = Validator.add(v00, Rule.build(TextRule.emailAddress(Rule.ruleFor("email", |s: Signup| s.email))))
  let code = match TextRule.matches(Rule.ruleFor("code", |s: Signup| s.code), "^[A-Z]\{2\}-[0-9]\{4\}$") {
    case Ok(rule) => Rule.build(rule)
    case Err(_) => panic("expected compilable pattern")
  }
  let v02 = Validator.add(v01, code)
  let v03 = Validator.add(v02, Rule.build(TextRule.creditCard(Rule.ruleFor("card", |s: Signup| s.card))))
  let v04 = Validator.add(v03, Rule.build(TextRule.enumName(Rule.ruleFor("role", |s: Signup| s.role), ["admin", "member"], true)))
  let v05 = Validator.add(v04, Rule.build(TextRule.enumName(Rule.ruleFor("title", |s: Signup| s.title), ["admin"], false)))
  let v06 = Validator.add(v05, Rule.build(PresenceRule.notNull(Rule.ruleFor("nickname", |s: Signup| s.nickname))))
  let v07 = Validator.add(v06, Rule.build(PresenceRule.isNull(Rule.ruleFor("middle", |s: Signup| s.middle))))
  let v08 = Validator.add(v07, Rule.build(PresenceRule.notEmpty(Rule.ruleFor("nickname", |s: Signup| s.nickname))))
  let v09 = Validator.add(v08, Rule.build(DecimalRule.precisionScale(Rule.ruleFor("price", |s: Signup| s.price), 6, 2, false)))
  let v10 = Validator.add(v09, Rule.build(ComparisonRule.equalToProperty(
    Rule.ruleFor("confirm", |s: Signup| s.confirm), "password", |s: Signup| s.password)))
  let v11 = Validator.add(v10, Rule.build(ComparisonRule.oneOf(Rule.ruleFor("flavor", |s: Signup| s.flavor), ["red", "green"])))
  let v12 = Validator.add(v11, Rule.build(OrderingRule.greaterThanProperty(
    Rule.ruleFor("high", |s: Signup| s.high), "low", |s: Signup| s.low)))
  let v13 = Validator.add(v12, ChildRule.setOptionalValidator("address", |s: Signup| s.address, addressValidator()))
  let v14 = Validator.add(v13, CaseRule.forCase("contact", |s: Signup| s.contact, contactValidator()))
  let v15 = Validator.add(v14, Rule.build(NumberRule.inclusiveBetween(Rule.ruleFor("low", |s: Signup| s.low), 1, 9)))
  let v16 = Validator.add(v15, Rule.build(CollectionRule.countBetween(Rule.ruleFor("tags", |s: Signup| s.tags), 1, 3)))
  v16
}

fn good() -> Signup {
  Signup{name: "Ada", email: "ada@example.com", code: "AB-1234", card: "4111 1111 1111 1111",
    role: "admin", title: "ADMIN", nickname: Some("ada"), middle: None, price: 19.99d,
    password: "s3cret!", confirm: "s3cret!", flavor: "red", low: 3, high: 9,
    address: None, contact: None, tags: ["pudu"]}
}

export fn main() -> Int {
  let validator = extraValidator()
  let ok = Validator.validate(&validator, good())
  let bad = Validator.validate(&validator, Signup{..good(), email: "nope", code: "x",
    card: "4111 1111 1111 1112", role: "root", nickname: None, price: 19.999d,
    confirm: "other", flavor: "blue", high: 1,
    address: Some(Address{street: "", zip: "12345"}), contact: Some("")})
  let line = "ok=" + show(ok.isValid) + " bad=" + show(bad.errors.length())
  match Io.writeLine(line) {
    case Ok(_) => if ok.isValid && bad.errors.length() == 12 { 0 } else { 2 }
    case Err(_) => 1
  }
}

Output:

ok=true bad=12

The good signup passes all sixteen rules. The bad one fails email, code, card, role, both nickname presence rules, price scale, confirmation, flavor, ceiling, street, and contact: twelve failures, with the absent nickname failing twice. An absent address and an absent contact stay silent; Child.setOptionalValidator and Case.forCase only run when the value is present.

Bounds at a glance

Comparisons are exact at their endpoints: greaterThan rejects the bound it names while greaterThanOrEqualTo accepts it, and the ordering variants read the same way through Ord. Text.length and Collection.countBetween accept both of their bounds:

module Ex17Bounds

import Std.Io as Io
import PuduLangValidator.Rule as Rule
import PuduLangValidator.Rules.Collection as CollectionRule
import PuduLangValidator.Rules.Comparison as ComparisonRule
import PuduLangValidator.Rules.Number as NumberRule
import PuduLangValidator.Rules.Ordering as OrderingRule
import PuduLangValidator.Rules.Text as TextRule
import PuduLangValidator.Validator as Validator

type Probe = {
  a: Int, b: Int, c: Int, d: Int, e: Int, f: Int, g: Int, h: Int, i: Int, j: Int,
  name: Str, title: Str, level: Int, m: Int, k: Int, l: Int,
  want: Int, code: Int, code2: Int, tags: Array[Str], archived: Array[Str]
}

fn boundsValidator() -> Validator.Validator[Probe] {
  let v00 = Validator.create()
  let v01 = Validator.add(v00, Rule.build(NumberRule.greaterThan(Rule.ruleFor("a", |p: Probe| p.a), 5)))
  let v02 = Validator.add(v01, Rule.build(NumberRule.lessThan(Rule.ruleFor("b", |p: Probe| p.b), 5)))
  let v03 = Validator.add(v02, Rule.build(NumberRule.exclusiveBetween(Rule.ruleFor("c", |p: Probe| p.c), 1, 9)))
  let v04 = Validator.add(v03, Rule.build(NumberRule.equal(Rule.ruleFor("d", |p: Probe| p.d), 7)))
  let v05 = Validator.add(v04, Rule.build(NumberRule.notEqual(Rule.ruleFor("e", |p: Probe| p.e), 7)))
  let v06 = Validator.add(v05, Rule.build(NumberRule.notEmpty(Rule.ruleFor("f", |p: Probe| p.f))))
  let v07 = Validator.add(v06, Rule.build(NumberRule.empty(Rule.ruleFor("g", |p: Probe| p.g))))
  let v08 = Validator.add(v07, Rule.build(NumberRule.isInEnum(Rule.ruleFor("h", |p: Probe| p.h), [1, 2])))
  let v08b = Validator.add(v08, Rule.build(NumberRule.greaterThanOrEqualTo(Rule.ruleFor("i", |p: Probe| p.i), 5)))
  let v08c = Validator.add(v08b, Rule.build(NumberRule.lessThanOrEqualTo(Rule.ruleFor("j", |p: Probe| p.j), 5)))
  let v09 = Validator.add(v08c, Rule.build(TextRule.length(Rule.ruleFor("name", |p: Probe| p.name), 2, 4)))
  let v10 = Validator.add(v09, Rule.build(TextRule.empty(Rule.ruleFor("title", |p: Probe| p.title))))
  let v11 = Validator.add(v10, Rule.build(OrderingRule.greaterThan(Rule.ruleFor("level", |p: Probe| p.level), 5)))
  let v11b = Validator.add(v11, Rule.build(OrderingRule.lessThan(Rule.ruleFor("l", |p: Probe| p.l), 5)))
  let v11c = Validator.add(v11b, Rule.build(OrderingRule.inclusiveBetween(Rule.ruleFor("k", |p: Probe| p.k), 2, 4)))
  let v12 = Validator.add(v11c, Rule.build(OrderingRule.lessThanOrEqualTo(Rule.ruleFor("m", |p: Probe| p.m), 4)))
  let v13 = Validator.add(v12, Rule.build(ComparisonRule.equal(Rule.ruleFor("want", |p: Probe| p.want), 7)))
  let v14 = Validator.add(v13, Rule.build(ComparisonRule.notEqual(Rule.ruleFor("code", |p: Probe| p.code), 7)))
  let v14b = Validator.add(v14, Rule.build(ComparisonRule.notEqualToProperty(
    Rule.ruleFor("code2", |p: Probe| p.code2), "code", |p: Probe| p.code)))
  let v15 = Validator.add(v14b, Rule.build(CollectionRule.notEmpty(Rule.ruleFor("tags", |p: Probe| p.tags))))
  let v16 = Validator.add(v15, Rule.build(CollectionRule.empty(Rule.ruleFor("archived", |p: Probe| p.archived))))
  let v17 = Validator.add(v16, CollectionRule.forEach("tags", |p: Probe| p.tags,
    |tag: Str| tag.length() > 0, "Empty tag."))
  let chained = CollectionRule.withMessage(CollectionRule.must(
    CollectionRule.ruleForEach("tags", |p: Probe| p.tags), |tag: Str| tag.length() > 1), "Short tag.")
  let v18 = Validator.add(v17, CollectionRule.buildEach(chained))
  v18
}

fn good() -> Probe {
  Probe{a: 6, b: 4, c: 5, d: 7, e: 8, f: 1, g: 0, h: 2, i: 5, j: 5,
    name: "abcd", title: "", level: 6, m: 4, k: 3, l: 4,
    want: 7, code: 8, code2: 9, tags: ["aa", "bb"], archived: []}
}

export fn main() -> Int {
  let validator = boundsValidator()
  let ok = Validator.validate(&validator, good())
  let bad = Validator.validate(&validator, Probe{a: 5, b: 5, c: 1, d: 8, e: 7, f: 0, g: 1, h: 3, i: 4, j: 6,
    name: "abcde", title: "x", level: 5, m: 5, k: 1, l: 5,
    want: 8, code: 7, code2: 7, tags: [], archived: ["x"]})
  let line = "ok=" + show(ok.isValid) + " bad=" + show(bad.errors.length())
  match Io.writeLine(line) {
    case Ok(_) => if ok.isValid && bad.errors.length() == 21 { 0 } else { 2 }
    case Err(_) => 1
  }
}

Output:

ok=true bad=21

Every one of the twenty-one bounds fails exactly once on the bad probe, including the endpoints: a rejects 5 above greaterThan(5) while i accepts 5 at greaterThanOrEqualTo(5). The chained element rule at the end shares the tags array with the forEach check above it; both stay silent on the empty bad list because no element means no failure.

Ordering against bounds and siblings

The ordering checks read the same way through any Ord type: strict comparisons reject the bound they name while inclusive ones accept it, and the *Property forms compare against a sibling property instead of a fixed value:

module Ex23Ordering

import Std.Io as Io
import PuduLangValidator.Rule as Rule
import PuduLangValidator.Rules.Ordering as OrderingRule
import PuduLangValidator.Validator as Validator

type Probe = { low: Int, high: Int, mid: Int }

fn orderingValidator() -> Validator.Validator[Probe] {
  let v0 = Validator.create()
  let v1 = Validator.add(v0, Rule.build(OrderingRule.greaterThanOrEqualTo(Rule.ruleFor("high", |p: Probe| p.high), 5)))
  let v2 = Validator.add(v1, Rule.build(OrderingRule.exclusiveBetween(Rule.ruleFor("high", |p: Probe| p.high), 1, 9)))
  let v3 = Validator.add(v2, Rule.build(OrderingRule.lessThanProperty(
    Rule.ruleFor("low", |p: Probe| p.low), "high", |p: Probe| p.high)))
  let v4 = Validator.add(v3, Rule.build(OrderingRule.greaterThanOrEqualToProperty(
    Rule.ruleFor("high", |p: Probe| p.high), "low", |p: Probe| p.low)))
  let v5 = Validator.add(v4, Rule.build(OrderingRule.lessThanOrEqualToProperty(
    Rule.ruleFor("low", |p: Probe| p.low), "high", |p: Probe| p.high)))
  let v6 = Validator.add(v5, Rule.build(OrderingRule.lessThan(Rule.ruleFor("mid", |p: Probe| p.mid), 5)))
  let v7 = Validator.add(v6, Rule.build(OrderingRule.inclusiveBetween(Rule.ruleFor("mid", |p: Probe| p.mid), 2, 4)))
  v7
}

export fn main() -> Int {
  let validator = orderingValidator()
  let ok = Validator.validate(&validator, Probe{low: 3, high: 5, mid: 3})
  let bad = Validator.validate(&validator, Probe{low: 5, high: 1, mid: 5})
  let edge = Validator.validate(&validator, Probe{low: 5, high: 5, mid: 4})
  let line = "ok=" + show(ok.isValid) + " bad=" + show(bad.errors.length()) + " edge=" + show(edge.errors.length())
  match Io.writeLine(line) {
    case Ok(_) => if ok.isValid && bad.errors.length() == 7 && edge.errors.length() == 1 { 0 } else { 2 }
    case Err(_) => 1
  }
}

Output:

ok=true bad=7 edge=1

The good probe passes all seven rules. The bad probe fails each one exactly once. The edge probe, with both ends at 5, fails only the strict lessThanProperty: equality passes every inclusive check and fails every strict one.

Cross-property checks take the other property's path as well as its selector, so failure messages can name the property they were compared against.

Related

Clone this wiki locally