#![forbid(unsafe_code)]
use std::collections::HashMap;
use std::fmt;
use std::marker::PhantomData;
pub const ERROR_CODE_REQUIRED: &str = "required";
pub const ERROR_CODE_MIN_LENGTH: &str = "too_short";
pub const ERROR_CODE_MAX_LENGTH: &str = "too_long";
pub const ERROR_CODE_PATTERN: &str = "pattern";
pub const ERROR_CODE_EMAIL: &str = "email";
pub const ERROR_CODE_URL: &str = "url";
pub const ERROR_CODE_RANGE: &str = "range";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidationError {
pub code: &'static str,
pub message: String,
pub params: HashMap<String, String>,
}
impl ValidationError {
#[must_use]
pub fn new(code: &'static str, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
params: HashMap::new(),
}
}
#[must_use]
pub fn with_param(mut self, key: impl Into<String>, value: impl ToString) -> Self {
self.params.insert(key.into(), value.to_string());
self
}
#[must_use]
pub fn format_message(&self) -> String {
let mut result = self.message.clone();
for (key, value) in &self.params {
result = result.replace(&format!("{{{key}}}"), value);
}
result
}
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.format_message())
}
}
impl std::error::Error for ValidationError {}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum ValidationResult {
#[default]
Valid,
Invalid(ValidationError),
}
impl ValidationResult {
#[must_use]
pub fn is_valid(&self) -> bool {
matches!(self, Self::Valid)
}
#[must_use]
pub fn is_invalid(&self) -> bool {
matches!(self, Self::Invalid(_))
}
#[must_use]
pub fn error(&self) -> Option<&ValidationError> {
match self {
Self::Valid => None,
Self::Invalid(e) => Some(e),
}
}
#[must_use]
pub fn error_message(&self) -> Option<String> {
self.error().map(ValidationError::format_message)
}
#[must_use]
pub fn and(self, other: Self) -> Self {
match self {
Self::Valid => other,
Self::Invalid(_) => self,
}
}
#[must_use]
pub fn or(self, other: Self) -> Self {
match self {
Self::Valid => Self::Valid,
Self::Invalid(_) => other,
}
}
}
pub trait Validator<T: ?Sized>: Send + Sync {
fn validate(&self, value: &T) -> ValidationResult;
fn error_message(&self) -> &str;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Required {
pub allow_whitespace: bool,
}
impl Required {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn allow_whitespace(mut self) -> Self {
self.allow_whitespace = true;
self
}
}
impl Validator<str> for Required {
fn validate(&self, value: &str) -> ValidationResult {
let is_empty = if self.allow_whitespace {
value.is_empty()
} else {
value.trim().is_empty()
};
if is_empty {
ValidationResult::Invalid(ValidationError::new(
ERROR_CODE_REQUIRED,
"This field is required",
))
} else {
ValidationResult::Valid
}
}
fn error_message(&self) -> &str {
"This field is required"
}
}
#[derive(Debug, Clone, Copy)]
pub struct MinLength {
pub min: usize,
}
impl MinLength {
#[must_use]
pub fn new(min: usize) -> Self {
Self { min }
}
}
impl Validator<str> for MinLength {
fn validate(&self, value: &str) -> ValidationResult {
let len = value.chars().count();
if len < self.min {
ValidationResult::Invalid(
ValidationError::new(ERROR_CODE_MIN_LENGTH, "Must be at least {min} characters")
.with_param("min", self.min)
.with_param("actual", len),
)
} else {
ValidationResult::Valid
}
}
fn error_message(&self) -> &str {
"Must be at least {min} characters"
}
}
#[derive(Debug, Clone, Copy)]
pub struct MaxLength {
pub max: usize,
}
impl MaxLength {
#[must_use]
pub fn new(max: usize) -> Self {
Self { max }
}
}
impl Validator<str> for MaxLength {
fn validate(&self, value: &str) -> ValidationResult {
let len = value.chars().count();
if len > self.max {
ValidationResult::Invalid(
ValidationError::new(ERROR_CODE_MAX_LENGTH, "Must be at most {max} characters")
.with_param("max", self.max)
.with_param("actual", len),
)
} else {
ValidationResult::Valid
}
}
fn error_message(&self) -> &str {
"Must be at most {max} characters"
}
}
#[derive(Debug, Clone)]
pub struct Pattern {
pub pattern: String,
pub message: String,
pub exact: bool,
}
impl Pattern {
#[must_use]
pub fn contains(pattern: impl Into<String>) -> Self {
Self {
pattern: pattern.into(),
message: "Invalid format".to_string(),
exact: false,
}
}
#[must_use]
pub fn exact(pattern: impl Into<String>) -> Self {
Self {
pattern: pattern.into(),
message: "Invalid format".to_string(),
exact: true,
}
}
#[must_use]
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = message.into();
self
}
}
impl Validator<str> for Pattern {
fn validate(&self, value: &str) -> ValidationResult {
let matches = if self.exact {
value == self.pattern
} else {
value.contains(&self.pattern)
};
if matches {
ValidationResult::Valid
} else {
ValidationResult::Invalid(ValidationError::new(ERROR_CODE_PATTERN, &self.message))
}
}
fn error_message(&self) -> &str {
&self.message
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Email;
impl Email {
#[must_use]
pub fn new() -> Self {
Self
}
}
impl Validator<str> for Email {
fn validate(&self, value: &str) -> ValidationResult {
let trimmed = value.trim();
if trimmed.is_empty() {
return ValidationResult::Valid; }
let parts: Vec<&str> = trimmed.splitn(2, '@').collect();
if parts.len() != 2 {
return ValidationResult::Invalid(ValidationError::new(
ERROR_CODE_EMAIL,
"Invalid email address",
));
}
let (local, domain) = (parts[0], parts[1]);
if local.is_empty() || domain.is_empty() {
return ValidationResult::Invalid(ValidationError::new(
ERROR_CODE_EMAIL,
"Invalid email address",
));
}
if !domain.contains('.') {
return ValidationResult::Invalid(ValidationError::new(
ERROR_CODE_EMAIL,
"Invalid email address",
));
}
let domain_parts: Vec<&str> = domain.split('.').collect();
if domain_parts.iter().any(|p| p.is_empty()) {
return ValidationResult::Invalid(ValidationError::new(
ERROR_CODE_EMAIL,
"Invalid email address",
));
}
if let Some(tld) = domain_parts.last()
&& tld.len() < 2
{
return ValidationResult::Invalid(ValidationError::new(
ERROR_CODE_EMAIL,
"Invalid email address",
));
}
ValidationResult::Valid
}
fn error_message(&self) -> &str {
"Invalid email address"
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Url {
pub require_https: bool,
}
impl Url {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn require_https(mut self) -> Self {
self.require_https = true;
self
}
}
impl Validator<str> for Url {
fn validate(&self, value: &str) -> ValidationResult {
let trimmed = value.trim();
if trimmed.is_empty() {
return ValidationResult::Valid; }
let is_valid = if self.require_https {
trimmed.starts_with("https://") && trimmed.len() > 8
} else {
(trimmed.starts_with("http://") && trimmed.len() > 7)
|| (trimmed.starts_with("https://") && trimmed.len() > 8)
};
if is_valid {
ValidationResult::Valid
} else {
let message = if self.require_https {
"Invalid URL (must use HTTPS)"
} else {
"Invalid URL"
};
ValidationResult::Invalid(ValidationError::new(ERROR_CODE_URL, message))
}
}
fn error_message(&self) -> &str {
"Invalid URL"
}
}
#[derive(Debug, Clone, Copy)]
pub struct Range<T> {
pub min: T,
pub max: T,
}
impl<T: Copy> Range<T> {
#[must_use]
pub fn new(min: T, max: T) -> Self {
Self { min, max }
}
}
impl<T> Validator<T> for Range<T>
where
T: PartialOrd + fmt::Display + Copy + Send + Sync,
{
fn validate(&self, value: &T) -> ValidationResult {
if *value >= self.min && *value <= self.max {
ValidationResult::Valid
} else {
ValidationResult::Invalid(
ValidationError::new(ERROR_CODE_RANGE, "Must be between {min} and {max}")
.with_param("min", self.min)
.with_param("max", self.max)
.with_param("actual", *value),
)
}
}
fn error_message(&self) -> &str {
"Must be between {min} and {max}"
}
}
#[derive(Debug, Clone)]
pub struct And<A, B> {
pub first: A,
pub second: B,
}
impl<A, B> And<A, B> {
#[must_use]
pub fn new(first: A, second: B) -> Self {
Self { first, second }
}
}
impl<T: ?Sized, A, B> Validator<T> for And<A, B>
where
A: Validator<T>,
B: Validator<T>,
{
fn validate(&self, value: &T) -> ValidationResult {
match self.first.validate(value) {
ValidationResult::Valid => self.second.validate(value),
err => err,
}
}
fn error_message(&self) -> &str {
self.first.error_message()
}
}
#[derive(Debug, Clone)]
pub struct Or<A, B> {
pub first: A,
pub second: B,
}
impl<A, B> Or<A, B> {
#[must_use]
pub fn new(first: A, second: B) -> Self {
Self { first, second }
}
}
impl<T: ?Sized, A, B> Validator<T> for Or<A, B>
where
A: Validator<T>,
B: Validator<T>,
{
fn validate(&self, value: &T) -> ValidationResult {
match self.first.validate(value) {
ValidationResult::Valid => ValidationResult::Valid,
_ => self.second.validate(value),
}
}
fn error_message(&self) -> &str {
self.second.error_message()
}
}
#[derive(Debug, Clone)]
pub struct Not<V> {
pub inner: V,
pub message: String,
}
impl<V> Not<V> {
#[must_use]
pub fn new(inner: V, message: impl Into<String>) -> Self {
Self {
inner,
message: message.into(),
}
}
}
impl<T: ?Sized, V> Validator<T> for Not<V>
where
V: Validator<T>,
{
fn validate(&self, value: &T) -> ValidationResult {
match self.inner.validate(value) {
ValidationResult::Valid => {
ValidationResult::Invalid(ValidationError::new("not", &self.message))
}
ValidationResult::Invalid(_) => ValidationResult::Valid,
}
}
fn error_message(&self) -> &str {
&self.message
}
}
pub struct All<T: ?Sized> {
validators: Vec<Box<dyn Validator<T>>>,
}
impl<T: ?Sized> All<T> {
#[must_use]
pub fn new(validators: Vec<Box<dyn Validator<T>>>) -> Self {
Self { validators }
}
}
impl<T: ?Sized> Validator<T> for All<T> {
fn validate(&self, value: &T) -> ValidationResult {
for validator in &self.validators {
let result = validator.validate(value);
if result.is_invalid() {
return result;
}
}
ValidationResult::Valid
}
fn error_message(&self) -> &str {
self.validators
.first()
.map_or("Validation failed", |v| v.error_message())
}
}
impl<T: ?Sized> fmt::Debug for All<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("All")
.field(
"validators",
&format!("[{} validators]", self.validators.len()),
)
.finish()
}
}
pub struct Any<T: ?Sized> {
validators: Vec<Box<dyn Validator<T>>>,
}
impl<T: ?Sized> Any<T> {
#[must_use]
pub fn new(validators: Vec<Box<dyn Validator<T>>>) -> Self {
Self { validators }
}
}
impl<T: ?Sized> Validator<T> for Any<T> {
fn validate(&self, value: &T) -> ValidationResult {
let mut last_error = None;
for validator in &self.validators {
let result = validator.validate(value);
if result.is_valid() {
return ValidationResult::Valid;
}
last_error = result.error().cloned();
}
last_error.map_or(ValidationResult::Valid, ValidationResult::Invalid)
}
fn error_message(&self) -> &str {
self.validators
.last()
.map_or("Validation failed", |v| v.error_message())
}
}
impl<T: ?Sized> fmt::Debug for Any<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Any")
.field(
"validators",
&format!("[{} validators]", self.validators.len()),
)
.finish()
}
}
#[must_use]
pub struct ValidatorBuilder<T: ?Sized> {
validators: Vec<Box<dyn Validator<T>>>,
_phantom: PhantomData<T>,
}
impl<T: ?Sized> Default for ValidatorBuilder<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: ?Sized> ValidatorBuilder<T> {
pub fn new() -> Self {
Self {
validators: Vec::new(),
_phantom: PhantomData,
}
}
pub fn custom(mut self, validator: impl Validator<T> + 'static) -> Self {
self.validators.push(Box::new(validator));
self
}
#[must_use]
pub fn build(self) -> All<T> {
All::new(self.validators)
}
}
impl ValidatorBuilder<str> {
pub fn required(self) -> Self {
self.custom(Required::new())
}
pub fn min_length(self, min: usize) -> Self {
self.custom(MinLength::new(min))
}
pub fn max_length(self, max: usize) -> Self {
self.custom(MaxLength::new(max))
}
pub fn email(self) -> Self {
self.custom(Email::new())
}
pub fn url(self) -> Self {
self.custom(Url::new())
}
pub fn contains(self, pattern: impl Into<String>) -> Self {
self.custom(Pattern::contains(pattern))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validation_error_new() {
let err = ValidationError::new("test", "Test message");
assert_eq!(err.code, "test");
assert_eq!(err.message, "Test message");
assert!(err.params.is_empty());
}
#[test]
fn validation_error_with_param() {
let err = ValidationError::new("test", "Value is {value}").with_param("value", 42);
assert_eq!(err.params.get("value"), Some(&"42".to_string()));
}
#[test]
fn validation_error_format_message() {
let err =
ValidationError::new("test", "Must be at least {min} characters").with_param("min", 8);
assert_eq!(err.format_message(), "Must be at least 8 characters");
}
#[test]
fn validation_error_format_multiple_params() {
let err = ValidationError::new("test", "Between {min} and {max}")
.with_param("min", 1)
.with_param("max", 10);
assert_eq!(err.format_message(), "Between 1 and 10");
}
#[test]
fn validation_error_display() {
let err = ValidationError::new("test", "Error: {code}").with_param("code", "E001");
assert_eq!(format!("{err}"), "Error: E001");
}
#[test]
fn validation_result_is_valid() {
assert!(ValidationResult::Valid.is_valid());
assert!(!ValidationResult::Invalid(ValidationError::new("", "")).is_valid());
}
#[test]
fn validation_result_is_invalid() {
assert!(!ValidationResult::Valid.is_invalid());
assert!(ValidationResult::Invalid(ValidationError::new("", "")).is_invalid());
}
#[test]
fn validation_result_error() {
let valid = ValidationResult::Valid;
assert!(valid.error().is_none());
let invalid = ValidationResult::Invalid(ValidationError::new("test", "msg"));
assert!(invalid.error().is_some());
assert_eq!(invalid.error().unwrap().code, "test");
}
#[test]
fn validation_result_and() {
let valid = ValidationResult::Valid;
let invalid = ValidationResult::Invalid(ValidationError::new("", ""));
assert!(valid.clone().and(valid.clone()).is_valid());
assert!(valid.clone().and(invalid.clone()).is_invalid());
assert!(invalid.clone().and(valid.clone()).is_invalid());
assert!(invalid.clone().and(invalid.clone()).is_invalid());
}
#[test]
fn validation_result_or() {
let valid = ValidationResult::Valid;
let invalid = ValidationResult::Invalid(ValidationError::new("", ""));
assert!(valid.clone().or(valid.clone()).is_valid());
assert!(valid.clone().or(invalid.clone()).is_valid());
assert!(invalid.clone().or(valid.clone()).is_valid());
assert!(invalid.clone().or(invalid.clone()).is_invalid());
}
#[test]
fn required_empty_fails() {
let v = Required::new();
assert!(v.validate("").is_invalid());
}
#[test]
fn required_whitespace_only_fails() {
let v = Required::new();
assert!(v.validate(" ").is_invalid());
assert!(v.validate("\t\n").is_invalid());
}
#[test]
fn required_whitespace_allowed() {
let v = Required::new().allow_whitespace();
assert!(v.validate(" ").is_valid());
}
#[test]
fn required_non_empty_passes() {
let v = Required::new();
assert!(v.validate("hello").is_valid());
assert!(v.validate(" hello ").is_valid());
}
#[test]
fn min_length_boundary() {
let v = MinLength::new(3);
assert!(v.validate("ab").is_invalid()); assert!(v.validate("abc").is_valid()); assert!(v.validate("abcd").is_valid()); }
#[test]
fn min_length_unicode() {
let v = MinLength::new(4);
assert!(v.validate("café").is_valid()); assert!(v.validate("caf").is_invalid()); }
#[test]
fn min_length_emoji() {
let v = MinLength::new(1);
assert!(v.validate("🎉").is_valid()); }
#[test]
fn min_length_error_params() {
let v = MinLength::new(5);
let result = v.validate("ab");
match result {
ValidationResult::Invalid(err) => {
assert_eq!(err.params.get("min"), Some(&"5".to_string()));
assert_eq!(err.params.get("actual"), Some(&"2".to_string()));
}
other => {
assert!(other.is_invalid(), "Expected invalid result");
}
}
}
#[test]
fn max_length_boundary() {
let v = MaxLength::new(3);
assert!(v.validate("ab").is_valid()); assert!(v.validate("abc").is_valid()); assert!(v.validate("abcd").is_invalid()); }
#[test]
fn max_length_unicode() {
let v = MaxLength::new(4);
assert!(v.validate("café").is_valid()); assert!(v.validate("café!").is_invalid()); }
#[test]
fn pattern_contains() {
let v = Pattern::contains("@");
assert!(v.validate("test@example").is_valid());
assert!(v.validate("no at sign").is_invalid());
}
#[test]
fn pattern_exact() {
let v = Pattern::exact("hello");
assert!(v.validate("hello").is_valid());
assert!(v.validate("hello!").is_invalid());
assert!(v.validate("HELLO").is_invalid());
}
#[test]
fn pattern_custom_message() {
let v = Pattern::contains("@").with_message("Must contain @");
let result = v.validate("test");
if let ValidationResult::Invalid(err) = result {
assert_eq!(err.message, "Must contain @");
}
}
#[test]
fn email_valid() {
let v = Email::new();
assert!(v.validate("user@example.com").is_valid());
assert!(v.validate("user.name@example.co.uk").is_valid());
assert!(v.validate("user+tag@example.org").is_valid());
}
#[test]
fn email_invalid() {
let v = Email::new();
assert!(v.validate("not-an-email").is_invalid());
assert!(v.validate("@example.com").is_invalid());
assert!(v.validate("user@").is_invalid());
assert!(v.validate("user@example").is_invalid()); assert!(v.validate("user@.com").is_invalid());
}
#[test]
fn email_empty_is_valid() {
let v = Email::new();
assert!(v.validate("").is_valid()); }
#[test]
fn email_with_whitespace() {
let v = Email::new();
assert!(v.validate(" user@example.com ").is_valid()); }
#[test]
fn url_valid() {
let v = Url::new();
assert!(v.validate("http://example.com").is_valid());
assert!(v.validate("https://example.com").is_valid());
assert!(v.validate("https://example.com/path?query=1").is_valid());
}
#[test]
fn url_invalid() {
let v = Url::new();
assert!(v.validate("not-a-url").is_invalid());
assert!(v.validate("ftp://example.com").is_invalid());
assert!(v.validate("http://").is_invalid());
}
#[test]
fn url_require_https() {
let v = Url::new().require_https();
assert!(v.validate("https://example.com").is_valid());
assert!(v.validate("http://example.com").is_invalid());
}
#[test]
fn url_empty_is_valid() {
let v = Url::new();
assert!(v.validate("").is_valid()); }
#[test]
fn range_i32() {
let v = Range::new(1, 10);
assert!(v.validate(&0).is_invalid());
assert!(v.validate(&1).is_valid());
assert!(v.validate(&5).is_valid());
assert!(v.validate(&10).is_valid());
assert!(v.validate(&11).is_invalid());
}
#[test]
fn range_f64() {
let v = Range::new(0.0, 1.0);
assert!(v.validate(&0.5).is_valid());
assert!(v.validate(&1.5).is_invalid());
}
#[test]
fn and_both_valid() {
let v = And::new(Required::new(), MinLength::new(3));
assert!(v.validate("hello").is_valid());
}
#[test]
fn and_first_invalid() {
let v = And::new(Required::new(), MinLength::new(3));
assert!(v.validate("").is_invalid());
if let ValidationResult::Invalid(err) = v.validate("") {
assert_eq!(err.code, ERROR_CODE_REQUIRED);
}
}
#[test]
fn and_second_invalid() {
let v = And::new(Required::new(), MinLength::new(5));
let result = v.validate("ab");
assert!(result.is_invalid());
if let ValidationResult::Invalid(err) = result {
assert_eq!(err.code, ERROR_CODE_MIN_LENGTH);
}
}
#[test]
fn or_first_valid() {
let v = Or::new(Pattern::exact("yes"), Pattern::exact("no"));
assert!(v.validate("yes").is_valid());
}
#[test]
fn or_second_valid() {
let v = Or::new(Pattern::exact("yes"), Pattern::exact("no"));
assert!(v.validate("no").is_valid());
}
#[test]
fn or_neither_valid() {
let v = Or::new(Pattern::exact("yes"), Pattern::exact("no"));
assert!(v.validate("maybe").is_invalid());
}
#[test]
fn not_inverts_valid() {
let v = Not::new(Pattern::contains("@"), "Must not contain @");
assert!(v.validate("hello").is_valid());
assert!(v.validate("hello@world").is_invalid());
}
#[test]
fn all_validators() {
let v: All<str> = All::new(vec![
Box::new(Required::new()),
Box::new(MinLength::new(3)),
Box::new(MaxLength::new(10)),
]);
assert!(v.validate("hello").is_valid());
assert!(v.validate("").is_invalid());
assert!(v.validate("ab").is_invalid());
assert!(v.validate("this is too long").is_invalid());
}
#[test]
fn any_validators() {
let v: Any<str> = Any::new(vec![
Box::new(Pattern::exact("yes")),
Box::new(Pattern::exact("no")),
Box::new(Pattern::exact("maybe")),
]);
assert!(v.validate("yes").is_valid());
assert!(v.validate("no").is_valid());
assert!(v.validate("maybe").is_valid());
assert!(v.validate("dunno").is_invalid());
}
#[test]
fn builder_empty() {
let v = ValidatorBuilder::<str>::new().build();
assert!(v.validate("anything").is_valid());
}
#[test]
fn builder_required() {
let v = ValidatorBuilder::<str>::new().required().build();
assert!(v.validate("hello").is_valid());
assert!(v.validate("").is_invalid());
}
#[test]
fn builder_chain() {
let v = ValidatorBuilder::<str>::new()
.required()
.min_length(3)
.max_length(10)
.build();
assert!(v.validate("hello").is_valid());
assert!(v.validate("").is_invalid());
assert!(v.validate("ab").is_invalid());
assert!(v.validate("this is way too long").is_invalid());
}
#[test]
fn builder_email() {
let v = ValidatorBuilder::<str>::new().required().email().build();
assert!(v.validate("user@example.com").is_valid());
assert!(v.validate("").is_invalid());
assert!(v.validate("not-an-email").is_invalid());
}
#[test]
fn builder_url() {
let v = ValidatorBuilder::<str>::new().required().url().build();
assert!(v.validate("https://example.com").is_valid());
assert!(v.validate("").is_invalid());
assert!(v.validate("not-a-url").is_invalid());
}
struct NoDigits;
impl Validator<str> for NoDigits {
fn validate(&self, value: &str) -> ValidationResult {
if value.chars().any(|c| c.is_ascii_digit()) {
ValidationResult::Invalid(ValidationError::new(
"no_digits",
"Must not contain digits",
))
} else {
ValidationResult::Valid
}
}
fn error_message(&self) -> &str {
"Must not contain digits"
}
}
#[test]
fn custom_validator() {
let v = ValidatorBuilder::<str>::new()
.required()
.custom(NoDigits)
.build();
assert!(v.validate("hello").is_valid());
assert!(v.validate("hello123").is_invalid());
}
#[test]
fn empty_string_with_min_length() {
let v = MinLength::new(0);
assert!(v.validate("").is_valid());
}
#[test]
fn zero_max_length() {
let v = MaxLength::new(0);
assert!(v.validate("").is_valid());
assert!(v.validate("a").is_invalid());
}
#[test]
fn validation_error_equality() {
let err1 = ValidationError::new("test", "Message");
let err2 = ValidationError::new("test", "Message");
assert_eq!(err1, err2);
let err3 = ValidationError::new("test", "Different");
assert_ne!(err1, err3);
}
}