
Rule.pudu
Pudu314 lines11.8 KB
1/** @Validation.Rule.Engine — evaluates typed property checks */2module PuduLangValidator.Rule34import Std.Json as Json5import Std.List as List6import Std.Text as Text7import PuduLangValidator.Result as Validation8import PuduLangValidator.Selection as Selection910/** @Validation.Rule.ContextData — shares immutable application data */11export type ContextData = Map[Str, Json.Json]1213/** @Validation.Rule.Check — binds predicate and failure metadata */14export type Check[T, V] = {15 test: fn(T, V, ContextData) -> Option[Array[(Str, Str)]],16 condition: fn(T) -> Bool,17 message: Str,18 messageFactory: Option[fn(T, V) -> Str],19 arguments: Array[(Str, fn(T, V) -> Str)],20 code: Str,21 severity: Validation.Severity,22 severityFactory: Option[fn(T, V) -> Validation.Severity],23 state: Str,24 stateFactory: Option[fn(T, V) -> Str]25}2627/** @Validation.Rule.RuleBuilder — keeps property checks typed until build */28export type RuleBuilder[T, V] = {29 name: Str,30 displayName: Str,31 select: fn(T) -> V,32 checks: Array[Check[T, V]],33 ruleSets: Array[Str],34 stop: Bool35}3637/** @Validation.Rule.Rule — runs a property rule over its source */38export type Rule[T] = { name: Str, ruleSets: Array[Str], run: fn(T, Array[Str], Bool, Array[Str], ContextData) -> Array[Validation.Failure] }394041export fn ruleFor[T, V](name: Str, select: fn(T) -> V) -> RuleBuilder[T, V] {42 RuleBuilder{name: name, displayName: name, select: select, checks: [], ruleSets: [], stop: false}43}444546export fn must[T, V](builder: RuleBuilder[T, V], test: fn(V) -> Bool) -> RuleBuilder[T, V] {47 mustWith(builder, fn(_root: T, value: V) -> Bool { test(value) })48}495051export fn mustWith[T, V](builder: RuleBuilder[T, V], test: fn(T, V) -> Bool) -> RuleBuilder[T, V] {52 mustWithContext(builder, fn(root: T, value: V, _data: ContextData) -> Bool { test(root, value) })53}545556export fn mustWithContext[T, V](57 builder: RuleBuilder[T, V], test: fn(T, V, ContextData) -> Bool58) -> RuleBuilder[T, V] {59 addCheck(builder, fn(root: T, value: V, data: ContextData) -> Option[Array[(Str, Str)]] {60 if test(root, value, data) { None } else { Some([]) }61 })62}636465export fn mustWithArguments[T, V](66 builder: RuleBuilder[T, V], test: fn(T, V) -> Option[Array[(Str, Str)]]67) -> RuleBuilder[T, V] {68 addCheck(builder, fn(root: T, value: V, _data: ContextData) -> Option[Array[(Str, Str)]] {69 test(root, value)70 })71}7273fn addCheck[T, V](74 builder: RuleBuilder[T, V], test: fn(T, V, ContextData) -> Option[Array[(Str, Str)]]75) -> RuleBuilder[T, V] {76 let check = Check {77 test: test,78 condition: fn(_root: T) -> Bool { true },79 message: "The value of \{PropertyName\} did not satisfy its rule.",80 messageFactory: None,81 arguments: [],82 code: "Predicate",83 severity: Validation.Error,84 severityFactory: None,85 state: "",86 stateFactory: None87 }88 RuleBuilder{..builder, checks: builder.checks.push(check)}89}909192fn replaceLast[T, V](builder: RuleBuilder[T, V], replacement: Check[T, V]) -> RuleBuilder[T, V] {93 var checks: Array[Check[T, V]] = []94 var index = 095 let last = builder.checks.length() - 196 for check in builder.checks {97 checks = checks.push(if index == last { replacement } else { check })98 index = index + 199 }100 RuleBuilder{..builder, checks: checks}101}102103104export fn withMessage[T, V](builder: RuleBuilder[T, V], message: Str) -> RuleBuilder[T, V] {105 match List.last(&builder.checks) {106 case Some(check) => replaceLast(builder, Check{..check, message: message, messageFactory: None})107 case None => builder108 }109}110111112export fn withMessageFrom[T, V](builder: RuleBuilder[T, V], message: fn(T, V) -> Str) -> RuleBuilder[T, V] {113 match List.last(&builder.checks) {114 case Some(check) => replaceLast(builder, Check{..check, messageFactory: Some(message)})115 case None => builder116 }117}118119120export fn withArgument[T, V](builder: RuleBuilder[T, V], name: Str, value: Str) -> RuleBuilder[T, V] {121 withArgumentFrom(builder, name, fn(_root: T, _property: V) -> Str { value })122}123124125export fn withArgumentFrom[T, V](126 builder: RuleBuilder[T, V], name: Str, value: fn(T, V) -> Str127) -> RuleBuilder[T, V] {128 if name.isEmpty() || name == "PropertyName" || name == "PropertyPath" || name == "PropertyValue" { return builder }129 match List.last(&builder.checks) {130 case Some(check) => {131 var arguments: Array[(Str, fn(T, V) -> Str)] = []132 for entry in check.arguments {133 if entry[0] != name { arguments = arguments.push(entry) }134 }135 replaceLast(builder, Check{..check, arguments: arguments.push((name, value))})136 }137 case None => builder138 }139}140141142export fn withName[T, V](builder: RuleBuilder[T, V], name: Str) -> RuleBuilder[T, V] {143 RuleBuilder{..builder, displayName: name}144}145146147export fn overridePropertyName[T, V](builder: RuleBuilder[T, V], path: Str) -> RuleBuilder[T, V] {148 RuleBuilder{..builder, name: path}149}150151152export fn withCode[T, V](builder: RuleBuilder[T, V], code: Str) -> RuleBuilder[T, V] {153 match List.last(&builder.checks) {154 case Some(check) => replaceLast(builder, Check{..check, code: code})155 case None => builder156 }157}158159160export fn withSeverity[T, V](builder: RuleBuilder[T, V], severity: Validation.Severity) -> RuleBuilder[T, V] {161 match List.last(&builder.checks) {162 case Some(check) => replaceLast(builder, Check{..check, severity: severity, severityFactory: None})163 case None => builder164 }165}166167168export fn withSeverityFrom[T, V](169 builder: RuleBuilder[T, V], severity: fn(T, V) -> Validation.Severity170) -> RuleBuilder[T, V] {171 match List.last(&builder.checks) {172 case Some(check) => replaceLast(builder, Check{..check, severityFactory: Some(severity)})173 case None => builder174 }175}176177178export fn withState[T, V](builder: RuleBuilder[T, V], state: Str) -> RuleBuilder[T, V] {179 match List.last(&builder.checks) {180 case Some(check) => replaceLast(builder, Check{..check, state: state, stateFactory: None})181 case None => builder182 }183}184185186export fn withStateFrom[T, V](builder: RuleBuilder[T, V], state: fn(T, V) -> Str) -> RuleBuilder[T, V] {187 match List.last(&builder.checks) {188 case Some(check) => replaceLast(builder, Check{..check, stateFactory: Some(state)})189 case None => builder190 }191}192193194export fn when[T, V](builder: RuleBuilder[T, V], condition: fn(T) -> Bool) -> RuleBuilder[T, V] {195 RuleBuilder{..builder, checks: builder.checks.map(|check: Check[T, V]| Check {196 ..check, condition: fn(root: T) -> Bool { check.condition(root) && condition(root) }197 }) }198}199200201export fn unless[T, V](builder: RuleBuilder[T, V], condition: fn(T) -> Bool) -> RuleBuilder[T, V] {202 when(builder, fn(root: T) -> Bool { !condition(root) })203}204205206export fn whenCurrent[T, V](builder: RuleBuilder[T, V], condition: fn(T) -> Bool) -> RuleBuilder[T, V] {207 match List.last(&builder.checks) {208 case Some(check) => replaceLast(builder, Check {209 ..check, condition: fn(root: T) -> Bool { check.condition(root) && condition(root) }210 })211 case None => builder212 }213}214215216export fn unlessCurrent[T, V](builder: RuleBuilder[T, V], condition: fn(T) -> Bool) -> RuleBuilder[T, V] {217 whenCurrent(builder, fn(root: T) -> Bool { !condition(root) })218}219220221export fn stopOnFirst[T, V](builder: RuleBuilder[T, V]) -> RuleBuilder[T, V] {222 RuleBuilder{..builder, stop: true}223}224225226export fn inRuleSet[T, V](builder: RuleBuilder[T, V], name: Str) -> RuleBuilder[T, V] {227 if builder.ruleSets.contains(name) { builder } else { RuleBuilder{..builder, ruleSets: builder.ruleSets.push(name)} }228}229230231export fn build[T, V](builder: RuleBuilder[T, V]) -> Rule[T] {232 let name = builder.name233 let displayName = builder.displayName234 let sets = builder.ruleSets235 Rule{name: name, ruleSets: sets, run: fn(root: T, _sets: Array[Str], _defaults: Bool, properties: Array[Str], data: ContextData) -> Array[Validation.Failure] {236 if !properties.isEmpty() && !List.any(&properties, |wanted: Str| Selection.covers(wanted, name)) { return [] }237 let value = builder.select(root)238 var failures: Array[Validation.Failure] = []239 for check in builder.checks {240 let outcome = if check.condition(root) { check.test(root, value, data) } else { None }241 if let Some(captured) = outcome {242 let template = match check.messageFactory {243 case Some(factory) => factory(root, value)244 case None => check.message245 }246 let message = render(template, displayName, name, root, value, &check.arguments, &captured)247 let severity = match check.severityFactory {248 case Some(factory) => factory(root, value)249 case None => check.severity250 }251 let state = match check.stateFactory {252 case Some(factory) => factory(root, value)253 case None => check.state254 }255 failures = failures.push(Validation.Failure {256 propertyName: name, errorMessage: message, errorCode: check.code,257 severity: severity, customState: state258 })259 if builder.stop { break }260 }261 }262 failures263 } }264}265266267fn render[T, V](268 template: Str, displayName: Str, path: Str, root: T, value: V,269 arguments: &Array[(Str, fn(T, V) -> Str)], captured: &Array[(Str, Str)]270) -> Str {271 let chars = template.chars()272 var pieces: Array[Str] = []273 var literalStart = 0274 var index = 0275 while index < chars.length() {276 if chars[index] != '{' {277 index = index + 1278 continue279 }280 var close = index + 1281 while close < chars.length() && chars[close] != '}' { close = close + 1 }282 if close == chars.length() { break }283 let key = Text.take(Text.drop(template, index + 1), close - index - 1)284 match argument(key, displayName, path, root, value, arguments, captured) {285 case Some(replacement) => {286 pieces = pieces.push(Text.take(Text.drop(template, literalStart), index - literalStart))287 pieces = pieces.push(replacement)288 index = close + 1289 literalStart = index290 }291 case None => { index = close + 1 }292 }293 }294 pieces = pieces.push(Text.drop(template, literalStart))295 Text.join(&pieces, "")296}297298299fn argument[T, V](300 name: Str, displayName: Str, path: Str, root: T, value: V,301 arguments: &Array[(Str, fn(T, V) -> Str)], captured: &Array[(Str, Str)]302) -> Option[Str] {303 if name == "PropertyName" { return Some(displayName) }304 if name == "PropertyPath" { return Some(path) }305 if name == "PropertyValue" { return Some(value.toText()) }306 for entry in *arguments {307 if entry[0] == name { return Some(entry[1](root, value)) }308 }309 for entry in *captured {310 if entry[0] == name { return Some(entry[1]) }311 }312 None313}314