
Collection.pudu
Pudu151 lines6.5 KB
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 Selection8910export 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}141516export 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}202122export 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}282930export 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}333435export 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}424344export 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}777879export 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}888990export 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}939495export fn withMessage[T, V](builder: ElementBuilder[T, V], message: Str) -> ElementBuilder[T, V] {96 ElementBuilder{..builder, checks: Rule.withMessage(builder.checks, message)}97}9899100export fn withCode[T, V](builder: ElementBuilder[T, V], code: Str) -> ElementBuilder[T, V] {101 ElementBuilder{..builder, checks: Rule.withCode(builder.checks, code)}102}103104105export fn whereItems[T, V](builder: ElementBuilder[T, V], keep: fn(V) -> Bool) -> ElementBuilder[T, V] {106 ElementBuilder{..builder, keep: keep}107}108109110export fn withIndex[T, V](builder: ElementBuilder[T, V], indexer: fn(Int, V) -> Str) -> ElementBuilder[T, V] {111 ElementBuilder{..builder, indexer: indexer}112}113114115export fn stopOnFirst[T, V](builder: ElementBuilder[T, V]) -> ElementBuilder[T, V] {116 ElementBuilder{..builder, checks: Rule.stopOnFirst(builder.checks)}117}118119120export fn inRuleSet[T, V](builder: ElementBuilder[T, V], name: Str) -> ElementBuilder[T, V] {121 ElementBuilder{..builder, checks: Rule.inRuleSet(builder.checks, name)}122}123124125export 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}146147148fn pathSelected(path: Str, properties: &Array[Str]) -> Bool {149 properties.isEmpty() || List.any(properties, |wanted: Str| Selection.covers(wanted, path))150}151