Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-validator

Typed fluent validation rules for Pudu

0.1.1Apache-2.01

InstallClose

Text.pudu

Pudu100 lines4.7 KB

GitHub ↗
1/** @Validation.Rules.Text.Module — checks textual property constraints */2module PuduLangValidator.Rules.Text34import Std.Char as Char5import Std.List as List6import Std.Regex as Regex7import Std.Text as Text8import PuduLangValidator.Rule as Rule910/// Require at least one non-whitespace character.11export fn notEmpty[T](builder: Rule.RuleBuilder[T, Str]) -> Rule.RuleBuilder[T, Str] {12  Rule.withCode(Rule.withMessage(Rule.must(builder, |value: Str| !value.trim().isEmpty()), "Text is required."), "NotEmpty")13}1415/// Require no non-whitespace characters.16export fn empty[T](builder: Rule.RuleBuilder[T, Str]) -> Rule.RuleBuilder[T, Str] {17  Rule.withCode(Rule.withMessage(Rule.must(builder, |value: Str| value.trim().isEmpty()), "Text must be empty."), "Empty")18}1920/// Require a Unicode-scalar length within inclusive bounds.21export fn length[T](builder: Rule.RuleBuilder[T, Str], minimum: Int, maximum: Int) -> Rule.RuleBuilder[T, Str] {22  let checked = Rule.withCode(Rule.withMessage(23      Rule.must(builder, |value: Str| value.length() >= minimum && value.length() <= maximum),24      "Text must contain between \{MinLength\} and \{MaxLength\} characters."25    ), "Length")26  let bounded = Rule.withArgument(Rule.withArgument(checked, "MinLength", show(minimum)), "MaxLength", show(maximum))27  Rule.withArgumentFrom(bounded, "TotalLength", fn(_root: T, value: Str) -> Str { show(value.length()) })28}2930/// Require at least the stated number of Unicode scalars.31export fn minimumLength[T](builder: Rule.RuleBuilder[T, Str], minimum: Int) -> Rule.RuleBuilder[T, Str] {32  let checked = Rule.withCode(Rule.withMessage(Rule.must(builder, |value: Str| value.length() >= minimum),33      "Text must contain at least \{MinLength\} characters."), "MinimumLength")34  Rule.withArgumentFrom(Rule.withArgument(checked, "MinLength", show(minimum)),35    "TotalLength", fn(_root: T, value: Str) -> Str { show(value.length()) })36}3738/// Require no more than the stated number of Unicode scalars.39export fn maximumLength[T](builder: Rule.RuleBuilder[T, Str], maximum: Int) -> Rule.RuleBuilder[T, Str] {40  let checked = Rule.withCode(Rule.withMessage(Rule.must(builder, |value: Str| value.length() <= maximum),41      "Text must contain no more than \{MaxLength\} characters."), "MaximumLength")42  Rule.withArgumentFrom(Rule.withArgument(checked, "MaxLength", show(maximum)),43    "TotalLength", fn(_root: T, value: Str) -> Str { show(value.length()) })44}4546/// Require exactly one internal `@`; this checks shape, not delivery.47export fn emailAddress[T](builder: Rule.RuleBuilder[T, Str]) -> Rule.RuleBuilder[T, Str] {48  Rule.withCode(Rule.withMessage(Rule.must(builder, |value: Str|49        Text.countOccurrences(value, "@") == 1 && !value.startsWith("@") && !value.endsWith("@")50      ), "Text must have an internal @ sign."), "EmailAddress")51}5253/// Compile a pattern once and attach it, or return the pattern error. Braces interpolate in54/// string literals, so a quantifier is written with escaped braces (`\{2\}`).55export fn matches[T](builder: Rule.RuleBuilder[T, Str], pattern: Str) -> Result[Rule.RuleBuilder[T, Str], Regex.RegexError] {56  let compiled = Regex.compile(pattern) ?57  Ok(Rule.withArgument(Rule.withCode(Rule.withMessage(58          Rule.must(builder, |value: Str| Regex.isMatch(&compiled, value)),59          "Text does not match the required pattern."60        ), "Matches"), "RegularExpression", pattern))61}6263/// Require a plausible card number with a valid Luhn checksum.64export fn creditCard[T](builder: Rule.RuleBuilder[T, Str]) -> Rule.RuleBuilder[T, Str] {65  Rule.withCode(Rule.withMessage(Rule.must(builder, |value: Str| isCardNumber(value)),66      "Card number failed its checksum."), "CreditCard")67}6869/// Require one of the supplied enum names.70export fn enumName[T](71  builder: Rule.RuleBuilder[T, Str], names: Array[Str], caseSensitive: Bool72) -> Rule.RuleBuilder[T, Str] {73  Rule.withCode(Rule.withMessage(Rule.must(builder, |value: Str|74        List.any(&names, |name: Str| if caseSensitive { value == name } else { value.toLower() == name.toLower() })75      ), "Text is not a recognized name."), "EnumName")76}7778/// Count digits from the right, doubling alternating positions.79fn isCardNumber(value: Str) -> Bool {80  var digits: Array[Int] = []81  for character in value.chars() {82    if character == ' ' || character == '-' { continue }83    if character < '0' || character > '9' { return false }84    let digit = match Char.toDigit(character) {85      case Some(found) => found86      case None => 087    }88    digits = digits.push(digit)89  }90  if digits.length() < 12 || digits.length() > 19 { return false }91  var sum = 092  var index = 093  for digit in digits.reverse() {94    let doubled = if index % 2 == 1 { digit * 2 } else { digit }95    sum = sum + if doubled >= 10 { doubled - 9 } else { doubled }96    index = index + 197  }98  sum % 10 == 099}100