use std::sync::LazyLock;
use regex::Regex;
use crate::validation::data_errors::DataValidationError;
static HANDLE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$")
.expect("handle regex should compile")
});
pub fn validate_handle(value: &str) -> Result<(), DataValidationError> {
if value.is_empty() {
return Err(DataValidationError::StringFormatInvalid {
format: "handle".to_string(),
value: value.to_string(),
reason: "handle cannot be empty".to_string(),
});
}
if value.len() > 253 {
return Err(DataValidationError::StringFormatInvalid {
format: "handle".to_string(),
value: value.to_string(),
reason: "handle exceeds maximum length of 253 characters".to_string(),
});
}
let labels: Vec<&str> = value.split('.').collect();
if labels.len() < 2 {
return Err(DataValidationError::StringFormatInvalid {
format: "handle".to_string(),
value: value.to_string(),
reason: "handle must have at least two labels".to_string(),
});
}
for label in &labels {
if label.is_empty() || label.len() > 63 {
return Err(DataValidationError::StringFormatInvalid {
format: "handle".to_string(),
value: value.to_string(),
reason: "handle labels must be 1-63 characters".to_string(),
});
}
}
let tld = labels.last().unwrap();
if tld.chars().all(|c| c.is_ascii_digit()) {
return Err(DataValidationError::StringFormatInvalid {
format: "handle".to_string(),
value: value.to_string(),
reason: "handle TLD must not be all numeric".to_string(),
});
}
if !HANDLE_REGEX.is_match(value) {
return Err(DataValidationError::StringFormatInvalid {
format: "handle".to_string(),
value: value.to_string(),
reason: "handle does not match expected syntax".to_string(),
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_handles() {
let valid = [
"john.test",
"jan.test",
"a234567890123456789.test",
"user.bsky.social",
"example.com",
"sub.domain.example.com",
];
for handle in valid {
assert!(
validate_handle(handle).is_ok(),
"should be valid: {}",
handle
);
}
}
#[test]
fn test_invalid_handles() {
let invalid = [
"",
"john-.test",
"john.0",
"-john.test",
"john",
"@handle",
"john..test",
];
for handle in invalid {
assert!(
validate_handle(handle).is_err(),
"should be invalid: {}",
handle
);
}
}
}