-
Notifications
You must be signed in to change notification settings - Fork 0
Rules And Validators
Three functions build most validators. Rule.ruleFor names a property and selects it. Rule.must adds a predicate over the selected value. Rule.build freezes the rule so a validator can hold it:
module Ex02Rules
import Std.Io as Io
import PuduLangValidator.Rule as Rule
import PuduLangValidator.Validator as Validator
type Customer = { name: Str, age: Int }
fn startsUpper(value: Str) -> Bool {
if value.isEmpty() {
false
} else {
let first = value.chars()[0]
first >= 'A' && first <= 'Z'
}
}
fn customerValidator() -> Validator.Validator[Customer] {
let name = Rule.build(Rule.withMessage(
Rule.must(Rule.ruleFor("name", |customer: Customer| customer.name), startsUpper),
"Name must start with a capital letter."))
let adult = Rule.build(Rule.mustWith(Rule.ruleFor("age", |customer: Customer| customer.age),
fn(customer: Customer, age: Int) -> Bool { age >= 18 || customer.name == "Junior" }))
Validator.add(Validator.add(Validator.create(), name), adult)
}
fn seniorValidator() -> Validator.Validator[Customer] {
Validator.add(Validator.create(), Rule.build(Rule.withMessage(
Rule.must(Rule.ruleFor("age", |customer: Customer| customer.age), |age: Int| age <= 120),
"Customers top out at 120.")))
}
export fn main() -> Int {
let validator = customerValidator()
let bad = Validator.validate(&validator, Customer{name: "ada", age: 16})
let junior = Validator.validate(&validator, Customer{name: "Junior", age: 16})
let combined = Validator.include(seniorValidator(), &validator)
let ancient = Validator.validate(&combined, Customer{name: "ada", age: 200})
let line = "bad=" + show(bad.errors.length()) + " juniorValid=" + show(junior.isValid)
+ " included=" + show(ancient.errors.map(|failure| failure.propertyName))
match Io.writeLine(line) {
case Ok(_) => if bad.errors.length() == 2 && junior.isValid
&& ancient.errors.length() == 2
&& ancient.errors[0].propertyName == "age"
&& ancient.errors[1].propertyName == "name" { 0 } else { 2 }
case Err(_) => 1
}
}
Output:
bad=2 juniorValid=true included=["age", "name"]
Two details matter here. Rule.mustWith receives the whole record alongside the selected value, so one property's check can read another. Rule.withMessage replaces the default message for the latest check only; anything you do not touch keeps its built-in text and code.
Validators compose. Validator.include appends another validator's rules in order — the seniority
rule runs first because its validator was included first, so the ancient failure reads
["age", "name"] — and Validator.stopOnFirst stops a validator after the first rule that fails. Rules are immutable values, so one validator can be reused across many inputs safely.
Rule.mustWithContext receives the immutable context data alongside the root and the selected
value; Validator.validateWithData supplies it. The withMessageFrom, withSeverityFrom, and
withStateFrom decorators compute a check's message, severity, and state from the same inputs,
so one check can warn for one record and error for another. Rule.stopOnFirst ends a rule's own
chain after its first failure, and Rule.whenCurrent guards only the latest check while Rule.when
guards them all:
module Ex10Context
import Std.Io as Io
import Std.Json as Json
import Std.Map as Map
import PuduLangValidator.Result as Validation
import PuduLangValidator.Rule as Rule
import PuduLangValidator.Validator as Validator
type Signup = { name: Str, age: Int }
fn contextValidator() -> Validator.Validator[Signup] {
let invited = Rule.build(Rule.withStateFrom(Rule.withSeverityFrom(Rule.withMessageFrom(
Rule.mustWithContext(Rule.ruleFor("name", |signup: Signup| signup.name),
fn(_root: Signup, name: Str, data: Rule.ContextData) -> Bool {
match Map.get(&data, "expected") {
case Some(Json.Text(wanted)) => name == wanted
case _ => false
}
}),
|_root: Signup, name: Str| "Only invited guests may join, not " + name + "."),
|root: Signup, _name: Str| if root.age >= 18 { Validation.Warning } else { Validation.Error }),
|_root: Signup, name: Str| "saw:" + name))
let short = Rule.build(Rule.stopOnFirst(Rule.must(
Rule.must(Rule.ruleFor("name", |signup: Signup| signup.name), |name: Str| name.length() >= 2),
|name: Str| name != "admin")))
Validator.add(Validator.add(Validator.create(), invited), short)
}
export fn main() -> Int {
let validator = contextValidator()
let data: Rule.ContextData = mapOf([("expected", Json.Text("Ada"))])
let guest = Validator.validateWithData(&validator, Signup{name: "Bo", age: 20}, Validator.defaults(), data)
let failure = guest.errors[0]
let empty = Validator.validate(&validator, Signup{name: "", age: 30})
let latest = Rule.build(Rule.whenCurrent(
Rule.must(Rule.must(Rule.ruleFor("name", |signup: Signup| signup.name), |name: Str| name.length() >= 2),
|name: Str| !name.isEmpty()), |signup: Signup| signup.age >= 18))
let latestValidator = Validator.add(Validator.create(), latest)
let allChecks = Validator.validate(&latestValidator, Signup{name: "", age: 20})
let currentChecks = Validator.validate(&latestValidator, Signup{name: "", age: 3})
let line = "message=" + failure.errorMessage + " severity=" + show(failure.severity)
+ " state=" + failure.customState + " emptyShort=" + show(empty.errors.length())
+ " all=" + show(allChecks.errors.length()) + " current=" + show(currentChecks.errors.length())
match Io.writeLine(line) {
case Ok(_) => if guest.errors.length() == 1 && failure.severity == Validation.Warning
&& failure.customState == "saw:Bo" && empty.errors.length() == 2
&& allChecks.errors.length() == 2 && currentChecks.errors.length() == 1 { 0 } else { 2 }
case Err(_) => 1
}
}
Output:
message=Only invited guests may join, not Bo. severity=Warning state=saw:Bo emptyShort=2 all=2 current=1
"Bo" is not the invited "Ada", so the context check fails with its computed message, a Warning
for an adult, and the observed name as state. An empty name fails the retry rule's single check
plus the context check. The whenCurrent rule runs both of its checks for the adult (two failures)
but only the first for the child (one failure). Rule.withName renames a builder for messages and
Rule.overridePropertyName re-roots its failures; collection indexing uses the latter internally.