use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone, Default)]
pub struct ValidationErrors {
errors: HashMap<String, Vec<String>>,
}
impl ValidationErrors {
pub fn new() -> Self {
Self {
errors: HashMap::new(),
}
}
pub fn add(&mut self, field: impl Into<String>, message: impl Into<String>) {
self.errors
.entry(field.into())
.or_default()
.push(message.into());
}
pub fn is_empty(&self) -> bool {
self.errors.is_empty()
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn len(&self) -> usize {
self.errors.len()
}
pub fn get(&self, field: &str) -> Option<&Vec<String>> {
self.errors.get(field)
}
pub fn field_errors(&self, field: &str) -> Vec<String> {
self.errors.get(field).cloned().unwrap_or_default()
}
pub fn all(&self) -> &HashMap<String, Vec<String>> {
&self.errors
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &Vec<String>)> {
self.errors.iter()
}
pub fn first(&self) -> Option<(&String, &String)> {
self.errors.iter().next().and_then(|(field, messages)| {
messages.first().map(|msg| (field, msg))
})
}
pub fn messages(&self) -> Vec<String> {
self.errors
.iter()
.flat_map(|(field, messages)| {
messages.iter().map(move |msg| format!("{}: {}", field, msg))
})
.collect()
}
pub fn merge(&mut self, other: ValidationErrors) {
for (field, messages) in other.errors {
for message in messages {
self.add(field.clone(), message);
}
}
}
pub fn to_result(self) -> Result<(), Self> {
if self.is_empty() {
Ok(())
} else {
Err(self)
}
}
pub fn errors(&self) -> Vec<(String, String)> {
self.errors
.iter()
.flat_map(|(field, messages)| {
messages.iter().map(move |msg| (field.clone(), msg.clone()))
})
.collect()
}
pub fn into_error(self) -> Option<crate::error::Error> {
self.first().map(|(field, message)| {
crate::error::Error::Validation {
field: field.clone(),
message: message.clone(),
}
})
}
}
impl fmt::Display for ValidationErrors {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let messages: Vec<String> = self.messages();
write!(f, "{}", messages.join("; "))
}
}
impl std::error::Error for ValidationErrors {}
impl From<ValidationErrors> for crate::error::Error {
fn from(errors: ValidationErrors) -> Self {
if let Some((field, message)) = errors.first() {
crate::error::Error::Validation {
field: field.clone(),
message: message.clone(),
}
} else {
crate::error::Error::Validation {
field: "unknown".to_string(),
message: "Validation failed".to_string(),
}
}
}
}
#[derive(Debug, Clone)]
pub enum ValidationRule {
Required,
Email,
Url,
MinLength(usize),
MaxLength(usize),
Length(usize),
Min(f64),
Max(f64),
Range(f64, f64),
Regex(String),
Alpha,
Alphanumeric,
Numeric,
Uuid,
In(Vec<String>),
NotIn(Vec<String>),
Confirmed(String),
Custom(String),
}
impl ValidationRule {
pub fn message(&self, field: &str) -> String {
match self {
ValidationRule::Required => format!("The {} field is required", field),
ValidationRule::Email => format!("The {} must be a valid email address", field),
ValidationRule::Url => format!("The {} must be a valid URL", field),
ValidationRule::MinLength(len) => {
format!("The {} must be at least {} characters", field, len)
}
ValidationRule::MaxLength(len) => {
format!("The {} must not exceed {} characters", field, len)
}
ValidationRule::Length(len) => {
format!("The {} must be exactly {} characters", field, len)
}
ValidationRule::Min(val) => format!("The {} must be at least {}", field, val),
ValidationRule::Max(val) => format!("The {} must not exceed {}", field, val),
ValidationRule::Range(min, max) => {
format!("The {} must be between {} and {}", field, min, max)
}
ValidationRule::Regex(pattern) => {
format!("The {} format is invalid (must match: {})", field, pattern)
}
ValidationRule::Alpha => format!("The {} must only contain letters", field),
ValidationRule::Alphanumeric => {
format!("The {} must only contain letters and numbers", field)
}
ValidationRule::Numeric => format!("The {} must be a number", field),
ValidationRule::Uuid => format!("The {} must be a valid UUID", field),
ValidationRule::In(values) => {
format!("The {} must be one of: {}", field, values.join(", "))
}
ValidationRule::NotIn(values) => {
format!("The {} must not be one of: {}", field, values.join(", "))
}
ValidationRule::Confirmed(other) => {
format!("The {} confirmation does not match {}", field, other)
}
ValidationRule::Custom(msg) => msg.clone(),
}
}
pub fn validate<T: ValidatableValue>(&self, value: &T) -> Result<(), String> {
match Validator::validate_rule(value, self, "field") {
Some(error) => Err(error),
None => Ok(()),
}
}
}
pub trait ValidatableValue {
fn is_empty_value(&self) -> bool;
fn as_str_value(&self) -> Option<&str>;
fn as_f64_value(&self) -> Option<f64>;
}
impl ValidatableValue for String {
fn is_empty_value(&self) -> bool {
self.trim().is_empty()
}
fn as_str_value(&self) -> Option<&str> {
Some(self.as_str())
}
fn as_f64_value(&self) -> Option<f64> {
self.parse().ok()
}
}
impl ValidatableValue for &str {
fn is_empty_value(&self) -> bool {
self.trim().is_empty()
}
fn as_str_value(&self) -> Option<&str> {
Some(self)
}
fn as_f64_value(&self) -> Option<f64> {
self.parse().ok()
}
}
impl<T: ValidatableValue> ValidatableValue for Option<T> {
fn is_empty_value(&self) -> bool {
match self {
Some(v) => v.is_empty_value(),
None => true,
}
}
fn as_str_value(&self) -> Option<&str> {
self.as_ref().and_then(|v| v.as_str_value())
}
fn as_f64_value(&self) -> Option<f64> {
self.as_ref().and_then(|v| v.as_f64_value())
}
}
macro_rules! impl_validatable_for_int {
($($t:ty),*) => {
$(
impl ValidatableValue for $t {
fn is_empty_value(&self) -> bool {
false }
fn as_str_value(&self) -> Option<&str> {
None
}
fn as_f64_value(&self) -> Option<f64> {
Some(*self as f64)
}
}
)*
};
}
impl_validatable_for_int!(i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64);
pub struct Validator;
impl Validator {
pub fn validate_rule<T: ValidatableValue>(
value: &T,
rule: &ValidationRule,
field: &str,
) -> Option<String> {
match rule {
ValidationRule::Required => {
if value.is_empty_value() {
return Some(rule.message(field));
}
}
ValidationRule::Email => {
if let Some(s) = value.as_str_value() {
if !Self::is_valid_email(s) {
return Some(rule.message(field));
}
}
}
ValidationRule::Url => {
if let Some(s) = value.as_str_value() {
if !Self::is_valid_url(s) {
return Some(rule.message(field));
}
}
}
ValidationRule::MinLength(min) => {
if let Some(s) = value.as_str_value() {
if s.len() < *min {
return Some(rule.message(field));
}
}
}
ValidationRule::MaxLength(max) => {
if let Some(s) = value.as_str_value() {
if s.len() > *max {
return Some(rule.message(field));
}
}
}
ValidationRule::Length(len) => {
if let Some(s) = value.as_str_value() {
if s.len() != *len {
return Some(rule.message(field));
}
}
}
ValidationRule::Min(min) => {
if let Some(n) = value.as_f64_value() {
if n < *min {
return Some(rule.message(field));
}
}
}
ValidationRule::Max(max) => {
if let Some(n) = value.as_f64_value() {
if n > *max {
return Some(rule.message(field));
}
}
}
ValidationRule::Range(min, max) => {
if let Some(n) = value.as_f64_value() {
if n < *min || n > *max {
return Some(rule.message(field));
}
}
}
ValidationRule::Regex(pattern) => {
if let Some(s) = value.as_str_value() {
if let Ok(re) = regex::Regex::new(pattern) {
if !re.is_match(s) {
return Some(rule.message(field));
}
}
}
}
ValidationRule::Alpha => {
if let Some(s) = value.as_str_value() {
if !s.chars().all(|c| c.is_alphabetic()) {
return Some(rule.message(field));
}
}
}
ValidationRule::Alphanumeric => {
if let Some(s) = value.as_str_value() {
if !s.chars().all(|c| c.is_alphanumeric()) {
return Some(rule.message(field));
}
}
}
ValidationRule::Numeric => {
if let Some(s) = value.as_str_value() {
if s.parse::<f64>().is_err() {
return Some(rule.message(field));
}
}
}
ValidationRule::Uuid => {
if let Some(s) = value.as_str_value() {
if uuid::Uuid::parse_str(s).is_err() {
return Some(rule.message(field));
}
}
}
ValidationRule::In(values) => {
if let Some(s) = value.as_str_value() {
if !values.iter().any(|v| v == s) {
return Some(rule.message(field));
}
}
}
ValidationRule::NotIn(values) => {
if let Some(s) = value.as_str_value() {
if values.iter().any(|v| v == s) {
return Some(rule.message(field));
}
}
}
ValidationRule::Confirmed(_) => {
}
ValidationRule::Custom(msg) => {
return Some(msg.clone());
}
}
None
}
pub fn is_valid_email(s: &str) -> bool {
let email_regex = regex::Regex::new(
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
).unwrap();
email_regex.is_match(s)
}
pub fn is_valid_url(s: &str) -> bool {
s.starts_with("http://") || s.starts_with("https://")
}
}
pub trait Validate {
fn validation_rules() -> Vec<(&'static str, Vec<ValidationRule>)> {
vec![]
}
fn validate(&self) -> Result<(), ValidationErrors>;
fn validate_all(&self) -> Result<(), ValidationErrors> {
self.validate()
}
fn custom_validations(&self) -> Result<(), ValidationErrors> {
Ok(())
}
fn validated(self) -> Result<Self, ValidationErrors>
where
Self: Sized,
{
self.validate()?;
Ok(self)
}
}
pub struct ValidationBuilder {
field: String,
rules: Vec<ValidationRule>,
}
impl ValidationBuilder {
pub fn new(field: impl Into<String>) -> Self {
Self {
field: field.into(),
rules: vec![],
}
}
pub fn required(mut self) -> Self {
self.rules.push(ValidationRule::Required);
self
}
pub fn email(mut self) -> Self {
self.rules.push(ValidationRule::Email);
self
}
pub fn url(mut self) -> Self {
self.rules.push(ValidationRule::Url);
self
}
pub fn min_length(mut self, len: usize) -> Self {
self.rules.push(ValidationRule::MinLength(len));
self
}
pub fn max_length(mut self, len: usize) -> Self {
self.rules.push(ValidationRule::MaxLength(len));
self
}
pub fn length(mut self, len: usize) -> Self {
self.rules.push(ValidationRule::Length(len));
self
}
pub fn min(mut self, val: f64) -> Self {
self.rules.push(ValidationRule::Min(val));
self
}
pub fn max(mut self, val: f64) -> Self {
self.rules.push(ValidationRule::Max(val));
self
}
pub fn range(mut self, min: f64, max: f64) -> Self {
self.rules.push(ValidationRule::Range(min, max));
self
}
pub fn regex(mut self, pattern: impl Into<String>) -> Self {
self.rules.push(ValidationRule::Regex(pattern.into()));
self
}
pub fn alpha(mut self) -> Self {
self.rules.push(ValidationRule::Alpha);
self
}
pub fn alphanumeric(mut self) -> Self {
self.rules.push(ValidationRule::Alphanumeric);
self
}
pub fn numeric(mut self) -> Self {
self.rules.push(ValidationRule::Numeric);
self
}
pub fn uuid(mut self) -> Self {
self.rules.push(ValidationRule::Uuid);
self
}
pub fn in_list(mut self, values: Vec<impl Into<String>>) -> Self {
self.rules.push(ValidationRule::In(
values.into_iter().map(|v| v.into()).collect(),
));
self
}
pub fn not_in(mut self, values: Vec<impl Into<String>>) -> Self {
self.rules.push(ValidationRule::NotIn(
values.into_iter().map(|v| v.into()).collect(),
));
self
}
pub fn custom(mut self, message: impl Into<String>) -> Self {
self.rules.push(ValidationRule::Custom(message.into()));
self
}
pub fn build(self) -> (String, Vec<ValidationRule>) {
(self.field, self.rules)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_email_validation() {
assert!(Validator::is_valid_email("test@example.com"));
assert!(Validator::is_valid_email("user.name+tag@domain.co.uk"));
assert!(!Validator::is_valid_email("invalid"));
assert!(!Validator::is_valid_email("@example.com"));
assert!(!Validator::is_valid_email("test@"));
}
#[test]
fn test_url_validation() {
assert!(Validator::is_valid_url("http://example.com"));
assert!(Validator::is_valid_url("https://example.com/path?query=1"));
assert!(!Validator::is_valid_url("example.com"));
assert!(!Validator::is_valid_url("ftp://example.com"));
}
#[test]
fn test_min_length() {
let rule = ValidationRule::MinLength(3);
assert!(Validator::validate_rule(&"ab".to_string(), &rule, "name").is_some());
assert!(Validator::validate_rule(&"abc".to_string(), &rule, "name").is_none());
assert!(Validator::validate_rule(&"abcd".to_string(), &rule, "name").is_none());
}
#[test]
fn test_max_length() {
let rule = ValidationRule::MaxLength(5);
assert!(Validator::validate_rule(&"abc".to_string(), &rule, "name").is_none());
assert!(Validator::validate_rule(&"abcde".to_string(), &rule, "name").is_none());
assert!(Validator::validate_rule(&"abcdef".to_string(), &rule, "name").is_some());
}
#[test]
fn test_range() {
let rule = ValidationRule::Range(1.0, 10.0);
assert!(Validator::validate_rule(&0, &rule, "age").is_some());
assert!(Validator::validate_rule(&1, &rule, "age").is_none());
assert!(Validator::validate_rule(&5, &rule, "age").is_none());
assert!(Validator::validate_rule(&10, &rule, "age").is_none());
assert!(Validator::validate_rule(&11, &rule, "age").is_some());
}
#[test]
fn test_validation_errors() {
let mut errors = ValidationErrors::new();
assert!(errors.is_empty());
errors.add("email", "Invalid email");
errors.add("email", "Email already taken");
errors.add("name", "Name is required");
assert!(!errors.is_empty());
assert_eq!(errors.len(), 2);
assert_eq!(errors.get("email").unwrap().len(), 2);
assert_eq!(errors.get("name").unwrap().len(), 1);
let messages = errors.messages();
assert_eq!(messages.len(), 3);
}
#[test]
fn test_validation_builder() {
let (field, rules) = ValidationBuilder::new("email")
.required()
.email()
.max_length(255)
.build();
assert_eq!(field, "email");
assert_eq!(rules.len(), 3);
}
}