//! Validation utilities for Veza services //! //! This module provides validation functions for common data types //! such as email addresses, usernames, passwords, URLs, etc. use crate::error::{CommonError, CommonResult}; use lazy_static::lazy_static; use regex::Regex; lazy_static! { // Email validation regex - RFC 5322 compliant static ref EMAIL_REGEX: Regex = Regex::new( r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$" ).unwrap(); // Username validation regex - 3-30 characters, alphanumeric and underscores static ref USERNAME_REGEX: Regex = Regex::new( r"^[a-zA-Z0-9_]{3,30}$" ).unwrap(); // Password validation regex - at least 8 characters, alphanumeric and special chars // Note: We'll validate letter and number requirements separately static ref PASSWORD_REGEX: Regex = Regex::new( r"^[A-Za-z\d@$!%*#?&]{8,}$" ).unwrap(); // URL validation regex static ref URL_REGEX: Regex = Regex::new( r"^https?://(?:[-\w.])+(?:[:\d]+)?(?:/(?:[\w/_.])*)?(?:\?(?:[\w&=%.])*)?(?:#(?:[\w.])*)?$" ).unwrap(); // Phone number validation regex - international format (E.164) // Must be 10-15 digits, optionally starting with + static ref PHONE_REGEX: Regex = Regex::new( r"^\+?[1-9]\d{9,14}$" ).unwrap(); } /// Validate an email address /// /// Returns true if the email address is valid according to RFC 5322. /// /// # Examples /// ``` /// use veza_common::utils::validation::validate_email; /// /// assert!(validate_email("test@example.com")); /// assert!(validate_email("user.name@example.co.uk")); /// assert!(!validate_email("invalid-email")); /// ``` pub fn validate_email(email: &str) -> bool { if email.is_empty() || email.len() > 254 { return false; } // Check basic structure: must contain @ and . if !email.contains('@') || !email.contains('.') { return false; } // Split by @ and check parts let parts: Vec<&str> = email.split('@').collect(); if parts.len() != 2 { return false; } let domain = parts[1]; // Domain must contain at least one dot after @ if !domain.contains('.') { return false; } EMAIL_REGEX.is_match(email) } /// Validate an email address and return a Result /// /// Returns Ok(()) if the email is valid, or an error if invalid. pub fn validate_email_result(email: &str) -> CommonResult<()> { if validate_email(email) { Ok(()) } else { Err(CommonError::ValidationError(format!("Invalid email address: {}", email))) } } /// Validate a username /// /// Username must be: /// - 3 to 30 characters long /// - Contains only alphanumeric characters and underscores /// /// # Examples /// ``` /// use veza_common::utils::validation::validate_username; /// /// assert!(validate_username("user123")); /// assert!(validate_username("test_user")); /// assert!(!validate_username("ab")); // Too short /// assert!(!validate_username("user-name")); // Contains hyphen /// ``` pub fn validate_username(username: &str) -> bool { USERNAME_REGEX.is_match(username) } /// Validate a username and return a Result /// /// Returns Ok(()) if the username is valid, or an error if invalid. pub fn validate_username_result(username: &str) -> CommonResult<()> { if validate_username(username) { Ok(()) } else { Err(CommonError::ValidationError( format!("Invalid username: must be 3-30 characters, alphanumeric and underscores only") )) } } /// Validate a password /// /// Password must be: /// - At least 8 characters long /// - Contains at least one letter /// - Contains at least one number /// - May contain special characters: @$!%*#?& /// /// # Examples /// ``` /// use veza_common::utils::validation::validate_password; /// /// assert!(validate_password("Password123")); /// assert!(validate_password("MyP@ssw0rd")); /// assert!(!validate_password("short")); // Too short /// assert!(!validate_password("NoNumbers")); // No numbers /// ``` pub fn validate_password(password: &str) -> bool { if password.is_empty() || password.len() < 8 { return false; } // Check format (alphanumeric and allowed special chars) if !PASSWORD_REGEX.is_match(password) { return false; } // Check for at least one letter let has_letter = password.chars().any(|c| c.is_alphabetic()); if !has_letter { return false; } // Check for at least one number let has_number = password.chars().any(|c| c.is_ascii_digit()); if !has_number { return false; } true } /// Validate a password and return a Result /// /// Returns Ok(()) if the password is valid, or an error if invalid. pub fn validate_password_result(password: &str) -> CommonResult<()> { if validate_password(password) { Ok(()) } else { Err(CommonError::ValidationError( format!("Invalid password: must be at least 8 characters with at least one letter and one number") )) } } /// Validate a URL /// /// Validates HTTP/HTTPS URLs. /// /// # Examples /// ``` /// use veza_common::utils::validation::validate_url; /// /// assert!(validate_url("https://example.com")); /// assert!(validate_url("http://example.com/path?query=value")); /// assert!(!validate_url("not-a-url")); /// ``` pub fn validate_url(url: &str) -> bool { if url.is_empty() { return false; } URL_REGEX.is_match(url) } /// Validate a URL and return a Result /// /// Returns Ok(()) if the URL is valid, or an error if invalid. pub fn validate_url_result(url: &str) -> CommonResult<()> { if validate_url(url) { Ok(()) } else { Err(CommonError::ValidationError(format!("Invalid URL: {}", url))) } } /// Validate a phone number /// /// Validates international phone number format (E.164). /// /// # Examples /// ``` /// use veza_common::utils::validation::validate_phone; /// /// assert!(validate_phone("+1234567890")); /// assert!(validate_phone("1234567890")); /// assert!(!validate_phone("123")); // Too short /// ``` pub fn validate_phone(phone: &str) -> bool { if phone.is_empty() { return false; } PHONE_REGEX.is_match(phone) } /// Validate a phone number and return a Result /// /// Returns Ok(()) if the phone number is valid, or an error if invalid. pub fn validate_phone_result(phone: &str) -> CommonResult<()> { if validate_phone(phone) { Ok(()) } else { Err(CommonError::ValidationError(format!("Invalid phone number: {}", phone))) } } /// Validate a string length /// /// Returns true if the string length is within the specified range (inclusive). pub fn validate_length(value: &str, min: usize, max: usize) -> bool { let len = value.len(); len >= min && len <= max } /// Validate a string length and return a Result pub fn validate_length_result(value: &str, min: usize, max: usize) -> CommonResult<()> { if validate_length(value, min, max) { Ok(()) } else { Err(CommonError::ValidationError( format!("String length must be between {} and {} characters", min, max) )) } } /// Validate that a string is not empty pub fn validate_not_empty(value: &str) -> bool { !value.trim().is_empty() } /// Validate that a string is not empty and return a Result pub fn validate_not_empty_result(value: &str) -> CommonResult<()> { if validate_not_empty(value) { Ok(()) } else { Err(CommonError::ValidationError("Value cannot be empty".to_string())) } } /// Validate a numeric range /// /// Returns true if the value is within the specified range (inclusive). pub fn validate_range(value: &T, min: &T, max: &T) -> bool { value >= min && value <= max } /// Validate a numeric range and return a Result pub fn validate_range_result(value: T, min: T, max: T) -> CommonResult<()> { if validate_range(&value, &min, &max) { Ok(()) } else { Err(CommonError::ValidationError( format!("Value must be between {} and {}", min, max) )) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_validate_email() { // Valid emails assert!(validate_email("test@example.com")); assert!(validate_email("user.name@example.com")); assert!(validate_email("user+tag@example.co.uk")); assert!(validate_email("user_name@example.com")); assert!(validate_email("user123@example123.com")); assert!(validate_email("a@b.co")); // Invalid emails assert!(!validate_email("")); assert!(!validate_email("invalid-email")); assert!(!validate_email("@example.com")); assert!(!validate_email("test@")); assert!(!validate_email("test@.com")); assert!(!validate_email("test @example.com")); assert!(!validate_email("test@example")); assert!(!validate_email(&"a".repeat(255))); // Too long } #[test] fn test_validate_email_result() { assert!(validate_email_result("test@example.com").is_ok()); assert!(validate_email_result("invalid-email").is_err()); } #[test] fn test_validate_username() { // Valid usernames assert!(validate_username("user123")); assert!(validate_username("test_user")); assert!(validate_username("abc")); assert!(validate_username("User123")); assert!(validate_username("_user_")); assert!(validate_username(&"a".repeat(30))); // Max length // Invalid usernames assert!(!validate_username("")); assert!(!validate_username("ab")); // Too short assert!(!validate_username(&"a".repeat(31))); // Too long assert!(!validate_username("user-name")); // Contains hyphen assert!(!validate_username("user name")); // Contains space assert!(!validate_username("user.name")); // Contains dot assert!(!validate_username("user@name")); // Contains @ } #[test] fn test_validate_username_result() { assert!(validate_username_result("user123").is_ok()); assert!(validate_username_result("ab").is_err()); } #[test] fn test_validate_password() { // Valid passwords assert!(validate_password("Password123")); assert!(validate_password("MyP@ssw0rd")); assert!(validate_password("test123456")); assert!(validate_password("ABCdef123")); assert!(validate_password("Pass@123")); // Invalid passwords assert!(!validate_password("")); assert!(!validate_password("short")); // Too short assert!(!validate_password("NoNumbers")); // No numbers assert!(!validate_password("12345678")); // No letters assert!(!validate_password("abcdefgh")); // No numbers } #[test] fn test_validate_password_result() { assert!(validate_password_result("Password123").is_ok()); assert!(validate_password_result("short").is_err()); } #[test] fn test_validate_url() { // Valid URLs assert!(validate_url("https://example.com")); assert!(validate_url("http://example.com")); assert!(validate_url("https://example.com/path")); assert!(validate_url("https://example.com/path?query=value")); assert!(validate_url("https://example.com/path?query=value#fragment")); assert!(validate_url("http://subdomain.example.com")); // Invalid URLs assert!(!validate_url("")); assert!(!validate_url("not-a-url")); assert!(!validate_url("example.com")); // Missing protocol assert!(!validate_url("ftp://example.com")); // Unsupported protocol } #[test] fn test_validate_url_result() { assert!(validate_url_result("https://example.com").is_ok()); assert!(validate_url_result("not-a-url").is_err()); } #[test] fn test_validate_phone() { // Valid phone numbers assert!(validate_phone("+1234567890")); assert!(validate_phone("1234567890")); assert!(validate_phone("+12345678901234")); // Invalid phone numbers assert!(!validate_phone("")); assert!(!validate_phone("123")); // Too short (less than 10 digits) assert!(!validate_phone("123456789")); // Too short (9 digits) assert!(!validate_phone("+0123456789")); // Starts with 0 assert!(!validate_phone("123-456-7890")); // Contains hyphens assert!(!validate_phone("(123) 456-7890")); // Contains parentheses } #[test] fn test_validate_phone_result() { assert!(validate_phone_result("+1234567890").is_ok()); assert!(validate_phone_result("123").is_err()); } #[test] fn test_validate_length() { assert!(validate_length("abc", 3, 5)); assert!(validate_length("abc", 3, 3)); assert!(validate_length("abcde", 3, 5)); assert!(!validate_length("ab", 3, 5)); // Too short assert!(!validate_length("abcdef", 3, 5)); // Too long } #[test] fn test_validate_length_result() { assert!(validate_length_result("abc", 3, 5).is_ok()); assert!(validate_length_result("ab", 3, 5).is_err()); } #[test] fn test_validate_not_empty() { assert!(validate_not_empty("test")); assert!(validate_not_empty(" test ")); assert!(!validate_not_empty("")); assert!(!validate_not_empty(" ")); } #[test] fn test_validate_not_empty_result() { assert!(validate_not_empty_result("test").is_ok()); assert!(validate_not_empty_result("").is_err()); } #[test] fn test_validate_range() { assert!(validate_range(&5, &1, &10)); assert!(validate_range(&1, &1, &10)); assert!(validate_range(&10, &1, &10)); assert!(!validate_range(&0, &1, &10)); // Too low assert!(!validate_range(&11, &1, &10)); // Too high } #[test] fn test_validate_range_result() { assert!(validate_range_result(5, 1, 10).is_ok()); assert!(validate_range_result(0, 1, 10).is_err()); } }