455 lines
14 KiB
Rust
455 lines
14 KiB
Rust
|
|
//! 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());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|