Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-validator

Typed fluent validation rules for Pudu

0.1.1Apache-2.01

InstallClose

AsyncTest.pudu

Pudu126 lines7.4 KB

GitHub ↗
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