use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ValidationError {
pub message: String,
pub field: Option<String>,
}
impl ValidationError {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
field: None,
}
}
pub fn with_field(message: impl Into<String>, field: impl Into<String>) -> Self {
Self {
message: message.into(),
field: Some(field.into()),
}
}
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.field {
Some(field) => write!(f, "{}: {}", field, self.message),
None => write!(f, "{}", self.message),
}
}
}
impl std::error::Error for ValidationError {}
pub type ValidationResult = Result<(), ValidationError>;
pub type Validator = Box<dyn Fn(&str) -> ValidationResult + Send + Sync>;
pub fn required() -> Validator {
Box::new(|value: &str| {
if value.trim().is_empty() {
Err(ValidationError::new("This field is required"))
} else {
Ok(())
}
})
}
pub fn min_length(min: usize) -> Validator {
Box::new(move |value: &str| {
if value.len() < min {
Err(ValidationError::new(format!(
"Must be at least {} characters",
min
)))
} else {
Ok(())
}
})
}
pub fn max_length(max: usize) -> Validator {
Box::new(move |value: &str| {
if value.len() > max {
Err(ValidationError::new(format!(
"Must be at most {} characters",
max
)))
} else {
Ok(())
}
})
}
pub fn length_range(min: usize, max: usize) -> Validator {
Box::new(move |value: &str| {
let len = value.len();
if len < min || len > max {
Err(ValidationError::new(format!(
"Must be between {} and {} characters",
min, max
)))
} else {
Ok(())
}
})
}
pub fn email() -> Validator {
Box::new(|value: &str| {
if value.is_empty() {
return Ok(()); }
let parts: Vec<&str> = value.split('@').collect();
if parts.len() != 2 {
return Err(ValidationError::new("Invalid email format"));
}
let (local, domain) = (parts[0], parts[1]);
if local.is_empty() || domain.is_empty() {
return Err(ValidationError::new("Invalid email format"));
}
if !domain.contains('.') {
return Err(ValidationError::new("Invalid email domain"));
}
Ok(())
})
}
pub fn url() -> Validator {
Box::new(|value: &str| {
if value.is_empty() {
return Ok(());
}
if !value.starts_with("http://") && !value.starts_with("https://") {
return Err(ValidationError::new(
"URL must start with http:// or https://",
));
}
let rest = value
.trim_start_matches("https://")
.trim_start_matches("http://");
if rest.is_empty() || !rest.contains('.') {
return Err(ValidationError::new("Invalid URL format"));
}
Ok(())
})
}
pub fn pattern(pattern: &'static str, message: &'static str) -> Validator {
Box::new(move |value: &str| {
if value.is_empty() {
return Ok(());
}
let matches = match pattern {
r"^\d+$" => value.chars().all(|c| c.is_ascii_digit()),
r"^[a-zA-Z]+$" => value.chars().all(|c| c.is_ascii_alphabetic()),
r"^[a-zA-Z0-9]+$" => value.chars().all(|c| c.is_ascii_alphanumeric()),
r"^[a-zA-Z0-9_]+$" => value.chars().all(|c| c.is_ascii_alphanumeric() || c == '_'),
r"^[a-zA-Z0-9_-]+$" => value
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-'),
r"^[a-z]+$" => value.chars().all(|c| c.is_ascii_lowercase()),
r"^[A-Z]+$" => value.chars().all(|c| c.is_ascii_uppercase()),
_ => true, };
if matches {
Ok(())
} else {
Err(ValidationError::new(message))
}
})
}
pub fn numeric() -> Validator {
pattern(r"^\d+$", "Must be a number")
}
pub fn alphabetic() -> Validator {
pattern(r"^[a-zA-Z]+$", "Must contain only letters")
}
pub fn alphanumeric() -> Validator {
pattern(r"^[a-zA-Z0-9]+$", "Must contain only letters and numbers")
}
pub fn lowercase() -> Validator {
pattern(r"^[a-z]+$", "Must be lowercase")
}
pub fn uppercase() -> Validator {
pattern(r"^[A-Z]+$", "Must be uppercase")
}
pub fn min_value(min: i64) -> Validator {
Box::new(move |value: &str| {
if value.is_empty() {
return Ok(());
}
match value.parse::<i64>() {
Ok(n) if n >= min => Ok(()),
Ok(_) => Err(ValidationError::new(format!("Must be at least {}", min))),
Err(_) => Err(ValidationError::new("Must be a number")),
}
})
}
pub fn max_value(max: i64) -> Validator {
Box::new(move |value: &str| {
if value.is_empty() {
return Ok(());
}
match value.parse::<i64>() {
Ok(n) if n <= max => Ok(()),
Ok(_) => Err(ValidationError::new(format!("Must be at most {}", max))),
Err(_) => Err(ValidationError::new("Must be a number")),
}
})
}
pub fn value_range(min: i64, max: i64) -> Validator {
Box::new(move |value: &str| {
if value.is_empty() {
return Ok(());
}
match value.parse::<i64>() {
Ok(n) if n >= min && n <= max => Ok(()),
Ok(_) => Err(ValidationError::new(format!(
"Must be between {} and {}",
min, max
))),
Err(_) => Err(ValidationError::new("Must be a number")),
}
})
}
pub fn custom<F>(predicate: F, message: &'static str) -> Validator
where
F: Fn(&str) -> bool + Send + Sync + 'static,
{
Box::new(move |value: &str| {
if predicate(value) {
Ok(())
} else {
Err(ValidationError::new(message))
}
})
}
pub fn any_of(validators: Vec<Validator>) -> Validator {
Box::new(move |value: &str| {
for validator in &validators {
if validator(value).is_ok() {
return Ok(());
}
}
Err(ValidationError::new("None of the conditions were met"))
})
}
pub fn all_of(validators: Vec<Validator>) -> Validator {
Box::new(move |value: &str| {
for validator in &validators {
validator(value)?;
}
Ok(())
})
}
pub fn one_of(values: &'static [&'static str]) -> Validator {
Box::new(move |value: &str| {
if value.is_empty() || values.contains(&value) {
Ok(())
} else {
Err(ValidationError::new(format!(
"Must be one of: {}",
values.join(", ")
)))
}
})
}
pub fn not_one_of(values: &'static [&'static str]) -> Validator {
Box::new(move |value: &str| {
if values.contains(&value) {
Err(ValidationError::new("This value is not allowed"))
} else {
Ok(())
}
})
}
pub fn matches(other_value: String, field_name: &'static str) -> Validator {
Box::new(move |value: &str| {
if value == other_value {
Ok(())
} else {
Err(ValidationError::new(format!("Must match {}", field_name)))
}
})
}
pub struct FormValidator {
fields: Vec<(String, Vec<Validator>)>,
}
impl FormValidator {
pub fn new() -> Self {
Self { fields: Vec::new() }
}
pub fn field(mut self, name: impl Into<String>, validators: Vec<Validator>) -> Self {
self.fields.push((name.into(), validators));
self
}
pub fn validate(&self, values: &[(&str, &str)]) -> Result<(), Vec<ValidationError>> {
let mut errors = Vec::new();
for (field_name, validators) in &self.fields {
let value = values
.iter()
.find(|(name, _)| name == field_name)
.map(|(_, v)| *v)
.unwrap_or("");
for validator in validators {
if let Err(mut err) = validator(value) {
err.field = Some(field_name.clone());
errors.push(err);
break; }
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
impl Default for FormValidator {
fn default() -> Self {
Self::new()
}
}