// retoor import "regex" for Regex import "base64" for Base64 import "json" for Json import "uuid" for Uuid class Validator { static email(value) { if (!(value is String)) return false var at = value.indexOf("@") if (at < 1) return false var dot = value.indexOf(".", at) if (dot < at + 2) return false if (dot >= value.count - 1) return false for (c in value) { var code = c.codePoints.toList[0] if (c == "@" || c == "." || c == "_" || c == "-" || c == "+") continue if (code >= 48 && code <= 57) continue if (code >= 65 && code <= 90) continue if (code >= 97 && code <= 122) continue return false } return true } static domain(value) { if (!(value is String)) return false if (value.count == 0) return false if (value[0] == "-" || value[-1] == "-") return false var parts = value.split(".") if (parts.count < 2) return false for (part in parts) { if (part.count == 0 || part.count > 63) return false if (part[0] == "-" || part[-1] == "-") return false for (c in part) { var code = c.codePoints.toList[0] if (c == "-") continue if (code >= 48 && code <= 57) continue if (code >= 65 && code <= 90) continue if (code >= 97 && code <= 122) continue return false } } return true } static safeStr(value) { if (!(value is String)) return false for (b in value.bytes) { if (b < 32 || b > 126) return false } return true } static httpEncoded(value) { if (!(value is String)) return false for (c in value) { var code = c.codePoints.toList[0] if (c == "_" || c == "." || c == "~" || c == "-" || c == "+") continue if (c == "\%") continue if (code >= 48 && code <= 57) continue if (code >= 65 && code <= 90) continue if (code >= 97 && code <= 122) continue return false } return true } static base64(value) { if (!(value is String)) return false if (value.count % 4 != 0) return false var padCount = 0 for (i in 0...value.count) { var c = value[i] if (c == "=") { padCount = padCount + 1 if (i < value.count - 2) return false } else if (padCount > 0) { return false } else { var code = c.codePoints.toList[0] if (c == "+" || c == "/") continue if (code >= 48 && code <= 57) continue if (code >= 65 && code <= 90) continue if (code >= 97 && code <= 122) continue return false } } return padCount <= 2 } static json(value) { if (!(value is String)) return false var fiber = Fiber.new { Json.parse(value) } var result = fiber.try() return !fiber.error } static regex(value, pattern) { if (!(value is String)) return false var re = Regex.new(pattern) return re.test(value) } static url(value) { if (!(value is String)) return false if (!value.startsWith("http://") && !value.startsWith("https://")) return false var rest = value.startsWith("https://") ? value[8..-1] : value[7..-1] if (rest.count == 0) return false return true } static uuid(value) { return Uuid.isValid(value) } static minLength(value, min) { if (value is String) return value.count >= min if (value is List) return value.count >= min return false } static maxLength(value, max) { if (value is String) return value.count <= max if (value is List) return value.count <= max return false } static range(value, min, max) { if (!(value is Num)) return false return value >= min && value <= max } static positive(value) { if (!(value is Num)) return false return value > 0 } static negative(value) { if (!(value is Num)) return false return value < 0 } static integer(value) { if (!(value is Num)) return false return value == value.floor } static ipv4(value) { if (!(value is String)) return false var parts = value.split(".") if (parts.count != 4) return false for (part in parts) { if (part.count == 0 || part.count > 3) return false var n = Num.fromString(part) if (n == null || n < 0 || n > 255) return false if (part.count > 1 && part[0] == "0") return false } return true } } class Field { static string() { StringField.new() } static string(options) { StringField.new(options) } static integer() { IntegerField.new() } static integer(options) { IntegerField.new(options) } static number() { NumberField.new() } static number(options) { NumberField.new(options) } static email() { EmailField.new() } static boolean() { BooleanField.new() } static list(itemType) { ListField.new(itemType) } static map() { MapField.new() } static optional(fieldDef) { OptionalField.new(fieldDef) } static required(fieldDef) { fieldDef.required = true return fieldDef } } class BaseField { construct new() { _required = true _default = null _validators = [] } construct new(options) { _required = options.containsKey("required") ? options["required"] : true _default = options.containsKey("default") ? options["default"] : null _validators = options.containsKey("validators") ? options["validators"] : [] } required { _required } required=(value) { _required = value } default { _default } validators { _validators } addValidator(fn) { _validators.add(fn) return this } validate(value, path) { if (value == null) { if (_required) return ValidationError.new(path, "Field is required") return null } var typeError = validateType_(value, path) if (typeError) return typeError for (validator in _validators) { if (!validator.call(value)) { return ValidationError.new(path, "Custom validation failed") } } return null } validateType_(value, path) { null } coerce(value) { value } } class StringField is BaseField { construct new() { super() _minLength = null _maxLength = null _pattern = null } construct new(options) { super(options) _minLength = options.containsKey("minLength") ? options["minLength"] : null _maxLength = options.containsKey("maxLength") ? options["maxLength"] : null _pattern = options.containsKey("pattern") ? options["pattern"] : null } validateType_(value, path) { if (!(value is String)) return ValidationError.new(path, "Expected string") if (_minLength && value.count < _minLength) { return ValidationError.new(path, "String too short") } if (_maxLength && value.count > _maxLength) { return ValidationError.new(path, "String too long") } if (_pattern) { var re = Regex.new(_pattern) if (!re.test(value)) { return ValidationError.new(path, "String does not match pattern") } } return null } minLength { _minLength } maxLength { _maxLength } } class IntegerField is BaseField { construct new() { super() _min = null _max = null } construct new(options) { super(options) _min = options.containsKey("min") ? options["min"] : null _max = options.containsKey("max") ? options["max"] : null } validateType_(value, path) { if (!(value is Num)) return ValidationError.new(path, "Expected integer") if (value != value.floor) return ValidationError.new(path, "Expected integer, got float") if (_min && value < _min) return ValidationError.new(path, "Value below minimum") if (_max && value > _max) return ValidationError.new(path, "Value above maximum") return null } min { _min } max { _max } coerce(value) { if (value is String) return Num.fromString(value) return value } } class NumberField is BaseField { construct new() { super() _min = null _max = null } construct new(options) { super(options) _min = options.containsKey("min") ? options["min"] : null _max = options.containsKey("max") ? options["max"] : null } validateType_(value, path) { if (!(value is Num)) return ValidationError.new(path, "Expected number") if (_min && value < _min) return ValidationError.new(path, "Value below minimum") if (_max && value > _max) return ValidationError.new(path, "Value above maximum") return null } min { _min } max { _max } coerce(value) { if (value is String) return Num.fromString(value) return value } } class EmailField is BaseField { construct new() { super() } validateType_(value, path) { if (!(value is String)) return ValidationError.new(path, "Expected string") if (!Validator.email(value)) return ValidationError.new(path, "Invalid email format") return null } } class BooleanField is BaseField { construct new() { super() } validateType_(value, path) { if (!(value is Bool)) return ValidationError.new(path, "Expected boolean") return null } coerce(value) { if (value is String) { if (value == "true" || value == "1") return true if (value == "false" || value == "0") return false } return value } } class ListField is BaseField { construct new(itemType) { super() _itemType = itemType } validateType_(value, path) { if (!(value is List)) return ValidationError.new(path, "Expected list") for (i in 0...value.count) { var itemPath = path + "[" + i.toString + "]" var error = _itemType.validate(value[i], itemPath) if (error) return error } return null } itemType { _itemType } } class MapField is BaseField { construct new() { super() } validateType_(value, path) { if (!(value is Map)) return ValidationError.new(path, "Expected map") return null } } class OptionalField is BaseField { construct new(fieldDef) { super() _field = fieldDef _required = false } validate(value, path) { if (value == null) return null return _field.validate(value, path) } } class ValidationError { construct new(path, message) { _path = path _message = message } path { _path } message { _message } toString { _path + ": " + _message } } class ValidationResult { construct new(isValid, errors, data) { _isValid = isValid _errors = errors _data = data } isValid { _isValid } errors { _errors } data { _data } } class Schema { construct new(definition) { _definition = definition } validate(data) { if (!(data is Map)) { return ValidationResult.new(false, [ValidationError.new("", "Expected object")], null) } var errors = [] var validData = {} for (key in _definition.keys) { var field = _definition[key] var value = data.containsKey(key) ? data[key] : null if (value != null) { value = field.coerce(value) } var error = field.validate(value, key) if (error) { errors.add(error) } else { validData[key] = value != null ? value : field.default } } return ValidationResult.new(errors.count == 0, errors, errors.count == 0 ? validData : null) } validateOrAbort(data) { var result = validate(data) if (!result.isValid) { var messages = [] for (error in result.errors) { messages.add(error.toString) } Fiber.abort("Validation failed: " + messages.join(", ")) } return result.data } }