-
Notifications
You must be signed in to change notification settings - Fork 0
Nested Data
Records nest, and validators nest with them. Child.setValidator runs a validator for one nested record. Child.setOptionalValidator skips an absent Option value. Child.forEachValidator runs a validator per array element. Every failure keeps its full path, with collection indices in brackets:
module Ex04Nested
import Std.Io as Io
import PuduLangValidator.Rule as Rule
import PuduLangValidator.Rules.Child as ChildRule
import PuduLangValidator.Rules.Text as TextRule
import PuduLangValidator.Validator as Validator
type Address = { street: Str, zip: Str }
type Order = { id: Str, address: Address, items: Array[OrderItem] }
type OrderItem = { sku: Str, quantity: Int }
fn addressValidator() -> Validator.Validator[Address] {
let street = Rule.build(TextRule.notEmpty(Rule.ruleFor("street", |address: Address| address.street)))
let zip = Rule.build(TextRule.length(Rule.ruleFor("zip", |address: Address| address.zip), 5, 5))
Validator.add(Validator.add(Validator.create(), street), zip)
}
fn itemValidator() -> Validator.Validator[OrderItem] {
let sku = Rule.build(TextRule.notEmpty(Rule.ruleFor("sku", |item: OrderItem| item.sku)))
let quantity = Rule.build(Rule.must(Rule.ruleFor("quantity", |item: OrderItem| item.quantity),
|quantity: Int| quantity > 0))
Validator.add(Validator.add(Validator.create(), sku), quantity)
}
fn orderValidator() -> Validator.Validator[Order] {
let id = Rule.build(TextRule.notEmpty(Rule.ruleFor("id", |order: Order| order.id)))
let address = ChildRule.setValidator("address", |order: Order| order.address, addressValidator())
let items = ChildRule.forEachValidator("items", |order: Order| order.items, itemValidator())
Validator.add(Validator.add(Validator.add(Validator.create(), id), address), items)
}
export fn main() -> Int {
let validator = orderValidator()
let bad = Order{id: "o1", address: Address{street: "", zip: "12345"},
items: [OrderItem{sku: "a", quantity: 0}, OrderItem{sku: "b", quantity: 2}]}
let outcome = Validator.validate(&validator, bad)
let first = if outcome.errors.isEmpty() { "none" } else { outcome.errors[0].propertyName }
let line = "failures=" + show(outcome.errors.length()) + " first=" + first
match Io.writeLine(line) {
case Ok(_) => if outcome.errors.length() == 2 && first == "address.street" { 0 } else { 2 }
case Err(_) => 1
}
}
Output:
failures=2 first=address.street
The empty street fails as address.street; the zero quantity fails as items[0].quantity. Validation can also run over a subset of properties: Validator.includeProperties(Validator.defaults(), ["items[].sku"]) selects sku from every order item while failures still name their original indices.
Case.forCase dispatches on one variant of an Option value with its own validator, which is how a record with alternative shapes stays typed.
Selection.covers asks whether a request reaches a concrete path or its descendants;
Selection.overlaps asks whether a rule and a request meet at a path boundary; Selection.relative
strips a child prefix back off. Validator.includeProperties runs only overlapping rules,
Validator.onlyRuleSets runs exactly the named sets, and Validator.includeRulesNotInRuleSet
adds the default rules back to a named selection:
module Ex12Selection
import Std.Io as Io
import PuduLangValidator.Rule as Rule
import PuduLangValidator.Rules.Collection as CollectionRule
import PuduLangValidator.Rules.Text as TextRule
import PuduLangValidator.Selection as Selection
import PuduLangValidator.Validator as Validator
type Order = { id: Str, tags: Array[Str], note: Str }
fn orderValidator() -> Validator.Validator[Order] {
let id = Rule.build(TextRule.notEmpty(Rule.ruleFor("id", |order: Order| order.id)))
let tags = Rule.build(CollectionRule.countBetween(Rule.ruleFor("tags", |order: Order| order.tags), 1, 2))
let note = Rule.build(Rule.inRuleSet(
TextRule.notEmpty(Rule.ruleFor("note", |order: Order| order.note)), "strict"))
Validator.add(Validator.add(Validator.add(Validator.create(), id), tags), note)
}
export fn main() -> Int {
let validator = orderValidator()
let bad = Order{id: "", tags: [], note: ""}
let full = Validator.validate(&validator, bad)
let subset = Validator.validateWith(&validator, bad,
Validator.includeProperties(Validator.defaults(), ["tags"]))
let strict = Validator.validateWith(&validator, bad, Validator.onlyRuleSets(["strict"]))
let all = Validator.validateWith(&validator, bad,
Validator.includeRulesNotInRuleSet(Validator.onlyRuleSets(["strict"])))
let open: Array[Str] = []
let line = "covers=" + show(Selection.covers("items[]", "items[0]"))
+ " overlaps=" + show(Selection.overlaps("items", "items[0]"))
+ " relative=" + show(Selection.relative("address.street", "address"))
+ " full=" + show(full.errors.length())
+ " subset=" + show(subset.errors[0].propertyName)
+ " strict=" + show(strict.errors[0].propertyName)
+ " all=" + show(all.errors.length())
+ " accepts=" + show(Validator.accepts("tags", &open, &Validator.defaults()))
match Io.writeLine(line) {
case Ok(_) => if Selection.covers("items[]", "items[0]")
&& Selection.overlaps("items", "items[0]")
&& Selection.relative("address.street", "address") == Some("street")
&& full.errors.length() == 2 && subset.errors.length() == 1
&& strict.errors.length() == 1 && all.errors.length() == 3
&& Validator.accepts("tags", &open, &Validator.defaults()) { 0 } else { 2 }
case Err(_) => 1
}
}
Output:
covers=true overlaps=true relative=Some("street") full=2 subset="tags" strict="note" all=3 accepts=true
The default run skips the named note rule and reports the two unnamed failures. Selecting tags
keeps only it; selecting the strict set keeps only the note; adding the default rules back runs
all three. Validator.validateWithData threads the same options alongside immutable context data,
as Rules And Validators shows.
Collection.forEachWhere checks only the elements its filter keeps, and Collection.forEachWhereIndexed
names each failure with a caller-supplied index key instead of the raw position:
module Ex22Collection
import Std.Io as Io
import PuduLangValidator.Result as Validation
import PuduLangValidator.Rules.Collection as Collection
import PuduLangValidator.Validator as Validator
type Basket = { lines: Array[Str] }
fn whereValidator() -> Validator.Validator[Basket] {
Validator.add(Validator.create(), Collection.forEachWhere("lines",
|basket: Basket| basket.lines, |line: Str| line != "skip",
|line: Str| line.length() > 3, "Bad line."))
}
fn indexedValidator() -> Validator.Validator[Basket] {
Validator.add(Validator.create(), Collection.forEachWhereIndexed("lines",
|basket: Basket| basket.lines, |line: Str| line != "skip",
|line: Str| line.length() > 3, "Bad line at \{CollectionIndex\}.",
|index: Int, _line: Str| "at-" + show(index)))
}
export fn main() -> Int {
let basket = Basket{lines: ["skip", "no", "skip", "yes!"]}
let kept = Validator.validate(&whereValidator(), basket)
let indexed = Validator.validate(&indexedValidator(), basket)
let chained = Collection.stopOnFirst(Collection.must(
Collection.ruleForEach("lines", |basket: Basket| basket.lines),
|line: Str| !line.isEmpty()))
let stopping = Collection.withMessage(Collection.must(chained, |line: Str| line.length() >= 3),
"Line is short.")
let stopped = Validator.validate(&Validator.add(Validator.create(),
Collection.buildEach(stopping)), Basket{lines: ["", "ok"]})
let tagged = Collection.inRuleSet(Collection.must(
Collection.ruleForEach("lines", |basket: Basket| basket.lines),
|line: Str| line.length() > 2), "long")
let namedValidator = Validator.add(Validator.create(), Collection.buildEach(tagged))
let stocked = Basket{lines: ["", "ok", "longer"]}
let namedSkip = Validator.validate(&namedValidator, stocked)
let namedRun = Validator.validateWith(&namedValidator, stocked, Validator.onlyRuleSets(["long"]))
let line = "kept=" + show(kept.errors.map(|error: Validation.Failure| error.propertyName))
+ " indexed=" + show(indexed.errors.map(|error: Validation.Failure| error.propertyName))
+ " message=" + indexed.errors[0].errorMessage
+ " stopped=" + show(stopped.errors.length())
+ " namedSkip=" + show(namedSkip.errors.length())
+ " namedRun=" + show(namedRun.errors.length())
match Io.writeLine(line) {
case Ok(_) => if kept.errors.length() == 1 && kept.errors[0].propertyName == "lines[1]"
&& indexed.errors.length() == 1 && indexed.errors[0].propertyName == "lines[at-1]"
&& indexed.errors[0].errorMessage == "Bad line at 1."
&& stopped.errors.length() == 2
&& namedSkip.errors.length() == 0 && namedRun.errors.length() == 2 { 0 } else { 2 }
case Err(_) => 1
}
}
Output:
kept=["lines[1]"] indexed=["lines[at-1]"] message=Bad line at 1. stopped=2 namedSkip=0 namedRun=2
"skip" never reaches the predicate in either validator. "no" fails at its source index 1;
the indexed validator reports the same element as lines[at-1] while CollectionIndex still
renders the numeric position in the message. The chained rule (ruleForEach, must, stopOnFirst,
buildEach) stops each element at its first failure: the empty line reports once instead of twice,
so the two lines yield two failures. Filing the chain under a set with inRuleSet keeps it out of
the default run and selects it by name.