Skip to content

Conditions And Sets

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

Pudu

Conditions And Sets

Not every rule runs on every input. Rule.when and Rule.unless guard a whole rule on the record. Rule.inRuleSet files a rule under a name, and the caller chooses which names run: Validator.onlyRuleSets runs exactly those sets, Validator.allRuleSets runs everything, and the default run skips named rules. Validator.withPreValidation decides before any rule runs and can return an early result:

module Ex05Sets

import Std.Io as Io
import PuduLangValidator.Result as Validation
import PuduLangValidator.Rule as Rule
import PuduLangValidator.Rules.Number as NumberRule
import PuduLangValidator.Rules.Text as TextRule
import PuduLangValidator.Validator as Validator

type Customer = { name: Str, age: Int }

fn baseValidator() -> Validator.Validator[Customer] {
  let name = Rule.build(TextRule.notEmpty(Rule.ruleFor("name", |customer: Customer| customer.name)))
  let age = Rule.build(Rule.inRuleSet(NumberRule.greaterThanOrEqualTo(
    Rule.ruleFor("age", |customer: Customer| customer.age), 18), "strict"))
  Validator.add(Validator.add(Validator.create(), name), age)
}

fn guardedValidator() -> Validator.Validator[Customer] {
  let name = Rule.build(Rule.when(TextRule.notEmpty(Rule.ruleFor("name", |customer: Customer| customer.name)),
    fn(customer: Customer) -> Bool { customer.age >= 18 }))
  Validator.add(Validator.create(), name)
}

fn preValidator() -> Validator.Validator[Customer] {
  Validator.withPreValidation(Validator.create(),
    fn(customer: Customer) -> Option[Array[Validation.Failure]] {
      if customer.name == "Blocked" { Some([Validation.Failure{
          propertyName: "", errorMessage: "Blocked.", errorCode: "Blocked",
          severity: Validation.Error, customState: "" }]) } else { None }
    })
}

export fn main() -> Int {
  let validator = baseValidator()
  let kid = Customer{name: "Bo", age: 3}
  let loose = Validator.validate(&validator, kid)
  let strict = Validator.validateWith(&validator, kid, Validator.onlyRuleSets(["strict"]))
  let teen = Validator.validate(&guardedValidator(), Customer{name: "", age: 3})
  let blocked = Validator.validate(&preValidator(), Customer{name: "Blocked", age: 30})
  let line = "loose=" + show(loose.isValid) + " strict=" + show(strict.errors.length())
    + " teen=" + show(teen.isValid) + " blocked=" + show(blocked.errors[0].errorCode)
  match Io.writeLine(line) {
    case Ok(_) => if loose.isValid && strict.errors.length() == 1 && teen.isValid && blocked.errors[0].errorCode == "Blocked" { 0 } else { 2 }
    case Err(_) => 1
  }
}

Output:

loose=true strict=1 teen=true blocked="Blocked"

The default run passes the three-year-old because the age rule sits in the strict set. The guarded name rule only runs for adults. The prevalidation answer arrives without running any rule at all. Rule.stopOnFirst and Validator.stopOnFirst give the same early-exit shape inside one rule chain and across a whole validator.

Whole-record checks with Advanced

Advanced.custom answers failures for the whole record at once instead of one selected property; Advanced.customWithContext also reads the immutable context data. Advanced.dependent runs its second rule only when the first yields nothing, so a ceiling is never measured against a nonsense floor. Advanced.when and Advanced.unless guard a finished rule the way Rule.when guards a builder. Validator.stopOnFirst ends the whole validator after its first failing rule:

module Ex11Advanced

import Std.Io as Io
import Std.Json as Json
import Std.Map as Map
import PuduLangValidator.Advanced as Advanced
import PuduLangValidator.Result as Validation
import PuduLangValidator.Rule as Rule
import PuduLangValidator.Rules.Number as NumberRule
import PuduLangValidator.Validator as Validator

type Customer = { name: Str, age: Int }

fn adult() -> Rule.Rule[Customer] {
  Advanced.custom("adult", fn(customer: Customer) -> Array[Validation.Failure] {
    if customer.age >= 18 { [] } else { [Validation.Failure{
      propertyName: "age", errorMessage: "Must be an adult.",
      errorCode: "Adult", severity: Validation.Error, customState: "" }] }
  })
}

fn campaign() -> Rule.Rule[Customer] {
  Advanced.customWithContext("campaign",
    fn(_root: Customer, data: Rule.ContextData) -> Array[Validation.Failure] {
      match Map.get(&data, "campaign") {
        case Some(Json.Text(tag)) => [Validation.Failure{
            propertyName: "campaign", errorMessage: "Campaign " + tag + " needs review.",
            errorCode: "Campaign", severity: Validation.Info, customState: tag }]
        case _ => []
      }
    })
}

fn saneAge() -> Rule.Rule[Customer] {
  let floor = Rule.build(NumberRule.greaterThanOrEqualTo(Rule.ruleFor("age", |customer: Customer| customer.age), 0))
  let ceiling = Rule.build(NumberRule.lessThanOrEqualTo(Rule.ruleFor("age", |customer: Customer| customer.age), 150))
  Advanced.dependent(floor, ceiling)
}

fn vipOnly() -> Rule.Rule[Customer] {
  Advanced.when(Rule.build(Rule.must(Rule.ruleFor("name", |customer: Customer| customer.name),
    |name: Str| name == "Ada")), |customer: Customer| customer.age >= 21)
}

