Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-validator

Typed fluent validation rules for Pudu

0.1.1Apache-2.01

InstallClose

Validator.pudu

Pudu112 lines4.1 KB

GitHub ↗
1/** @Validation.Validator.Aggregate — composes ordered validation rules */2module PuduLangValidator.Validator34import Std.List as List5import PuduLangValidator.Result as Validation6import PuduLangValidator.Rule as Rule7import PuduLangValidator.Selection as Selection89/** @Validation.Validator.Options — selects named rule subsets */10export type Options = { properties: Array[Str], ruleSets: Array[Str], includeDefault: Bool }1112/** @Validation.Validator.Validator — holds rules for one source type */13export type Validator[T] = {14  rules: Array[Rule.Rule[T]],15  stop: Bool,16  preflight: fn(T) -> Option[Array[Validation.Failure]]17}1819/// A validator with no rules.20export fn create[T]() -> Validator[T] {21  Validator{rules: [], stop: false, preflight: fn(_root: T) -> Option[Array[Validation.Failure]] { None }}22}2324/// Return preflight failures before ordinary rules, or `None` to continue.25export fn withPreValidation[T](26  validator: Validator[T], preflight: fn(T) -> Option[Array[Validation.Failure]]27) -> Validator[T] {28  Validator{..validator, preflight: preflight}29}3031/// Append a built rule without changing existing rule order.32export fn add[T](validator: Validator[T], rule: Rule.Rule[T]) -> Validator[T] {33  Validator{..validator, rules: validator.rules.push(rule)}34}3536/// Append another validator's rules in their original order.37export fn include[T](validator: Validator[T], other: &Validator[T]) -> Validator[T] {38  var rules = validator.rules39  for rule in other.rules { rules = rules.push(rule) }40  Validator{..validator, rules: rules}41}4243/// Stop after the first rule that reports a failure.44export fn stopOnFirst[T](validator: Validator[T]) -> Validator[T] {45  Validator{..validator, stop: true}46}4748/// Select every property and the unnamed rules.49export fn defaults() -> Options { Options{properties: [], ruleSets: [], includeDefault: true} }5051/// Select named rule sets without unnamed rules.52export fn onlyRuleSets(names: Array[Str]) -> Options {53  Options{properties: [], ruleSets: names, includeDefault: false}54}5556/// Select every named rule set and unnamed rules.57export fn allRuleSets() -> Options {58  Options{properties: [], ruleSets: ["*"], includeDefault: true}59}6061/// Restrict a selection to these property paths.62export fn includeProperties(options: Options, names: Array[Str]) -> Options {63  Options{..options, properties: names}64}6566/// Also run rules outside named rule sets.67export fn includeRulesNotInRuleSet(options: Options) -> Options {68  Options{..options, includeDefault: true}69}7071/// Run the default rules against one source value.72export fn validate[T](validator: &Validator[T], root: T) -> Validation.ValidationResult {73  validateWith(validator, root, defaults())74}7576/// Run selected rules and preserve their declaration order.77export fn validateWith[T](validator: &Validator[T], root: T, options: Options) -> Validation.ValidationResult {78  validateWithData(validator, root, options, mapOf([]))79}8081/// Run selected rules with immutable application context data.82export fn validateWithData[T](83  validator: &Validator[T], root: T, options: Options, data: Rule.ContextData84) -> Validation.ValidationResult {85  if let Some(early) = validator.preflight(root) { return Validation.fromFailures(early) }86  var failures: Array[Validation.Failure] = []87  for rule in validator.rules {88    if accepts(rule.name, &rule.ruleSets, &options) {89      let found = rule.run(root, options.ruleSets, options.includeDefault, options.properties, data)90      for failure in found { failures = failures.push(failure) }91      if validator.stop && !found.isEmpty() { break }92    }93  }94  Validation.fromFailures(failures)95}9697/// Whether a rule path and its sets meet the requested selection.98export fn accepts(path: Str, ruleSets: &Array[Str], options: &Options) -> Bool {99  let pathAllowed = if options.properties.isEmpty() {100    true101  } else {102    List.any(&options.properties, |name: Str| Selection.overlaps(name, path))103  }104  let setAllowed = if ruleSets.isEmpty() {105    options.includeDefault106  } else {107    options.ruleSets.contains("*") || List.any(ruleSets, |name: Str|108      options.ruleSets.contains(name) || (name == "default" && options.includeDefault))109  }110  pathAllowed && setAllowed111}112