
AsyncTest.pudu
Pudu126 lines7.4 KB
1/** @Test.Validation.Async.Suite — checks awaited failure order */2module PuduLangValidator.AsyncTest34import Std.Json as Json5import Std.Map as Map6import Std.Test as Test7import PuduLangValidator.Async as Async8import PuduLangValidator.Result as Validation9import PuduLangValidator.Rule as Rule10import PuduLangValidator.Validator as Validator1112type Item = { key: Str, count: Int, tags: Array[Str] }1314async fn keyExists(_root: Item, key: Str) -> Bool { key == "present" }1516async fn included(tag: Str) -> Bool { tag != "skip" }1718async fn longEnough(_root: Item, tag: Str) -> Bool { tag.length() >= 3 }1920async fn rejectsSkipAndShort(_root: Item, tag: Str) -> Bool { tag != "skip" && tag.length() >= 3 }2122async fn matchesContext(_root: Item, key: Str, data: Rule.ContextData) -> Bool {23 match Map.get(&data, "expected") {24 case Some(Json.Text(expected)) => key == expected25 case _ => false26 }27}2829async fn main() -> Int {30 let asyncRule = Async.mustAsync("key", |item: Item| item.key, keyExists,31 "The key was not found.", "MissingKey")32 let syncRule = Rule.build(Rule.withMessage(Rule.must(Rule.ruleFor("count", |item: Item| item.count),33 |count: Int| count > 0), "Count must be positive."))34 let empty: Async.AsyncValidator[Item] = Async.create()35 let validator = Async.add(Async.add(empty, asyncRule), Async.fromSync(syncRule))36 let bad = Async.validateAsync(&validator, Item{key: "absent", count: 0, tags: []}).await37 let good = Async.validateAsync(&validator, Item{key: "present", count: 1, tags: []}).await38 let tagsRule = Async.forEachWhereAsync("tags", |item: Item| item.tags, included, longEnough,39 "Tag \{CollectionIndex\} is short.", "TagLength")40 let tags = Async.validateAsync(&Async.add(Async.create(), tagsRule),41 Item{key: "present", count: 1, tags: ["skip", "x", "long"]}).await42 let selectedTags = Async.validateAsyncWith(&Async.add(Async.create(), tagsRule),43 Item{key: "present", count: 1, tags: ["x", "z"]},44 Validator.includeProperties(Validator.defaults(), ["tags[1]"])).await45 let unfiltered = Async.forEachAsync("tags", |item: Item| item.tags, rejectsSkipAndShort,46 "Tag \{CollectionIndex\} is short.", "TagLength")47 let every = Async.validateAsync(&Async.add(Async.create(), unfiltered),48 Item{key: "present", count: 1, tags: ["skip", "x", "longer"]}).await49 let filtered = Async.forEachWhereAsync("tags", |item: Item| item.tags, included, rejectsSkipAndShort,50 "Tag \{CollectionIndex\} is short.", "TagLength")51 let kept = Async.validateAsync(&Async.add(Async.create(), filtered),52 Item{key: "present", count: 1, tags: ["skip", "x", "longer"]}).await53 let named = Async.inRuleSet(Async.withSeverity(asyncRule, Validation.Warning), "lookup")54 let namedValidator = Async.add(Async.create(), named)55 let defaultNamed = Async.validateAsync(&namedValidator, Item{key: "absent", count: 0, tags: []}).await56 let chosenNamed = Async.validateAsyncWith(&namedValidator, Item{key: "absent", count: 0, tags: []},57 Validator.onlyRuleSets(["lookup"])).await58 let data: Rule.ContextData = mapOf([("expected", Json.Text("present"))])59 let contextRule = Async.withState(Async.mustAsyncWithContext("key", |item: Item| item.key,60 matchesContext, "Key differs from context.", "ContextKey"), "lookup")61 let contextValidator = Async.add(Async.create(), contextRule)62 let contextGood = Async.validateAsyncWithData(&contextValidator,63 Item{key: "present", count: 0, tags: []}, Validator.defaults(), data).await64 let contextBad = Async.validateAsyncWithData(&contextValidator,65 Item{key: "absent", count: 0, tags: []}, Validator.defaults(), data).await66 let lifted = Rule.build(Rule.mustWithContext(Rule.ruleFor("key", |item: Item| item.key),67 fn(_root: Item, key: Str, context: Rule.ContextData) -> Bool {68 match Map.get(&context, "expected") {69 case Some(Json.Text(expected)) => key == expected70 case _ => false71 }72 }))73 let liftedGood = Async.validateAsyncWithData(&Async.add(Async.create(), Async.fromSync(lifted)),74 Item{key: "present", count: 0, tags: []}, Validator.defaults(), data).await75 let descendantOnly = Async.validateAsyncWith(&contextValidator,76 Item{key: "absent", count: 0, tags: []},77 Validator.includeProperties(Validator.defaults(), ["key.detail"])).await78 let tagData: Rule.ContextData = mapOf([("expected", Json.Text("long")), ("ignored", Json.Text("skip"))])79 let contextTags = Async.forEachWhereAsyncWithContext("tags", |item: Item| item.tags,80 async fn(tag: Str, context: Rule.ContextData) -> Bool {81 match Map.get(&context, "ignored") {82 case Some(Json.Text(ignored)) => tag != ignored83 case _ => true84 }85 },86 async fn(_root: Item, tag: Str, context: Rule.ContextData) -> Bool {87 match Map.get(&context, "expected") {88 case Some(Json.Text(expected)) => tag == expected89 case _ => false90 }91 }, "Tag differs from context.", "TagContext")92 let contextTagFailures = Async.validateAsyncWithData(&Async.add(Async.create(), contextTags),93 Item{key: "present", count: 0, tags: ["skip", "x", "long"]}, Validator.defaults(), tagData).await94 let stopSecond = Async.mustAsync("other", |item: Item| item.key, keyExists,95 "The other key was not found.", "MissingOther")96 let flowing = Async.add(Async.add(Async.create(), asyncRule), stopSecond)97 let stopped = Async.stopOnFirst(flowing)98 let stoppedBad = Async.validateAsync(&stopped, Item{key: "absent", count: 1, tags: []}).await99 let flowingBad = Async.validateAsync(&flowing, Item{key: "absent", count: 1, tags: []}).await100 let checks = Test.suite("Async", &[101 Test.equals("async and sync failures keep order", &bad.errors.map(|failure: Validation.Failure| failure.propertyName),102 &["key", "count"]),103 Test.equals("successful async validation is valid", &good.isValid, &true),104 Test.equals("async filter preserves source index", &tags.errors[0].propertyName, &"tags[1]"),105 Test.equals("async collection message renders index", &tags.errors[0].errorMessage, &"Tag 1 is short."),106 Test.equals("async property selection keeps one element", &selectedTags.errors[0].propertyName, &"tags[1]"),107 Test.equals("async named set is absent from defaults", &defaultNamed.errors.length(), &0),108 Test.equals("async named set retains severity", &chosenNamed.errors[0].severity, &Validation.Warning),109 Test.equals("async check receives context", &contextGood.isValid, &true),110 Test.equals("async decorator preserves context and state", &contextBad.errors[0].customState, &"lookup"),111 Test.equals("lifted synchronous rule receives context", &liftedGood.isValid, &true),112 Test.equals("scalar async rule skips descendant selection", &descendantOnly.errors.length(), &0),113 Test.equals("async collection filter and check receive context", &contextTagFailures.errors.map(|failure: Validation.Failure| failure.propertyName),114 &["tags[1]"]),115 Test.equals("async forEach checks every element unfiltered", &every.errors.map(|failure: Validation.Failure| failure.propertyName),116 &["tags[0]", "tags[1]"]),117 Test.equals("async forEachWhere checks kept elements only", &kept.errors.map(|failure: Validation.Failure| failure.propertyName),118 &["tags[1]"]),119 Test.equals("async stopOnFirst keeps the first failure", &stoppedBad.errors.map(|failure: Validation.Failure| failure.propertyName),120 &["key"]),121 Test.equals("async rules flow without stopOnFirst", &flowingBad.errors.map(|failure: Validation.Failure| failure.propertyName),122 &["key", "other"])123 ])124 Test.report(&Test.run(&checks))125}126