Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-validator

Typed fluent validation rules for Pudu

0.1.1Apache-2.01

InstallClose

Async.pudu

Pudu321 lines13.9 KB

GitHub ↗
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}2526/// Build an asynchronous check for a typed property.27export 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}3536/// Build an awaited property check that may read immutable root data.37export 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}5354/// Check every array element asynchronously in source order.55export 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}6162/// Await a filter and check for each selected array element.63export 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}7273/// Await a context-aware filter and predicate for each selected item.74export 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}9798/// Lift a synchronous rule into an async validator.99export 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}106107/// Run this asynchronous rule in a named rule set.108export 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}111112/// Classify every failure from this asynchronous rule.113export fn withSeverity[T](rule: AsyncRule[T], severity: Validation.Severity) -> AsyncRule[T] {114  withSeverityFrom(rule, fn(_root: T) -> Validation.Severity { severity })115}116117/// Classify this rule's failures from the root source.118export fn withSeverityFrom[T](rule: AsyncRule[T], severity: fn(T) -> Validation.Severity) -> AsyncRule[T] {119  AsyncRule{..rule, severityOverride: Some(severity)}120}121122/// Attach application state to every failure from this rule.123export fn withState[T](rule: AsyncRule[T], state: Str) -> AsyncRule[T] {124  withStateFrom(rule, fn(_root: T) -> Str { state })125}126127/// Attach source-dependent state to this rule's failures.128export fn withStateFrom[T](rule: AsyncRule[T], state: fn(T) -> Str) -> AsyncRule[T] {129  AsyncRule{..rule, stateOverride: Some(state)}130}131132/// Run an asynchronous rule only when the source condition holds.133export 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}140141/// Await a condition before running an asynchronous rule.142export 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}149150/// Await application code that reports structured failures.151export 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}158159/// Await a child validator and prefix its failure paths.160export 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}167168/// Skip an absent child and await validation when a value is present.169export 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}183184/// Await one child validator per selected array element in source order.185export 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}205206/// A validator with no asynchronous rules.207export 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}211212/// Return Some failures to stop, including an empty success, or None to run rules.213export 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}219220/// Await root and context checks before selecting asynchronous rules.221export 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}226227/// Append an asynchronous rule in execution order.228export fn add[T](validator: AsyncValidator[T], rule: AsyncRule[T]) -> AsyncValidator[T] {229  AsyncValidator{..validator, rules: validator.rules.push(rule)}230}231232/// Stop after the first asynchronous rule that reports failures.233export fn stopOnFirst[T](validator: AsyncValidator[T]) -> AsyncValidator[T] {234  AsyncValidator{..validator, stop: true}235}236237/// Await unnamed rules in declaration order and collect failures.238export async fn validateAsync[T](validator: &AsyncValidator[T], root: T) -> Validation.ValidationResult {239  validateAsyncWith(validator, root, Validator.defaults()).await240}241242/// Await selected rules in declaration order and collect failures.243export async fn validateAsyncWith[T](244  validator: &AsyncValidator[T], root: T, options: Validator.Options245) -> Validation.ValidationResult {246  validateAsyncWithData(validator, root, options, mapOf([])).await247}248249/// Await selected rules with immutable application context data.250export 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}280281/// Whether an indexed element is part of property selection.282fn selectedElement(path: Str, properties: &Array[Str]) -> Bool {283  properties.isEmpty() || List.any(properties, |wanted: Str| Selection.overlaps(wanted, path))284}285286/// Whether a requested path selects this scalar array element.287fn selectedScalarElement(path: Str, properties: &Array[Str]) -> Bool {288  properties.isEmpty() || List.any(properties, |wanted: Str| Selection.covers(wanted, path))289}290291/// Rule-set tags visible through a nested async validator.292fn 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}302303/// Translate root selections into paths relative to a child.304fn 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}313314/// Add the parent path to each child failure exactly once.315fn 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