Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-validator

Typed fluent validation rules for Pudu

0.1.1Apache-2.01

InstallClose

Collection.pudu

Pudu151 lines6.5 KB

GitHub ↗
1/** @Validation.Rules.Collection.Module — checks arrays and indexed elements */2module PuduLangValidator.Rules.Collection34import Std.List as List5import PuduLangValidator.Result as Validation6import PuduLangValidator.Rule as Rule7import PuduLangValidator.Selection as Selection89/// Require an array to contain an element.10export fn notEmpty[T, V](builder: Rule.RuleBuilder[T, Array[V]]) -> Rule.RuleBuilder[T, Array[V]] {11  Rule.withCode(Rule.withMessage(Rule.must(builder, |items: Array[V]| !items.isEmpty()),12      "The collection must contain an item."), "NotEmpty")13}1415/// Require an array to contain no elements.16export fn empty[T, V](builder: Rule.RuleBuilder[T, Array[V]]) -> Rule.RuleBuilder[T, Array[V]] {17  Rule.withCode(Rule.withMessage(Rule.must(builder, |items: Array[V]| items.isEmpty()),18      "The collection must be empty."), "Empty")19}2021/// Require an array count within inclusive bounds.22export fn countBetween[T, V](builder: Rule.RuleBuilder[T, Array[V]], low: Int, high: Int) -> Rule.RuleBuilder[T, Array[V]] {23  let checked = Rule.withCode(Rule.withMessage(Rule.must(builder, |items: Array[V]| items.length() >= low && items.length() <= high),24      "The collection must contain from \{From\} through \{To\} items."), "CountBetween")25  let bounded = Rule.withArgument(Rule.withArgument(checked, "From", show(low)), "To", show(high))26  Rule.withArgumentFrom(bounded, "TotalCount", fn(_root: T, items: Array[V]) -> Str { show(items.length()) })27}2829/// Apply one element predicate at every original array index.30export fn forEach[T, V](name: Str, select: fn(T) -> Array[V], test: fn(V) -> Bool, message: Str) -> Rule.Rule[T] {31  forEachWhere(name, select, fn(_item: V) -> Bool { true }, test, message)32}3334/// Apply one element predicate where the filter admits the item.35export fn forEachWhere[T, V](36  name: Str, select: fn(T) -> Array[V], keep: fn(V) -> Bool,37  test: fn(V) -> Bool, message: Str38) -> Rule.Rule[T] {39  forEachWhereIndexed(name, select, keep, test, message,40    fn(index: Int, _item: V) -> Str { show(index) })41}4243/// Name each failing element with a caller-supplied index key.44export fn forEachWhereIndexed[T, V](45  name: Str, select: fn(T) -> Array[V], keep: fn(V) -> Bool,46  test: fn(V) -> Bool, message: Str, indexer: fn(Int, V) -> Str47) -> Rule.Rule[T] {48  Rule.Rule{name: name, ruleSets: [], run: fn(root: T, _sets: Array[Str], _defaults: Bool, properties: Array[Str], _data: Rule.ContextData) -> Array[Validation.Failure] {49      let items = select(root)50      var failures: Array[Validation.Failure] = []51      var index = 052      for item in items {53        let path = name + "[" + indexer(index, item) + "]"54        if pathSelected(path, &properties) && keep(item) && !test(item) {55          let rendered = message.replace("\{CollectionIndex\}", show(index))56            .replace("\{PropertyPath\}", path)57            .replace("\{PropertyName\}", name)58          failures = failures.push(Validation.Failure {59              propertyName: path, errorMessage: rendered, errorCode: "ForEach",60              severity: Validation.Error, customState: ""61            })62        }63        index = index + 164      }65      failures66    } }67}6869/** @Validation.Rules.Collection.ElementBuilder — chains checks per source element */70export type ElementBuilder[T, V] = {71  name: Str,72  select: fn(T) -> Array[V],73  keep: fn(V) -> Bool,74  indexer: fn(Int, V) -> Str,75  checks: Rule.RuleBuilder[(T, V), V]76}7778/// Start an element rule with a typed array selector.79export fn ruleForEach[T, V](name: Str, select: fn(T) -> Array[V]) -> ElementBuilder[T, V] {80  let checks: Rule.RuleBuilder[(T, V), V] = Rule.ruleFor(name, |pair: (T, V)| pair[1])81  ElementBuilder {82    name: name, select: select,83    keep: fn(_item: V) -> Bool { true },84    indexer: fn(index: Int, _item: V) -> Str { show(index) },85    checks: checks86  }87}8889/// Add one predicate to each selected element's chain.90export fn must[T, V](builder: ElementBuilder[T, V], test: fn(V) -> Bool) -> ElementBuilder[T, V] {91  ElementBuilder{..builder, checks: Rule.must(builder.checks, test)}92}9394/// Set the latest element check's message.95export fn withMessage[T, V](builder: ElementBuilder[T, V], message: Str) -> ElementBuilder[T, V] {96  ElementBuilder{..builder, checks: Rule.withMessage(builder.checks, message)}97}9899/// Set the latest element check's stable code.100export fn withCode[T, V](builder: ElementBuilder[T, V], code: Str) -> ElementBuilder[T, V] {101  ElementBuilder{..builder, checks: Rule.withCode(builder.checks, code)}102}103104/// Retain source indices while skipping elements outside the filter.105export fn whereItems[T, V](builder: ElementBuilder[T, V], keep: fn(V) -> Bool) -> ElementBuilder[T, V] {106  ElementBuilder{..builder, keep: keep}107}108109/// Render a caller-supplied bracket key for each failure path.110export fn withIndex[T, V](builder: ElementBuilder[T, V], indexer: fn(Int, V) -> Str) -> ElementBuilder[T, V] {111  ElementBuilder{..builder, indexer: indexer}112}113114/// Stop each element's chain at its first failure.115export fn stopOnFirst[T, V](builder: ElementBuilder[T, V]) -> ElementBuilder[T, V] {116  ElementBuilder{..builder, checks: Rule.stopOnFirst(builder.checks)}117}118119/// Tag this element rule with a named rule set.120export fn inRuleSet[T, V](builder: ElementBuilder[T, V], name: Str) -> ElementBuilder[T, V] {121  ElementBuilder{..builder, checks: Rule.inRuleSet(builder.checks, name)}122}123124/// Freeze an element chain as an indexed source rule.125export fn buildEach[T, V](builder: ElementBuilder[T, V]) -> Rule.Rule[T] {126  let name = builder.name127  let sets = builder.checks.ruleSets128  Rule.Rule{name: name, ruleSets: sets, run: fn(root: T, selected: Array[Str], includeDefault: Bool, properties: Array[Str], data: Rule.ContextData) -> Array[Validation.Failure] {129      var failures: Array[Validation.Failure] = []130      var index = 0131      for item in builder.select(root) {132        let path = name + "[" + builder.indexer(index, item) + "]"133        if pathSelected(path, &properties) && builder.keep(item) {134          let itemRule = Rule.build(Rule.overridePropertyName(builder.checks, path))135          for failure in itemRule.run((root, item), selected, includeDefault, properties, data) {136            failures = failures.push(Validation.Failure {137                ..failure, errorMessage: failure.errorMessage.replace("\{CollectionIndex\}", show(index))138              })139          }140        }141        index = index + 1142      }143      failures144    } }145}146147/// Whether a selected property reaches this source element.148fn pathSelected(path: Str, properties: &Array[Str]) -> Bool {149  properties.isEmpty() || List.any(properties, |wanted: Str| Selection.covers(wanted, path))150}151