export fn main() -> Int {
  let validator = Validator.add(Validator.add(Validator.add(Validator.add(
    Validator.create(), adult()), saneAge()), vipOnly()), campaign())
  let kid = Validator.validate(&validator, Customer{name: "Bo", age: 3})
  let senior = Validator.validate(&validator, Customer{name: "Bo", age: 200})
  let vipSkip = Validator.validate(&validator, Customer{name: "Bo", age: 20})
  let vipHit = Validator.validate(&validator, Customer{name: "Bo", age: 30})
  let data: Rule.ContextData = mapOf([("campaign", Json.Text("spring"))])
  let flagged = Validator.validateWithData(&validator, Customer{name: "Ada", age: 30}, Validator.defaults(), data)
  let halted = Validator.validate(&Validator.stopOnFirst(validator), Customer{name: "Bo", age: 3})
  let line = "kid=" + show(kid.errors.length()) + " senior=" + show(senior.errors.length())
    + " vipSkip=" + show(vipSkip.isValid) + " vipHit=" + show(vipHit.isValid)
    + " campaign=" + show(flagged.errors.length()) + " halted=" + show(halted.errors.length())
  match Io.writeLine(line) {
    case Ok(_) => if kid.errors.length() == 1 && senior.errors.length() == 2
      && vipSkip.isValid && !vipHit.isValid && flagged.errors.length() == 1
      && halted.errors.length() == 1 { 0 } else { 2 }
    case Err(_) => 1
  }
}

Output:

kid=1 senior=2 vipSkip=true vipHit=false campaign=1 halted=1

The three-year-old fails only the adult check; the 200-year-old fails the ceiling and the age-gated name check. The name check runs for the thirty-year-old but not the twenty-year-old. With campaign data present, the context check adds its Info failure. Stopping after the first failing rule keeps just the adult failure for the kid.

Skipping with unless, and running everything

Rule.unless is Rule.when with the condition flipped, and Rule.unlessCurrent flips only the latest check the way Rule.whenCurrent guards it. Advanced.unless does the same for a finished rule. Validator.allRuleSets runs every rule, named or not, where Validator.onlyRuleSets runs exactly the named ones:

module Ex19Unless

import Std.Io as Io
import PuduLangValidator.Advanced as Advanced
import PuduLangValidator.Rule as Rule
import PuduLangValidator.Rules.Text as TextRule
import PuduLangValidator.Validator as Validator

type Customer = { name: Str, age: Int }

fn unlessValidator() -> Validator.Validator[Customer] {
  Validator.add(Validator.create(), Rule.build(Rule.unless(
    Rule.must(Rule.ruleFor("name", |customer: Customer| customer.name), |name: Str| !name.isEmpty()),
    |customer: Customer| customer.age < 13)))
}

fn currentValidator() -> Validator.Validator[Customer] {
  Validator.add(Validator.create(), Rule.build(Rule.unlessCurrent(
    Rule.must(Rule.must(Rule.ruleFor("name", |customer: Customer| customer.name),
      |name: Str| name.length() >= 2), |name: Str| !name.isEmpty()),
    |customer: Customer| customer.age < 13)))
}

fn advancedValidator() -> Validator.Validator[Customer] {
  Validator.add(Validator.create(), Advanced.unless(
    Rule.build(Rule.must(Rule.ruleFor("name", |customer: Customer| customer.name),
      |name: Str| name == "Ada")),
    |customer: Customer| customer.age < 13))
}

fn setsValidator() -> Validator.Validator[Customer] {
  let id = Rule.build(TextRule.notEmpty(Rule.ruleFor("name", |customer: Customer| customer.name)))
  let strict = Rule.build(Rule.inRuleSet(
    TextRule.notEmpty(Rule.ruleFor("name", |customer: Customer| customer.name)), "strict"))
  Validator.add(Validator.add(Validator.create(), id), strict)
}

export fn main() -> Int {
  let kid = Customer{name: "", age: 3}
  let adult = Customer{name: "", age: 30}
  let unlessKid = Validator.validate(&unlessValidator(), kid)
  let unlessAdult = Validator.validate(&unlessValidator(), adult)
  let currentKid = Validator.validate(&currentValidator(), kid)
  let currentAdult = Validator.validate(&currentValidator(), adult)
  let advancedKid = Validator.validate(&advancedValidator(), Customer{name: "Bo", age: 3})
  let advancedAdult = Validator.validate(&advancedValidator(), Customer{name: "Bo", age: 30})
  let all = Validator.validateWith(&setsValidator(), kid, Validator.allRuleSets())
  let line = "unlessKid=" + show(unlessKid.isValid) + " unlessAdult=" + show(unlessAdult.isValid)
    + " currentKid=" + show(currentKid.errors.length()) + " currentAdult=" + show(currentAdult.errors.length())
    + " advancedKid=" + show(advancedKid.isValid) + " advancedAdult=" + show(advancedAdult.isValid)
    + " all=" + show(all.errors.length())
  match Io.writeLine(line) {
    case Ok(_) => if unlessKid.isValid && !unlessAdult.isValid
      && currentKid.errors.length() == 1 && currentAdult.errors.length() == 2
      && advancedKid.isValid && !advancedAdult.isValid && all.errors.length() == 2 { 0 } else { 2 }
    case Err(_) => 1
  }
}

Output:

unlessKid=true unlessAdult=false currentKid=1 currentAdult=2 advancedKid=true advancedAdult=false all=2

The empty name is skipped for the kid and fails for the adult. The latest-only variant runs one check for the kid and both for the adult. The built-rule variant skips the whole rule for the kid. Running all sets reports both the default and the named failure.

Related

Clone this wiki locally