use std::fmt;
pub type ValidationResult<T = ()> = Result<T, ValidationError>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidationError {
pub message: String,
pub code: &'static str,
}
impl ValidationError {
pub fn new(message: impl Into<String>, code: &'static str) -> Self {
Self {
message: message.into(),
code,
}
}
pub fn required(field: impl fmt::Display) -> Self {
Self::new(format!("{} is required", field), "REQUIRED")
}
pub fn min_length(field: impl fmt::Display, min: usize) -> Self {
Self::new(
format!("{} must be at least {} characters", field, min),
"MIN_LENGTH",
)
}
pub fn max_length(field: impl fmt::Display, max: usize) -> Self {
Self::new(
format!("{} must be at most {} characters", field, max),
"MAX_LENGTH",
)
}
pub fn pattern(field: impl fmt::Display, pattern: &str) -> Self {
Self::new(
format!("{} must match pattern: {}", field, pattern),
"PATTERN",
)
}
pub fn range(value: impl fmt::Display, min: impl fmt::Display, max: impl fmt::Display) -> Self {
Self::new(
format!("{} must be between {} and {}", value, min, max),
"RANGE",
)
}
pub fn email(value: impl fmt::Display) -> Self {
Self::new(format!("'{}' is not a valid email address", value), "EMAIL")
}
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for ValidationError {}
pub trait Validatable {
type Error: std::error::Error + Send + Sync + 'static;
fn validate(&self) -> ValidationResult<Self::Error>;
fn is_valid(&self) -> bool {
self.validate().is_ok()
}
}
pub mod validators {
use super::ValidationError;
use std::fmt::Display;
pub fn require<T: Display>(value: &T, field: impl Display) -> Result<(), ValidationError> {
if value.to_string().is_empty() {
Err(ValidationError::required(field))
} else {
Ok(())
}
}
pub fn min_length(value: &str, min: usize, field: impl Display) -> Result<(), ValidationError> {
if value.len() < min {
Err(ValidationError::min_length(field, min))
} else {
Ok(())
}
}
pub fn max_length(value: &str, max: usize, field: impl Display) -> Result<(), ValidationError> {
if value.len() > max {
Err(ValidationError::max_length(field, max))
} else {
Ok(())
}
}
pub fn email(value: &str) -> Result<(), ValidationError> {
if !value.contains('@') || !value.contains('.') {
Err(ValidationError::email(value))
} else {
Ok(())
}
}
pub fn range<T: Display + PartialOrd>(
value: T,
min: T,
max: T,
field: impl Display,
) -> Result<(), ValidationError> {
if value < min || value > max {
Err(ValidationError::new(
format!("{} must be between {} and {}", field, min, max),
"RANGE",
))
} else {
Ok(())
}
}
pub fn custom<T>(
value: &T,
predicate: impl Fn(&T) -> bool,
error: impl Fn() -> ValidationError,
) -> Result<(), ValidationError> {
if predicate(value) {
Ok(())
} else {
Err(error())
}
}
pub fn pattern(value: &str, pattern: &str, field: impl Display) -> Result<(), ValidationError> {
if value.contains(pattern) {
Ok(())
} else {
Err(ValidationError::pattern(field, pattern))
}
}
}