
Async.pudu
Pudu321 lines13.9 KB
1/** @Validation.Async.Engine — awaits typed validation checks */2module PuduLangValidator.Async34import Std.List as List5import PuduLangValidator.Result as Validation6import PuduLangValidator.Rule as Rule7import PuduLangValidator.Selection as Selection8import PuduLangValidator.Validator as Validator910/** @Validation.Async.Rule — owns one asynchronous source check */11export type AsyncRule[T] = {12 name: Str,13 ruleSets: Array[Str],14 severityOverride: Option[fn(T) -> Validation.Severity],15 stateOverride: Option[fn(T) -> Str],16 run: async fn(T, Validator.Options, Rule.ContextData) -> Array[Validation.Failure]17}1819/** @Validation.Async.Validator — orders asynchronous and lifted rules */20export type AsyncValidator[T] = {21 rules: Array[AsyncRule[T]],22 stop: Bool,23 preflight: async fn(T, Rule.ContextData) -> Option[Array[Validation.Failure]]24}252627export fn mustAsync[T, V](28 name: Str, select: fn(T) -> V, test: async fn(T, V) -> Bool,29 message: Str, code: Str30) -> AsyncRule[T] {31 mustAsyncWithContext(name, select,32 async fn(root: T, value: V, _data: Rule.ContextData) -> Bool { test(root, value).await },33 message, code)34}353637export fn mustAsyncWithContext[T, V](38 name: Str, select: fn(T) -> V, test: async fn(T, V, Rule.ContextData) -> Bool,39 message: Str, code: Str40) -> AsyncRule[T] {41 AsyncRule{name: name, ruleSets: [], severityOverride: None, stateOverride: None, run: async fn(root: T, options: Validator.Options, data: Rule.ContextData) -> Array[Validation.Failure] {42 if !options.properties.isEmpty() && !List.any(&options.properties, |wanted: Str| Selection.covers(wanted, name)) { return [] }43 let value = select(root)44 if test(root, value, data).await { [] } else { [Validation.Failure {45 propertyName: name,46 errorMessage: message.replace("\{PropertyName\}", name)47 .replace("\{PropertyPath\}", name)48 .replace("\{PropertyValue\}", value.toText()),49 errorCode: code, severity: Validation.Error, customState: ""50 }] }51 } }52}535455export fn forEachAsync[T, V](56 name: Str, select: fn(T) -> Array[V], test: async fn(T, V) -> Bool,57 message: Str, code: Str58) -> AsyncRule[T] {59 forEachWhereAsync(name, select, async fn(_item: V) -> Bool { true }, test, message, code)60}616263export fn forEachWhereAsync[T, V](64 name: Str, select: fn(T) -> Array[V], keep: async fn(V) -> Bool,65 test: async fn(T, V) -> Bool, message: Str, code: Str66) -> AsyncRule[T] {67 forEachWhereAsyncWithContext(name, select,68 async fn(item: V, _data: Rule.ContextData) -> Bool { keep(item).await },69 async fn(root: T, item: V, _data: Rule.ContextData) -> Bool { test(root, item).await },70 message, code)71}727374export fn forEachWhereAsyncWithContext[T, V](75 name: Str, select: fn(T) -> Array[V], keep: async fn(V, Rule.ContextData) -> Bool,76 test: async fn(T, V, Rule.ContextData) -> Bool, message: Str, code: Str77) -> AsyncRule[T] {78 AsyncRule{name: name, ruleSets: [], severityOverride: None, stateOverride: None, run: async fn(root: T, options: Validator.Options, data: Rule.ContextData) -> Array[Validation.Failure] {79 var failures: Array[Validation.Failure] = []80 var index = 081 for item in select(root) {82 let path = name + "[" + show(index) + "]"83 if selectedScalarElement(path, &options.properties) && keep(item, data).await && !test(root, item, data).await {84 failures = failures.push(Validation.Failure {85 propertyName: path,86 errorMessage: message.replace("\{PropertyName\}", name)87 .replace("\{PropertyPath\}", path)88 .replace("\{CollectionIndex\}", show(index)),89 errorCode: code, severity: Validation.Error, customState: ""90 })91 }92 index = index + 193 }94 failures95 } }96}979899export fn fromSync[T](rule: Rule.Rule[T]) -> AsyncRule[T] {100 let name = rule.name101 let sets = rule.ruleSets102 AsyncRule{name: name, ruleSets: sets, severityOverride: None, stateOverride: None, run: async fn(root: T, options: Validator.Options, data: Rule.ContextData) -> Array[Validation.Failure] {103 rule.run(root, options.ruleSets, options.includeDefault, options.properties, data)104 } }105}106107108export fn inRuleSet[T](rule: AsyncRule[T], name: Str) -> AsyncRule[T] {109 if rule.ruleSets.contains(name) { rule } else { AsyncRule{..rule, ruleSets: rule.ruleSets.push(name)} }110}111112113export fn withSeverity[T](rule: AsyncRule[T], severity: Validation.Severity) -> AsyncRule[T] {114 withSeverityFrom(rule, fn(_root: T) -> Validation.Severity { severity })115}116117118export fn withSeverityFrom[T](rule: AsyncRule[T], severity: fn(T) -> Validation.Severity) -> AsyncRule[T] {119 AsyncRule{..rule, severityOverride: Some(severity)}120}121122123export fn withState[T](rule: AsyncRule[T], state: Str) -> AsyncRule[T] {124 withStateFrom(rule, fn(_root: T) -> Str { state })125}126127128export fn withStateFrom[T](rule: AsyncRule[T], state: fn(T) -> Str) -> AsyncRule[T] {129 AsyncRule{..rule, stateOverride: Some(state)}130}131132133export fn when[T](rule: AsyncRule[T], condition: fn(T) -> Bool) -> AsyncRule[T] {134 let name = rule.name135 let sets = rule.ruleSets136 AsyncRule{name: name, ruleSets: sets, severityOverride: rule.severityOverride, stateOverride: rule.stateOverride, run: async fn(root: T, options: Validator.Options, data: Rule.ContextData) -> Array[Validation.Failure] {137 if condition(root) { rule.run(root, options, data).await } else { [] }138 } }139}140141142export fn whenAsync[T](rule: AsyncRule[T], condition: async fn(T, Rule.ContextData) -> Bool) -> AsyncRule[T] {143 let name = rule.name144 let sets = rule.ruleSets145 AsyncRule{name: name, ruleSets: sets, severityOverride: rule.severityOverride, stateOverride: rule.stateOverride, run: async fn(root: T, options: Validator.Options, data: Rule.ContextData) -> Array[Validation.Failure] {146 if condition(root, data).await { rule.run(root, options, data).await } else { [] }147 } }148}149150151export fn customAsync[T](152 name: Str, check: async fn(T, Rule.ContextData) -> Array[Validation.Failure]153) -> AsyncRule[T] {154 AsyncRule{name: name, ruleSets: [], severityOverride: None, stateOverride: None, run: async fn(root: T, _options: Validator.Options, data: Rule.ContextData) -> Array[Validation.Failure] {155 check(root, data).await156 } }157}158159160export fn setValidatorAsync[T, V](name: Str, select: fn(T) -> V, child: AsyncValidator[V]) -> AsyncRule[T] {161 let sets = childSets(&child)162 AsyncRule{name: name, ruleSets: sets, severityOverride: None, stateOverride: None, run: async fn(root: T, options: Validator.Options, data: Rule.ContextData) -> Array[Validation.Failure] {163 let inner = Validator.Options{..options, properties: childProperties(name, &options.properties)}164 prefixFailures(name, &validateAsyncWithData(&child, select(root), inner, data).await.errors)165 } }166}167168169export fn setOptionalValidatorAsync[T, V](170 name: Str, select: fn(T) -> Option[V], child: AsyncValidator[V]171) -> AsyncRule[T] {172 let sets = childSets(&child)173 AsyncRule{name: name, ruleSets: sets, severityOverride: None, stateOverride: None, run: async fn(root: T, options: Validator.Options, data: Rule.ContextData) -> Array[Validation.Failure] {174 match select(root) {175 case Some(value) => {176 let inner = Validator.Options{..options, properties: childProperties(name, &options.properties)}177 prefixFailures(name, &validateAsyncWithData(&child, value, inner, data).await.errors)178 }179 case None => []180 }181 } }182}183184185export fn forEachValidatorAsync[T, V](186 name: Str, select: fn(T) -> Array[V], child: AsyncValidator[V]187) -> AsyncRule[T] {188 let sets = childSets(&child)189 AsyncRule{name: name, ruleSets: sets, severityOverride: None, stateOverride: None, run: async fn(root: T, options: Validator.Options, data: Rule.ContextData) -> Array[Validation.Failure] {190 var failures: Array[Validation.Failure] = []191 var index = 0192 for value in select(root) {193 let path = name + "[" + show(index) + "]"194 if selectedElement(path, &options.properties) {195 let inner = Validator.Options{..options, properties: childProperties(path, &options.properties)}196 for failure in prefixFailures(path, &validateAsyncWithData(&child, value, inner, data).await.errors) {197 failures = failures.push(failure)198 }199 }200 index = index + 1201 }202 failures203 } }204}205206207export fn create[T]() -> AsyncValidator[T] {208 AsyncValidator{rules: [], stop: false,209 preflight: async fn(_root: T, _data: Rule.ContextData) -> Option[Array[Validation.Failure]] { None } }210}211212213export fn withPreValidation[T](214 validator: AsyncValidator[T], preflight: fn(T) -> Option[Array[Validation.Failure]]215) -> AsyncValidator[T] {216 withPreValidationAsync(validator,217 async fn(root: T, _data: Rule.ContextData) -> Option[Array[Validation.Failure]] { preflight(root) })218}219220221export fn withPreValidationAsync[T](222 validator: AsyncValidator[T], preflight: async fn(T, Rule.ContextData) -> Option[Array[Validation.Failure]]223) -> AsyncValidator[T] {224 AsyncValidator{..validator, preflight: preflight}225}226227228export fn add[T](validator: AsyncValidator[T], rule: AsyncRule[T]) -> AsyncValidator[T] {229 AsyncValidator{..validator, rules: validator.rules.push(rule)}230}231232233export fn stopOnFirst[T](validator: AsyncValidator[T]) -> AsyncValidator[T] {234 AsyncValidator{..validator, stop: true}235}236237238export async fn validateAsync[T](validator: &AsyncValidator[T], root: T) -> Validation.ValidationResult {239 validateAsyncWith(validator, root, Validator.defaults()).await240}241242243export async fn validateAsyncWith[T](244 validator: &AsyncValidator[T], root: T, options: Validator.Options245) -> Validation.ValidationResult {246 validateAsyncWithData(validator, root, options, mapOf([])).await247}248249250export async fn validateAsyncWithData[T](251 validator: &AsyncValidator[T], root: T, options: Validator.Options, data: Rule.ContextData252) -> Validation.ValidationResult {253 if let Some(early) = validator.preflight(root, data).await { return Validation.fromFailures(early) }254 var failures: Array[Validation.Failure] = []255 for rule in validator.rules {256 if Validator.accepts(rule.name, &rule.ruleSets, &options) {257 let found = rule.run(root, options, data).await258 if !found.isEmpty() {259 let severity = match rule.severityOverride {260 case Some(factory) => Some(factory(root))261 case None => None262 }263 let state = match rule.stateOverride {264 case Some(factory) => Some(factory(root))265 case None => None266 }267 for failure in found {268 failures = failures.push(Validation.Failure {269 ..failure,270 severity: match severity { case Some(selected) => selected case None => failure.severity },271 customState: match state { case Some(selected) => selected case None => failure.customState }272 })273 }274 }275 if validator.stop && !found.isEmpty() { break }276 }277 }278 Validation.fromFailures(failures)279}280281282fn selectedElement(path: Str, properties: &Array[Str]) -> Bool {283 properties.isEmpty() || List.any(properties, |wanted: Str| Selection.overlaps(wanted, path))284}285286287fn selectedScalarElement(path: Str, properties: &Array[Str]) -> Bool {288 properties.isEmpty() || List.any(properties, |wanted: Str| Selection.covers(wanted, path))289}290291292fn childSets[V](child: &AsyncValidator[V]) -> Array[Str] {293 var sets: Array[Str] = []294 for rule in child.rules {295 for name in rule.ruleSets {296 if !sets.contains(name) { sets = sets.push(name) }297 }298 }299 if List.any(&child.rules, |rule: AsyncRule[V]| rule.ruleSets.isEmpty()) { sets = sets.push("default") }300 sets301}302303304fn childProperties(name: Str, properties: &Array[Str]) -> Array[Str] {305 if name.isEmpty() { return *properties }306 var selected: Array[Str] = []307 for wanted in *properties {308 if Selection.covers(wanted, name) { return [] }309 if let Some(relative) = Selection.relative(wanted, name) { selected = selected.push(relative) }310 }311 selected312}313314315fn prefixFailures(name: Str, failures: &Array[Validation.Failure]) -> Array[Validation.Failure] {316 failures.map(|failure: Validation.Failure| Validation.Failure {317 ..failure,318 propertyName: if name.isEmpty() { failure.propertyName } else if failure.propertyName.isEmpty() { name } else { name + "." + failure.propertyName }319 })320}321