veza/veza-common/src/utils/validation.rs

455 lines
14 KiB
Rust
Raw Normal View History

2025-12-03 21:24:14 +00:00
//! 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<T: PartialOrd>(value: &T, min: &T, max: &T) -> bool {
value >= min && value <= max
}
/// Validate a numeric range and return a Result
pub fn validate_range_result<T: PartialOrd + std::fmt::Display + Clone>(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());
}
}