use crate::error::{PathSegment, ValidationError, ValidationErrors};
use crate::rules::AsStr;
use std::sync::OnceLock;
static EMAIL_REGEX: OnceLock<regex::Regex> = OnceLock::new();
pub const EMAIL_REGEX_PATTERN: &str =
r"(?i)^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*\.[a-z]{2,}$";
pub fn get_email_regex() -> &'static regex::Regex {
EMAIL_REGEX.get_or_init(|| {
regex::Regex::new(
r"(?i)^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*\.[a-z]{2,}$"
).expect("email regex is valid")
})
}
pub fn validate_email<T: AsStr>(
value: &T,
path: &[PathSegment],
errors: &mut ValidationErrors,
) {
let s = value.as_str_ref();
if s.is_empty() || !s.contains('@') || s.len() > 254 {
errors.add(
ValidationError::new("email", "invalid email format")
.with_path(path.to_vec()),
);
return;
}
if !get_email_regex().is_match(s) {
errors.add(
ValidationError::new("email", "invalid email format")
.with_path(path.to_vec()),
);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn path(name: &str) -> Vec<PathSegment> {
vec![PathSegment::Field(name.to_string())]
}
#[test]
fn test_valid_emails() {
let valid = vec![
"user@example.com",
"first.last@example.com",
"user+tag@example.com",
"user@sub.domain.example.com",
"user123@example.com",
"USER@EXAMPLE.COM",
"user@example.co.uk",
"user@123.123.123.example.com",
"test.email-with-dash@example.com",
];
for email in valid {
let mut errors = ValidationErrors::new();
validate_email(&email.to_string(), &path("email"), &mut errors);
assert!(errors.is_empty(), "Expected valid: {}", email);
}
}
#[test]
fn test_invalid_emails() {
let invalid = vec![
"", "not-an-email", "@example.com", "user@", "user@@example.com", "user@.com", "user@com", " user@example.com", "user@example.com ", "user@-example.com", ];
for email in invalid {
let mut errors = ValidationErrors::new();
validate_email(&email.to_string(), &path("email"), &mut errors);
assert!(!errors.is_empty(), "Expected invalid: '{}'", email);
assert_eq!(errors.errors()[0].code, "email");
}
}
#[test]
fn test_email_too_long() {
let long_local = "a".repeat(250);
let email = format!("{}@example.com", long_local);
assert!(email.len() > 254);
let mut errors = ValidationErrors::new();
validate_email(&email, &path("email"), &mut errors);
assert!(!errors.is_empty());
}
#[test]
fn test_email_error_code() {
let mut errors = ValidationErrors::new();
validate_email(&"invalid".to_string(), &path("email"), &mut errors);
assert_eq!(errors.errors()[0].code, "email");
assert_eq!(errors.errors()[0].message, "invalid email format");
}
}