use log::error;
fn is_chinese_char(c: char) -> bool {
let ranges = [
(0x4E00, 0x9FFF), (0x3400, 0x4DBF), (0x20000, 0x2A6DF), (0x2A700, 0x2B73F), (0x2B740, 0x2B81F), (0x2B820, 0x2CEAF), (0x2CEB0, 0x2EBEF), (0x3000, 0x303F), (0x31C0, 0x31EF), (0x2F00, 0x2FD5), (0x2E80, 0x2EFF), (0xF900, 0xFAFF), (0x2F800, 0x2FA1F), ];
let code = c as u32;
ranges
.iter()
.any(|&(start, end)| code >= start && code <= end)
}
pub fn validate_string_length(input: String, max_len: usize, field_name: &str) -> String {
if input.len() > max_len {
error!(
"{} exceeds maximum length of {} characters: {}",
field_name,
max_len,
input.len()
);
input[..max_len].to_string()
} else {
input
}
}
pub fn validate_required(value: &str, field_name: &str) -> bool {
if value.is_empty() {
error!("{} is required but empty", field_name);
false
} else {
true
}
}
pub fn validate_content_size(content: &str, max_size: usize, content_type: &str) -> bool {
if content.len() > max_size {
error!(
"{} content exceeds maximum size of {} bytes: {}",
content_type,
max_size,
content.len()
);
false
} else {
true
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ValidationResult {
Valid,
Warning(String),
Invalid(String),
}
impl ValidationResult {
pub fn is_valid(&self) -> bool {
matches!(self, ValidationResult::Valid | ValidationResult::Warning(_))
}
pub fn is_strictly_valid(&self) -> bool {
matches!(self, ValidationResult::Valid)
}
pub fn error(&self) -> Option<&str> {
match self {
ValidationResult::Invalid(msg) | ValidationResult::Warning(msg) => Some(msg),
ValidationResult::Valid => None,
}
}
}
pub trait ValidateBuilder {
fn validate(&self) -> ValidationResult;
fn validate_and_log(&self) -> bool {
match self.validate() {
ValidationResult::Valid => true,
ValidationResult::Warning(msg) => {
error!("Builder validation warning: {}", msg);
true
}
ValidationResult::Invalid(msg) => {
error!("Builder validation failed: {}", msg);
false
}
}
}
}
pub mod message_limits {
pub const TEXT_MESSAGE_MAX_SIZE: usize = 150 * 1024; pub const RICH_MESSAGE_MAX_SIZE: usize = 30 * 1024; }
pub mod uuid_limits {
pub const MAX_LENGTH: usize = 50;
}
pub mod password_limits {
pub const MIN_LENGTH: usize = 8;
pub const MAX_LENGTH: usize = 128;
pub const REQUIRE_UPPERCASE: bool = true;
pub const REQUIRE_LOWERCASE: bool = true;
pub const REQUIRE_DIGIT: bool = true;
pub const REQUIRE_SPECIAL: bool = true;
}
pub mod file_limits {
pub const MAX_FILE_SIZE: usize = 100 * 1024 * 1024;
pub const IM_MAX_FILE_SIZE: usize = 50 * 1024 * 1024;
pub const MAX_IMAGE_SIZE: usize = 20 * 1024 * 1024;
pub const MAX_FILENAME_LENGTH: usize = 255;
pub const MAX_EXTENSION_LENGTH: usize = 10;
pub const ALLOWED_FILE_TYPES: &[&str] = &[
"pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "csv", "zip", "rar", "7z",
"tar", "gz", "jpg", "jpeg", "png", "gif", "bmp", "svg", "mp4", "avi", "mov", "wmv", "flv",
"mkv", "mp3", "wav", "flac", "aac", "ogg", "json", "xml", "html", "css", "js", "py", "rs",
"go",
];
pub const ALLOWED_IMAGE_TYPES: &[&str] = &["jpg", "jpeg", "png", "gif", "bmp", "svg", "webp"];
}
pub fn validate_password_strength(password: &str) -> ValidationResult {
if password.len() < password_limits::MIN_LENGTH {
return ValidationResult::Invalid(format!(
"Password must be at least {} characters long",
password_limits::MIN_LENGTH
));
}
if password.len() > password_limits::MAX_LENGTH {
return ValidationResult::Invalid(format!(
"Password must not exceed {} characters",
password_limits::MAX_LENGTH
));
}
let mut has_uppercase = false;
let mut has_lowercase = false;
let mut has_digit = false;
let mut has_special = false;
for ch in password.chars() {
if ch.is_uppercase() {
has_uppercase = true;
} else if ch.is_lowercase() {
has_lowercase = true;
} else if ch.is_ascii_digit() {
has_digit = true;
} else if ch.is_ascii_punctuation() || ch.is_ascii_whitespace() {
has_special = true;
}
}
let mut missing_requirements = Vec::new();
if password_limits::REQUIRE_UPPERCASE && !has_uppercase {
missing_requirements.push("uppercase letter");
}
if password_limits::REQUIRE_LOWERCASE && !has_lowercase {
missing_requirements.push("lowercase letter");
}
if password_limits::REQUIRE_DIGIT && !has_digit {
missing_requirements.push("digit");
}
if password_limits::REQUIRE_SPECIAL && !has_special {
missing_requirements.push("special character");
}
if !missing_requirements.is_empty() {
return ValidationResult::Invalid(format!(
"Password is missing required character types: {}",
missing_requirements.join(", ")
));
}
ValidationResult::Valid
}
pub fn validate_and_sanitize_password(
password: String,
field_name: &str,
) -> (String, ValidationResult) {
let password = password.trim().to_string();
let result = validate_password_strength(&password);
if let ValidationResult::Invalid(msg) = &result {
error!("{} validation failed: {}", field_name, msg);
}
(password, result)
}
pub fn validate_file_size(file_size: usize, max_size: usize, file_name: &str) -> ValidationResult {
if file_size == 0 {
return ValidationResult::Invalid("File size cannot be zero".to_string());
}
if file_size > max_size {
return ValidationResult::Invalid(format!(
"File '{}' exceeds maximum size of {} bytes (actual: {} bytes)",
file_name, max_size, file_size
));
}
ValidationResult::Valid
}
pub fn validate_file_name(file_name: &str) -> (String, ValidationResult) {
let cleaned_name = file_name.trim();
if cleaned_name.is_empty() {
return (
String::new(),
ValidationResult::Invalid("File name cannot be empty".to_string()),
);
}
if cleaned_name.len() > file_limits::MAX_FILENAME_LENGTH {
return (
String::new(),
ValidationResult::Invalid(format!(
"File name exceeds maximum length of {} characters",
file_limits::MAX_FILENAME_LENGTH
)),
);
}
let invalid_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|'];
for ch in cleaned_name.chars() {
if invalid_chars.contains(&ch) {
return (
String::new(),
ValidationResult::Invalid(format!(
"File name contains invalid character: '{}'",
ch
)),
);
}
}
let reserved_names = [
"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8",
"COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
];
let upper_name = cleaned_name.to_uppercase();
let name_without_ext = if let Some(dot_pos) = upper_name.find('.') {
&upper_name[..dot_pos]
} else {
&upper_name
};
if reserved_names.contains(&name_without_ext) {
return (
String::new(),
ValidationResult::Invalid(format!("'{}' is a reserved file name", cleaned_name)),
);
}
(cleaned_name.to_string(), ValidationResult::Valid)
}
pub fn validate_file_extension(
file_name: &str,
allowed_types: &[&str],
) -> (Option<String>, ValidationResult) {
let (_, validation_result) = validate_file_name(file_name);
if !validation_result.is_valid() {
return (None, validation_result);
}
let extension = if let Some(dot_pos) = file_name.rfind('.') {
let ext = &file_name[dot_pos + 1..];
if ext.is_empty() {
return (
None,
ValidationResult::Invalid("File extension cannot be empty".to_string()),
);
}
if ext.len() > file_limits::MAX_EXTENSION_LENGTH {
return (
None,
ValidationResult::Invalid(format!(
"File extension exceeds maximum length of {} characters",
file_limits::MAX_EXTENSION_LENGTH
)),
);
}
Some(ext.to_lowercase())
} else {
return (
None,
ValidationResult::Invalid("File must have an extension".to_string()),
);
};
if let Some(ref ext) = extension {
if !allowed_types.contains(&ext.as_str()) {
return (
None,
ValidationResult::Invalid(format!(
"File extension '.{}' is not allowed. Allowed types: {}",
ext,
allowed_types.join(", ")
)),
);
}
}
(extension, ValidationResult::Valid)
}
pub fn validate_image_file(file_data: &[u8], file_name: &str) -> ValidationResult {
let size_result = validate_file_size(file_data.len(), file_limits::MAX_IMAGE_SIZE, file_name);
if !size_result.is_valid() {
return size_result;
}
let (_, ext_result) = validate_file_extension(file_name, file_limits::ALLOWED_IMAGE_TYPES);
if !ext_result.is_valid() {
return ext_result;
}
ValidationResult::Valid
}
pub fn validate_upload_file(
file_data: &[u8],
file_name: &str,
is_im_upload: bool,
) -> ValidationResult {
let max_size = if is_im_upload {
file_limits::IM_MAX_FILE_SIZE
} else {
file_limits::MAX_FILE_SIZE
};
let size_result = validate_file_size(file_data.len(), max_size, file_name);
if !size_result.is_valid() {
return size_result;
}
let (_, name_result) = validate_file_name(file_name);
if !name_result.is_valid() {
return name_result;
}
let (_, ext_result) = validate_file_extension(file_name, file_limits::ALLOWED_FILE_TYPES);
if !ext_result.is_valid() {
return ext_result;
}
ValidationResult::Valid
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_string_length() {
let input = "hello".to_string();
let result = validate_string_length(input, 10, "test_field");
assert_eq!(result, "hello");
let input = "hello world".to_string();
let result = validate_string_length(input, 5, "test_field");
assert_eq!(result, "hello");
}
#[test]
fn test_validate_required() {
assert!(validate_required("hello", "test_field"));
assert!(!validate_required("", "test_field"));
}
#[test]
fn test_validate_content_size() {
assert!(validate_content_size("hello", 10, "test_content"));
assert!(!validate_content_size("hello world", 5, "test_content"));
}
#[test]
fn test_validation_result() {
let valid = ValidationResult::Valid;
assert!(valid.is_valid());
assert!(valid.is_strictly_valid());
assert!(valid.error().is_none());
let warning = ValidationResult::Warning("test warning".to_string());
assert!(warning.is_valid());
assert!(!warning.is_strictly_valid());
assert_eq!(warning.error(), Some("test warning"));
let invalid = ValidationResult::Invalid("test error".to_string());
assert!(!invalid.is_valid());
assert!(!invalid.is_strictly_valid());
assert_eq!(invalid.error(), Some("test error"));
}
#[test]
fn test_validate_password_strength() {
let valid_password = "SecurePass123!";
assert!(matches!(
validate_password_strength(valid_password),
ValidationResult::Valid
));
let short_password = "Short1!";
assert!(matches!(
validate_password_strength(short_password),
ValidationResult::Invalid(_)
));
let long_password = "a".repeat(password_limits::MAX_LENGTH + 1);
assert!(matches!(
validate_password_strength(&long_password),
ValidationResult::Invalid(_)
));
let no_upper = "lowercase123!";
assert!(matches!(
validate_password_strength(no_upper),
ValidationResult::Invalid(_)
));
let no_lower = "UPPERCASE123!";
assert!(matches!(
validate_password_strength(no_lower),
ValidationResult::Invalid(_)
));
let no_digit = "NoDigitsHere!";
assert!(matches!(
validate_password_strength(no_digit),
ValidationResult::Invalid(_)
));
let no_special = "NoSpecialChars123";
assert!(matches!(
validate_password_strength(no_special),
ValidationResult::Invalid(_)
));
}
#[test]
fn test_validate_and_sanitize_password() {
let (password, result) =
validate_and_sanitize_password(" GoodPass123! ".to_string(), "test_password");
assert_eq!(password, "GoodPass123!");
assert!(matches!(result, ValidationResult::Valid));
let (password, result) =
validate_and_sanitize_password(" weak ".to_string(), "test_password");
assert_eq!(password, "weak");
assert!(matches!(result, ValidationResult::Invalid(_)));
}
#[test]
fn test_validate_file_size() {
assert!(matches!(
validate_file_size(1024, 2048, "test.txt"),
ValidationResult::Valid
));
assert!(matches!(
validate_file_size(0, 2048, "test.txt"),
ValidationResult::Invalid(_)
));
assert!(matches!(
validate_file_size(3000, 2048, "test.txt"),
ValidationResult::Invalid(_)
));
}
#[test]
fn test_validate_file_name() {
let (name, result) = validate_file_name("document.pdf");
assert_eq!(name, "document.pdf");
assert!(matches!(result, ValidationResult::Valid));
let (name, result) = validate_file_name(" document.pdf ");
assert_eq!(name, "document.pdf");
assert!(matches!(result, ValidationResult::Valid));
let (name, result) = validate_file_name("");
assert_eq!(name, "");
assert!(matches!(result, ValidationResult::Invalid(_)));
let long_name = "a".repeat(file_limits::MAX_FILENAME_LENGTH + 1);
let (name, result) = validate_file_name(&long_name);
assert_eq!(name, "");
assert!(matches!(result, ValidationResult::Invalid(_)));
let invalid_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|'];
for ch in invalid_chars {
let (name, result) = validate_file_name(&format!("test{}.txt", ch));
assert_eq!(name, "");
assert!(matches!(result, ValidationResult::Invalid(_)));
}
let reserved_names = ["CON", "PRN", "AUX", "NUL", "COM1", "LPT1"];
for name in reserved_names {
let (cleaned, result) = validate_file_name(name);
assert_eq!(cleaned, "");
assert!(matches!(result, ValidationResult::Invalid(_)));
}
}
#[test]
fn test_validate_file_extension() {
let (ext, result) =
validate_file_extension("document.pdf", file_limits::ALLOWED_FILE_TYPES);
assert_eq!(ext, Some("pdf".to_string()));
assert!(matches!(result, ValidationResult::Valid));
let (ext, result) = validate_file_extension("document", file_limits::ALLOWED_FILE_TYPES);
assert_eq!(ext, None);
assert!(matches!(result, ValidationResult::Invalid(_)));
let (ext, result) = validate_file_extension("document.", file_limits::ALLOWED_FILE_TYPES);
assert_eq!(ext, None);
assert!(matches!(result, ValidationResult::Invalid(_)));
let (ext, result) =
validate_file_extension("document.exe", file_limits::ALLOWED_FILE_TYPES);
assert_eq!(ext, None);
assert!(matches!(result, ValidationResult::Invalid(_)));
let (ext, result) =
validate_file_extension("document.PDF", file_limits::ALLOWED_FILE_TYPES);
assert_eq!(ext, Some("pdf".to_string()));
assert!(matches!(result, ValidationResult::Valid));
}
#[test]
fn test_validate_image_file() {
let image_data = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; assert!(matches!(
validate_image_file(&image_data, "image.png"),
ValidationResult::Valid
));
let large_image_data = vec![0u8; file_limits::MAX_IMAGE_SIZE + 1];
assert!(matches!(
validate_image_file(&large_image_data, "large.png"),
ValidationResult::Invalid(_)
));
let image_data = vec![0x89, 0x50, 0x4E, 0x47];
assert!(matches!(
validate_image_file(&image_data, "image.tiff"),
ValidationResult::Invalid(_)
));
}
#[test]
fn test_validate_upload_file() {
let file_data = vec![0u8; 1024];
assert!(matches!(
validate_upload_file(&file_data, "document.pdf", false),
ValidationResult::Valid
));
let im_file_data = vec![0u8; file_limits::IM_MAX_FILE_SIZE + 1];
assert!(matches!(
validate_upload_file(&im_file_data, "large.pdf", true),
ValidationResult::Invalid(_)
));
let normal_file_data = vec![0u8; file_limits::MAX_FILE_SIZE + 1];
assert!(matches!(
validate_upload_file(&normal_file_data, "large.pdf", false),
ValidationResult::Invalid(_)
));
}
#[test]
fn test_is_chinese_char() {
assert!(is_chinese_char('中'));
assert!(is_chinese_char('文'));
assert!(is_chinese_char('字'));
assert!(is_chinese_char('符'));
assert!(is_chinese_char('。'));
assert!(!is_chinese_char(','));
assert!(!is_chinese_char('a'));
assert!(!is_chinese_char('1'));
assert!(!is_chinese_char('!'));
assert!(!is_chinese_char(' '));
}
#[test]
fn test_validate_name() {
assert!(matches!(
validate_name("张三", "姓名"),
ValidationResult::Valid
));
assert!(matches!(
validate_name("John Smith", "姓名"),
ValidationResult::Valid
));
assert!(matches!(
validate_name("", "姓名"),
ValidationResult::Invalid(_)
));
assert!(matches!(
validate_name("A", "姓名"),
ValidationResult::Invalid(_)
));
let long_name = "A".repeat(employee_limits::NAME_MAX_LENGTH + 1);
assert!(matches!(
validate_name(&long_name, "姓名"),
ValidationResult::Invalid(_)
));
}
#[test]
fn test_validate_email() {
assert!(matches!(
validate_email("test@example.com", "邮箱"),
ValidationResult::Valid
));
assert!(matches!(
validate_email("user.name+tag@domain.co.uk", "邮箱"),
ValidationResult::Valid
));
assert!(matches!(
validate_email("invalid-email", "邮箱"),
ValidationResult::Invalid(_)
));
assert!(matches!(
validate_email("@example.com", "邮箱"),
ValidationResult::Invalid(_)
));
assert!(matches!(
validate_email("test@", "邮箱"),
ValidationResult::Invalid(_)
));
assert!(matches!(
validate_email("", "邮箱"),
ValidationResult::Invalid(_)
));
let long_email = format!(
"{}@example.com",
"a".repeat(employee_limits::EMAIL_MAX_LENGTH)
);
assert!(matches!(
validate_email(&long_email, "邮箱"),
ValidationResult::Invalid(_)
));
}
#[test]
fn test_validate_phone() {
assert!(matches!(
validate_phone("13812345678", "电话"),
ValidationResult::Valid
));
assert!(matches!(
validate_phone("+86-138-1234-5678", "电话"),
ValidationResult::Valid
));
assert!(matches!(
validate_phone("021-12345678", "电话"),
ValidationResult::Valid
));
assert!(matches!(
validate_phone("123", "电话"),
ValidationResult::Invalid(_)
));
assert!(matches!(
validate_phone("abc123def", "电话"),
ValidationResult::Invalid(_)
));
assert!(matches!(
validate_phone("", "电话"),
ValidationResult::Valid
));
let long_phone = "1".repeat(employee_limits::PHONE_MAX_LENGTH + 1);
assert!(matches!(
validate_phone(&long_phone, "电话"),
ValidationResult::Invalid(_)
));
}
#[test]
fn test_validate_work_experience() {
assert!(matches!(
validate_work_experience(5, "工作年限"),
ValidationResult::Valid
));
assert!(matches!(
validate_work_experience(0, "工作年限"),
ValidationResult::Valid
));
assert!(matches!(
validate_work_experience(employee_limits::WORK_EXPERIENCE_MAX, "工作年限"),
ValidationResult::Valid
));
assert!(matches!(
validate_work_experience(employee_limits::WORK_EXPERIENCE_MAX + 1, "工作年限"),
ValidationResult::Invalid(_)
));
}
#[test]
fn test_validate_birthday() {
assert!(matches!(
validate_birthday(&Some("1990-01-01".to_string()), "生日"),
ValidationResult::Valid
));
assert!(matches!(
validate_birthday(&Some("2000-12-31".to_string()), "生日"),
ValidationResult::Valid
));
assert!(matches!(
validate_birthday(&None, "生日"),
ValidationResult::Valid
));
assert!(matches!(
validate_birthday(&Some("invalid-date".to_string()), "生日"),
ValidationResult::Invalid(_)
));
assert!(matches!(
validate_birthday(&Some("1990/01/01".to_string()), "生日"),
ValidationResult::Invalid(_)
));
assert!(matches!(
validate_birthday(&Some("1990-13-01".to_string()), "生日"),
ValidationResult::Invalid(_)
));
}
#[test]
fn test_validate_expected_salary() {
assert!(matches!(
validate_expected_salary(&Some("10000-15000".to_string()), "期望薪资"),
ValidationResult::Valid
));
assert!(matches!(
validate_expected_salary(&Some("面议".to_string()), "期望薪资"),
ValidationResult::Valid
));
assert!(matches!(
validate_expected_salary(&None, "期望薪资"),
ValidationResult::Valid
));
let long_salary = "1".repeat(employee_limits::EXPECTED_SALARY_MAX_LENGTH + 1);
assert!(matches!(
validate_expected_salary(&Some(long_salary), "期望薪资"),
ValidationResult::Invalid(_)
));
}
#[test]
fn test_validate_tags() {
let valid_tags = vec![
"Java".to_string(),
"Python".to_string(),
"React".to_string(),
];
assert!(matches!(
validate_tags(&valid_tags, "技能标签"),
ValidationResult::Valid
));
assert!(matches!(
validate_tags(&[], "技能标签"),
ValidationResult::Valid
));
let too_many_tags: Vec<String> = (0..employee_limits::MAX_TALENT_TAGS + 1)
.map(|i| format!("tag{}", i))
.collect();
assert!(matches!(
validate_tags(&too_many_tags, "技能标签"),
ValidationResult::Invalid(_)
));
let tags_with_empty = vec!["Java".to_string(), "".to_string()];
assert!(matches!(
validate_tags(&tags_with_empty, "技能标签"),
ValidationResult::Invalid(_)
));
let long_tag = "a".repeat(employee_limits::TAG_MAX_LENGTH + 1);
let tags_with_long = vec![long_tag];
assert!(matches!(
validate_tags(&tags_with_long, "技能标签"),
ValidationResult::Invalid(_)
));
}
#[test]
fn test_sanitize_name() {
assert_eq!(sanitize_name(" 张三 "), "张三");
assert_eq!(sanitize_name("张 三"), "张 三");
assert_eq!(sanitize_name("John Smith"), "John Smith");
assert_eq!(sanitize_name(""), "");
}
#[test]
fn test_sanitize_tags() {
let input_tags = vec![
" Java ".to_string(),
"Python".to_string(),
" ".to_string(),
"React JS".to_string(),
];
let sanitized = sanitize_tags(&input_tags);
assert_eq!(sanitized, vec!["java", "python", "react js"]);
}
#[test]
fn test_sanitize_tag() {
assert_eq!(sanitize_tag(" Java-Script "), "java_script");
assert_eq!(sanitize_tag("Node.js"), "node.js");
assert_eq!(sanitize_tag("C++"), "c++");
assert_eq!(sanitize_tag("React_Native"), "react_native");
}
#[test]
fn test_validate_page_size() {
assert!(matches!(
validate_page_size(10, "页面大小"),
ValidationResult::Valid
));
assert!(matches!(
validate_page_size(pagination_limits::MAX_PAGE_SIZE, "页面大小"),
ValidationResult::Valid
));
assert!(matches!(
validate_page_size(0, "页面大小"),
ValidationResult::Invalid(_)
));
assert!(matches!(
validate_page_size(pagination_limits::MAX_PAGE_SIZE + 1, "页面大小"),
ValidationResult::Invalid(_)
));
}
#[test]
fn test_validate_page_token() {
assert!(matches!(
validate_page_token("valid_token_123", "页面令牌"),
ValidationResult::Valid
));
assert!(matches!(
validate_page_token("", "页面令牌"),
ValidationResult::Valid
));
let long_token = "a".repeat(pagination_limits::MAX_PAGE_TOKEN_LENGTH + 1);
assert!(matches!(
validate_page_token(&long_token, "页面令牌"),
ValidationResult::Invalid(_)
));
}
#[test]
fn test_validate_pagination_params() {
assert!(matches!(
validate_pagination_params(Some(10), Some("token123"), "test"),
ValidationResult::Valid
));
assert!(matches!(
validate_pagination_params(Some(10), None, "test"),
ValidationResult::Valid
));
assert!(matches!(
validate_pagination_params(None, Some("token123"), "test"),
ValidationResult::Valid
));
assert!(matches!(
validate_pagination_params(None, None, "test"),
ValidationResult::Valid
));
assert!(matches!(
validate_pagination_params(Some(0), Some("token123"), "test"),
ValidationResult::Invalid(_)
));
assert!(matches!(
validate_pagination_params(Some(10), Some(""), "test"),
ValidationResult::Valid
));
}
#[test]
fn test_validate_custom_fields() {
use serde_json::Value;
use std::collections::HashMap;
assert!(matches!(
validate_custom_fields(&None, "自定义字段"),
ValidationResult::Valid
));
let mut valid_fields = HashMap::new();
valid_fields.insert(
"skill_level".to_string(),
Value::String("advanced".to_string()),
);
valid_fields.insert(
"years_exp".to_string(),
Value::Number(serde_json::Number::from(5)),
);
valid_fields.insert("is_certified".to_string(), Value::Bool(true));
valid_fields.insert(
"tags".to_string(),
Value::Array(vec![Value::String("rust".to_string())]),
);
valid_fields.insert("nullable_field".to_string(), Value::Null);
assert!(matches!(
validate_custom_fields(&Some(valid_fields), "自定义字段"),
ValidationResult::Valid
));
let mut too_many_fields = HashMap::new();
for i in 0..51 {
too_many_fields.insert(format!("field_{}", i), Value::String("value".to_string()));
}
assert!(matches!(
validate_custom_fields(&Some(too_many_fields), "自定义字段"),
ValidationResult::Invalid(_)
));
let mut empty_key_fields = HashMap::new();
empty_key_fields.insert("".to_string(), Value::String("value".to_string()));
assert!(matches!(
validate_custom_fields(&Some(empty_key_fields), "自定义字段"),
ValidationResult::Invalid(_)
));
let mut long_key_fields = HashMap::new();
let long_key = "a".repeat(employee_limits::CUSTOM_FIELD_KEY_MAX_LENGTH + 1);
long_key_fields.insert(long_key, Value::String("value".to_string()));
assert!(matches!(
validate_custom_fields(&Some(long_key_fields), "自定义字段"),
ValidationResult::Invalid(_)
));
let mut long_value_fields = HashMap::new();
let long_value = "a".repeat(employee_limits::CUSTOM_FIELD_VALUE_MAX_LENGTH + 1);
long_value_fields.insert("key".to_string(), Value::String(long_value));
assert!(matches!(
validate_custom_fields(&Some(long_value_fields), "自定义字段"),
ValidationResult::Invalid(_)
));
let mut large_array_fields = HashMap::new();
let large_array = (0..101)
.map(|i| Value::Number(serde_json::Number::from(i)))
.collect();
large_array_fields.insert("large_array".to_string(), Value::Array(large_array));
assert!(matches!(
validate_custom_fields(&Some(large_array_fields), "自定义字段"),
ValidationResult::Invalid(_)
));
let mut object_fields = HashMap::new();
let mut nested_object = HashMap::new();
nested_object.insert("nested".to_string(), Value::String("value".to_string()));
object_fields.insert(
"object_field".to_string(),
Value::Object(serde_json::Map::from_iter(nested_object)),
);
assert!(matches!(
validate_custom_fields(&Some(object_fields), "自定义字段"),
ValidationResult::Invalid(_)
));
}
#[test]
fn test_validate_resume_attachment_ids() {
let valid_ids = vec!["attachment_1".to_string(), "attachment_2".to_string()];
assert!(matches!(
validate_resume_attachment_ids(&valid_ids, "简历附件"),
ValidationResult::Valid
));
assert!(matches!(
validate_resume_attachment_ids(&[], "简历附件"),
ValidationResult::Valid
));
let too_many_ids: Vec<String> = (0..employee_limits::MAX_RESUME_ATTACHMENTS + 1)
.map(|i| format!("attachment_{}", i))
.collect();
assert!(matches!(
validate_resume_attachment_ids(&too_many_ids, "简历附件"),
ValidationResult::Invalid(_)
));
let empty_id_list = vec!["valid_id".to_string(), "".to_string()];
assert!(matches!(
validate_resume_attachment_ids(&empty_id_list, "简历附件"),
ValidationResult::Invalid(_)
));
let short_id_list = vec!["short".to_string()];
assert!(matches!(
validate_resume_attachment_ids(&short_id_list, "简历附件"),
ValidationResult::Invalid(_)
));
let long_id_list = vec!["a".repeat(101)];
assert!(matches!(
validate_resume_attachment_ids(&long_id_list, "简历附件"),
ValidationResult::Invalid(_)
));
}
#[test]
fn test_validate_talent_tag() {
assert!(matches!(
validate_talent_tag("rust", "技能标签"),
ValidationResult::Valid
));
assert!(matches!(
validate_talent_tag("Java_Spring", "技能标签"),
ValidationResult::Valid
));
assert!(matches!(
validate_talent_tag("前端开发", "技能标签"),
ValidationResult::Valid
));
assert!(matches!(
validate_talent_tag("Node_js", "技能标签"),
ValidationResult::Valid
));
assert!(matches!(
validate_talent_tag("", "技能标签"),
ValidationResult::Invalid(_)
));
let long_tag = "a".repeat(51);
assert!(matches!(
validate_talent_tag(&long_tag, "技能标签"),
ValidationResult::Invalid(_)
));
assert!(matches!(
validate_talent_tag("tag@domain", "技能标签"),
ValidationResult::Invalid(_)
));
assert!(matches!(
validate_talent_tag("tag*wildcard", "技能标签"),
ValidationResult::Invalid(_)
));
}
#[test]
fn test_validate_talent_tags() {
let valid_tags = vec![
"rust".to_string(),
"javascript".to_string(),
"前端开发".to_string(),
];
assert!(matches!(
validate_talent_tags(&valid_tags),
ValidationResult::Valid
));
assert!(matches!(validate_talent_tags(&[]), ValidationResult::Valid));
let too_many_tags: Vec<String> = (0..21).map(|i| format!("tag_{}", i)).collect();
assert!(matches!(
validate_talent_tags(&too_many_tags),
ValidationResult::Invalid(_)
));
let invalid_tags = vec!["valid_tag".to_string(), "invalid@tag".to_string()];
assert!(matches!(
validate_talent_tags(&invalid_tags),
ValidationResult::Invalid(_)
));
}
#[test]
fn test_validate_resume_attachment() {
assert!(matches!(
validate_resume_attachment("valid_attachment_123", "附件ID"),
ValidationResult::Valid
));
assert!(matches!(
validate_resume_attachment("attachment-with-hyphens", "附件ID"),
ValidationResult::Valid
));
assert!(matches!(
validate_resume_attachment("attachment_with_underscores", "附件ID"),
ValidationResult::Valid
));
assert!(matches!(
validate_resume_attachment("", "附件ID"),
ValidationResult::Invalid(_)
));
let long_id = "a".repeat(101);
assert!(matches!(
validate_resume_attachment(&long_id, "附件ID"),
ValidationResult::Invalid(_)
));
assert!(matches!(
validate_resume_attachment("attachment@domain", "附件ID"),
ValidationResult::Invalid(_)
));
assert!(matches!(
validate_resume_attachment("attachment with spaces", "附件ID"),
ValidationResult::Invalid(_)
));
assert!(matches!(
validate_resume_attachment("attachment/with/slashes", "附件ID"),
ValidationResult::Invalid(_)
));
}
#[test]
fn test_sanitize_name_edge_cases() {
assert_eq!(sanitize_name(" "), "");
assert_eq!(sanitize_name(" 张\t三 \n"), "张 三");
assert_eq!(sanitize_name("John Smith"), "John Smith");
assert_eq!(sanitize_name(" Mary Jane "), "Mary Jane");
assert_eq!(sanitize_name("Test\t\tName\n\n"), "Test Name");
assert_eq!(sanitize_name("A"), "A");
assert_eq!(sanitize_name(" 李 小 明 "), "李 小 明");
}
#[test]
fn test_sanitize_tags_edge_cases() {
let input_tags = vec!["java".to_string(), "JAVA".to_string(), "Java".to_string()];
let sanitized = sanitize_tags(&input_tags);
assert_eq!(sanitized, vec!["java"]);
let input_tags = vec![
"valid".to_string(),
"".to_string(),
" ".to_string(),
"another".to_string(),
];
let sanitized = sanitize_tags(&input_tags);
assert_eq!(sanitized, vec!["valid", "another"]);
let input_tags = vec![
"node-js".to_string(),
"react_native".to_string(),
"vue.js".to_string(),
];
let sanitized = sanitize_tags(&input_tags);
assert_eq!(sanitized, vec!["node_js", "react_native", "vue.js"]);
let sanitized = sanitize_tags(&[]);
assert!(sanitized.is_empty());
}
#[test]
fn test_sanitize_tag_individual() {
assert_eq!(sanitize_tag(" JavaScript "), "javascript");
assert_eq!(sanitize_tag("Node-JS"), "node_js");
assert_eq!(sanitize_tag("React_Native"), "react_native");
assert_eq!(sanitize_tag("Vue-Router_Plugin"), "vue_router_plugin");
assert_eq!(sanitize_tag(""), "");
assert_eq!(sanitize_tag(" "), "");
assert_eq!(sanitize_tag("lowercase"), "lowercase");
}
#[test]
fn test_validate_page_size_warnings() {
assert!(matches!(
validate_page_size(pagination_limits::MIN_PAGE_SIZE, "页面大小"),
ValidationResult::Valid
));
assert!(matches!(
validate_page_size(pagination_limits::MAX_PAGE_SIZE, "页面大小"),
ValidationResult::Valid
));
assert!(matches!(
validate_page_size(pagination_limits::RECOMMENDED_PAGE_SIZE, "页面大小"),
ValidationResult::Valid
));
assert!(matches!(
validate_page_size(pagination_limits::RECOMMENDED_PAGE_SIZE + 1, "页面大小"),
ValidationResult::Valid
));
}
#[test]
fn test_validate_page_token_formats() {
assert!(matches!(
validate_page_token("dGVzdA==", "页面令牌"),
ValidationResult::Valid
));
assert!(matches!(
validate_page_token("YWJjZGVmZw", "页面令牌"),
ValidationResult::Valid
));
assert!(matches!(
validate_page_token("abc-def_123", "页面令牌"),
ValidationResult::Valid
));
assert!(matches!(
validate_page_token("invalid@token", "页面令牌"),
ValidationResult::Invalid(_)
));
assert!(matches!(
validate_page_token("token with spaces", "页面令牌"),
ValidationResult::Invalid(_)
));
let long_token = "a".repeat(pagination_limits::MAX_PAGE_TOKEN_LENGTH + 1);
assert!(matches!(
validate_page_token(&long_token, "页面令牌"),
ValidationResult::Invalid(_)
));
}
#[test]
fn test_validation_result_error_method() {
let valid = ValidationResult::Valid;
assert_eq!(valid.error(), None);
let warning = ValidationResult::Warning("warning message".to_string());
assert_eq!(warning.error(), Some("warning message"));
let invalid = ValidationResult::Invalid("error message".to_string());
assert_eq!(invalid.error(), Some("error message"));
}
#[test]
fn test_is_chinese_char_comprehensive() {
assert!(is_chinese_char('中')); assert!(is_chinese_char('文')); assert!(is_chinese_char('测')); assert!(is_chinese_char('试'));
assert!(is_chinese_char('\u{3400}')); assert!(is_chinese_char('\u{4DBF}'));
assert!(is_chinese_char('。')); assert!(is_chinese_char('、'));
assert!(is_chinese_char('\u{2F00}'));
assert!(is_chinese_char('\u{F900}'));
assert!(!is_chinese_char('a'));
assert!(!is_chinese_char('A'));
assert!(!is_chinese_char('1'));
assert!(!is_chinese_char('!'));
assert!(!is_chinese_char(' '));
assert!(!is_chinese_char('\n'));
assert!(!is_chinese_char('α')); assert!(!is_chinese_char('й'));
assert!(!is_chinese_char('\u{4DFF}')); assert!(is_chinese_char('\u{4E00}')); assert!(is_chinese_char('\u{9FFF}')); assert!(!is_chinese_char('\u{A000}')); }
#[test]
fn test_validate_content_size_edge_cases() {
assert!(validate_content_size("", 100, "测试内容"));
let content = "a".repeat(100);
assert!(validate_content_size(&content, 100, "测试内容"));
let content = "a".repeat(101);
assert!(!validate_content_size(&content, 100, "测试内容"));
let chinese_content = "中文测试内容";
assert!(validate_content_size(chinese_content, 50, "中文内容"));
assert!(!validate_content_size(chinese_content, 10, "中文内容"));
}
#[test]
fn test_validate_string_length_utf8() {
let chinese_input = "中文测试".to_string(); let result = validate_string_length(chinese_input, 6, "中文字段");
assert_eq!(result.len(), 6);
let english_input = "hello world".to_string();
let result = validate_string_length(english_input, 5, "英文字段");
assert_eq!(result, "hello");
}
#[test]
fn test_validate_builder_trait() {
struct TestBuilder {
result: ValidationResult,
}
impl ValidateBuilder for TestBuilder {
fn validate(&self) -> ValidationResult {
self.result.clone()
}
}
let valid_builder = TestBuilder {
result: ValidationResult::Valid,
};
assert!(valid_builder.validate_and_log());
let warning_builder = TestBuilder {
result: ValidationResult::Warning("test warning".to_string()),
};
assert!(warning_builder.validate_and_log());
let invalid_builder = TestBuilder {
result: ValidationResult::Invalid("test error".to_string()),
};
assert!(!invalid_builder.validate_and_log());
}
}
pub mod employee_limits {
pub const NAME_MIN_LENGTH: usize = 2;
pub const NAME_MAX_LENGTH: usize = 100;
pub const EMAIL_MAX_LENGTH: usize = 254;
pub const PHONE_MAX_LENGTH: usize = 20;
pub const PHONE_MIN_LENGTH: usize = 7;
pub const WORK_EXPERIENCE_MIN: u32 = 0;
pub const WORK_EXPERIENCE_MAX: u32 = 50;
pub const MAX_RESUME_ATTACHMENTS: usize = 10;
pub const MAX_TALENT_TAGS: usize = 20;
pub const TAG_MAX_LENGTH: usize = 50;
pub const CUSTOM_FIELD_KEY_MAX_LENGTH: usize = 100;
pub const CUSTOM_FIELD_VALUE_MAX_LENGTH: usize = 1000;
pub const EXPECTED_SALARY_MAX_LENGTH: usize = 100;
}
pub fn validate_name(name: &str, field_name: &str) -> ValidationResult {
if name.is_empty() {
return ValidationResult::Invalid(format!("{} cannot be empty", field_name));
}
let char_count = name.chars().count();
if char_count < employee_limits::NAME_MIN_LENGTH {
return ValidationResult::Invalid(format!(
"{} must be at least {} characters long",
field_name,
employee_limits::NAME_MIN_LENGTH
));
}
if char_count > employee_limits::NAME_MAX_LENGTH {
return ValidationResult::Invalid(format!(
"{} must not exceed {} characters",
field_name,
employee_limits::NAME_MAX_LENGTH
));
}
if !name
.chars()
.all(|c| c.is_alphanumeric() || c.is_whitespace() || is_chinese_char(c) || "-.".contains(c))
{
return ValidationResult::Invalid(format!(
"{} contains invalid characters. Only letters, numbers, Chinese characters, spaces, hyphens and periods are allowed",
field_name
));
}
ValidationResult::Valid
}
pub fn validate_email(email: &str, field_name: &str) -> ValidationResult {
if email.is_empty() {
return ValidationResult::Invalid(format!("{} cannot be empty", field_name));
}
if email.len() > employee_limits::EMAIL_MAX_LENGTH {
return ValidationResult::Invalid(format!(
"{} must not exceed {} characters",
field_name,
employee_limits::EMAIL_MAX_LENGTH
));
}
let email_regex =
regex::Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap();
if !email_regex.is_match(email) {
return ValidationResult::Invalid(format!("{} must be a valid email address", field_name));
}
ValidationResult::Valid
}
pub fn validate_phone(phone: &str, field_name: &str) -> ValidationResult {
if phone.is_empty() {
return ValidationResult::Valid; }
if phone.len() < employee_limits::PHONE_MIN_LENGTH {
return ValidationResult::Invalid(format!(
"{} must be at least {} characters long (got {})",
field_name,
employee_limits::PHONE_MIN_LENGTH,
phone.len()
));
}
if phone.len() > employee_limits::PHONE_MAX_LENGTH {
return ValidationResult::Invalid(format!(
"{} must not exceed {} characters",
field_name,
employee_limits::PHONE_MAX_LENGTH
));
}
if !phone
.chars()
.all(|c| c.is_ascii_digit() || c == '+' || c == ' ' || c == '-')
{
return ValidationResult::Invalid(format!(
"{} contains invalid characters. Only numbers, +, spaces and hyphens are allowed",
field_name
));
}
ValidationResult::Valid
}
pub fn validate_work_experience(years: u32, field_name: &str) -> ValidationResult {
if years > employee_limits::WORK_EXPERIENCE_MAX {
return ValidationResult::Invalid(format!(
"{} must not exceed {} years",
field_name,
employee_limits::WORK_EXPERIENCE_MAX
));
}
ValidationResult::Valid
}
pub fn validate_birthday(birthday: &Option<String>, field_name: &str) -> ValidationResult {
if let Some(bday) = birthday {
if bday.is_empty() {
return ValidationResult::Valid;
}
let date_regex = regex::Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap();
if !date_regex.is_match(bday) {
return ValidationResult::Invalid(format!(
"{} must be in YYYY-MM-DD format",
field_name
));
}
if chrono::NaiveDate::parse_from_str(bday, "%Y-%m-%d").is_err() {
return ValidationResult::Invalid(format!(
"{} must be a valid date in YYYY-MM-DD format",
field_name
));
}
}
ValidationResult::Valid
}
pub fn validate_expected_salary(salary: &Option<String>, field_name: &str) -> ValidationResult {
if let Some(sal) = salary {
if sal.is_empty() {
return ValidationResult::Valid;
}
if sal.len() > employee_limits::EXPECTED_SALARY_MAX_LENGTH {
return ValidationResult::Invalid(format!(
"{} must not exceed {} characters",
field_name,
employee_limits::EXPECTED_SALARY_MAX_LENGTH
));
}
let salary_regex = regex::Regex::new(r"^(\d+(-\d+)?[Kk万]?([+-])?|面议)$").unwrap();
if !salary_regex.is_match(sal) {
return ValidationResult::Invalid(format!(
"{} must be a valid salary format (e.g., 10-20K, 50-80万, 100万+)",
field_name
));
}
if let Some(captures) = salary_regex.captures(sal) {
if let Some(num_str) = captures.get(1) {
let num_part = num_str.as_str();
let num_regex = regex::Regex::new(r"^(\d+)").unwrap();
if let Some(num_captures) = num_regex.captures(num_part) {
if let Some(num_match) = num_captures.get(1) {
if let Ok(num) = num_match.as_str().parse::<u32>() {
let is_k = sal.contains('K') || sal.contains('k');
let is_wan = sal.contains('万');
let actual_num = if is_k {
num
} else if is_wan {
num * 10 } else {
num / 1000 };
if actual_num > 500 {
return ValidationResult::Invalid(format!(
"{}: salary range is unreasonably high",
field_name
));
}
}
}
}
}
}
}
ValidationResult::Valid
}
pub fn validate_tags(tags: &[String], field_name: &str) -> ValidationResult {
if tags.len() > employee_limits::MAX_TALENT_TAGS {
return ValidationResult::Invalid(format!(
"{} cannot have more than {} tags",
field_name,
employee_limits::MAX_TALENT_TAGS
));
}
for (i, tag) in tags.iter().enumerate() {
if tag.is_empty() {
return ValidationResult::Invalid(format!(
"{} tag at index {} cannot be empty",
field_name, i
));
}
if tag.len() > employee_limits::TAG_MAX_LENGTH {
return ValidationResult::Invalid(format!(
"{} tag '{}' exceeds maximum length of {} characters",
field_name,
tag,
employee_limits::TAG_MAX_LENGTH
));
}
if !tag
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '-' || is_chinese_char(c))
{
return ValidationResult::Invalid(format!(
"{} tag '{}' contains invalid characters. Only letters, numbers, Chinese characters, underscores and hyphens are allowed",
field_name,
tag
));
}
}
ValidationResult::Valid
}
pub fn validate_custom_fields(
fields: &Option<std::collections::HashMap<String, serde_json::Value>>,
field_name: &str,
) -> ValidationResult {
if let Some(custom_fields) = fields {
if custom_fields.len() > 50 {
return ValidationResult::Invalid(format!(
"{} cannot have more than 50 custom fields",
field_name
));
}
for (key, value) in custom_fields {
if key.is_empty() {
return ValidationResult::Invalid(format!(
"{} custom field key cannot be empty",
field_name
));
}
if key.len() > employee_limits::CUSTOM_FIELD_KEY_MAX_LENGTH {
return ValidationResult::Invalid(format!(
"{} custom field key '{}' exceeds maximum length of {} characters",
field_name,
key,
employee_limits::CUSTOM_FIELD_KEY_MAX_LENGTH
));
}
match value {
serde_json::Value::String(s) => {
if s.len() > employee_limits::CUSTOM_FIELD_VALUE_MAX_LENGTH {
return ValidationResult::Invalid(format!(
"{} custom field value for key '{}' exceeds maximum length of {} characters",
field_name,
key,
employee_limits::CUSTOM_FIELD_VALUE_MAX_LENGTH
));
}
}
serde_json::Value::Number(n) => {
if !n.is_i64() && !n.is_u64() && !n.is_f64() {
return ValidationResult::Invalid(format!(
"{} custom field value for key '{}' is not a valid number",
field_name, key
));
}
}
serde_json::Value::Bool(_) => {
}
serde_json::Value::Array(arr) => {
if arr.len() > 100 {
return ValidationResult::Invalid(format!(
"{} custom field array for key '{}' cannot have more than 100 items",
field_name, key
));
}
}
serde_json::Value::Object(_) => {
return ValidationResult::Invalid(format!(
"{} custom field value for key '{}' cannot be an object",
field_name, key
));
}
serde_json::Value::Null => {
}
}
}
}
ValidationResult::Valid
}
pub fn validate_resume_attachment_ids(
attachment_ids: &[String],
field_name: &str,
) -> ValidationResult {
if attachment_ids.len() > employee_limits::MAX_RESUME_ATTACHMENTS {
return ValidationResult::Invalid(format!(
"{} cannot have more than {} resume attachments",
field_name,
employee_limits::MAX_RESUME_ATTACHMENTS
));
}
for (i, id) in attachment_ids.iter().enumerate() {
if id.is_empty() {
return ValidationResult::Invalid(format!(
"{} attachment ID at index {} cannot be empty",
field_name, i
));
}
if id.len() < 10 || id.len() > 100 {
return ValidationResult::Invalid(format!(
"{} attachment ID at index {} has invalid length",
field_name, i
));
}
}
ValidationResult::Valid
}
pub fn sanitize_name(name: &str) -> String {
let trimmed = name.trim();
let normalized = trimmed.chars().collect::<Vec<_>>();
let mut result = Vec::new();
let mut prev_was_space = false;
for c in normalized {
if c.is_whitespace() {
if !prev_was_space {
result.push(' ');
prev_was_space = true;
}
} else {
result.push(c);
prev_was_space = false;
}
}
result.into_iter().collect()
}
pub fn sanitize_tags(tags: &[String]) -> Vec<String> {
let mut result = Vec::new();
for tag in tags {
let sanitized = tag
.trim()
.replace(['_', '-'], "_") .to_lowercase();
if !sanitized.is_empty() && !result.contains(&sanitized) {
result.push(sanitized);
}
}
result
}
pub fn validate_talent_tag(tag: &str, field_name: &str) -> ValidationResult {
if tag.is_empty() {
return ValidationResult::Invalid(format!("{}: tag cannot be empty", field_name));
}
if tag.len() > 50 {
return ValidationResult::Invalid(format!(
"{}: tag must not exceed 50 characters (got {})",
field_name,
tag.len()
));
}
for c in tag.chars() {
if !(c.is_alphanumeric() || c == '_' || c == '-' || c == ' ' || is_chinese_char(c)) {
return ValidationResult::Invalid(format!(
"{}: tag contains invalid character '{}'. Only letters, numbers, spaces, hyphens, underscores and Chinese characters are allowed",
field_name, c
));
}
}
ValidationResult::Valid
}
pub fn validate_talent_tags(tags: &[String]) -> ValidationResult {
if tags.len() > 20 {
return ValidationResult::Invalid(format!(
"Invalid tags: maximum number of tags is 20 (got {})",
tags.len()
));
}
for (index, tag) in tags.iter().enumerate() {
match validate_talent_tag(tag, &format!("tags[{}]", index)) {
ValidationResult::Valid => {}
ValidationResult::Warning(msg) => {
return ValidationResult::Warning(msg);
}
ValidationResult::Invalid(msg) => {
return ValidationResult::Invalid(msg);
}
}
}
ValidationResult::Valid
}
pub fn validate_resume_attachment(attachment_id: &str, field_name: &str) -> ValidationResult {
if attachment_id.is_empty() {
return ValidationResult::Invalid(format!("{}: attachment ID cannot be empty", field_name));
}
if attachment_id.len() > 100 {
return ValidationResult::Invalid(format!(
"{}: attachment ID must not exceed 100 characters (got {})",
field_name,
attachment_id.len()
));
}
if !attachment_id
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
{
return ValidationResult::Invalid(format!(
"{}: attachment ID can only contain letters, numbers, hyphens and underscores",
field_name
));
}
ValidationResult::Valid
}
pub mod pagination_limits {
pub const DEFAULT_PAGE_SIZE: u32 = 20;
pub const MIN_PAGE_SIZE: u32 = 1;
pub const MAX_PAGE_SIZE: u32 = 500;
pub const RECOMMENDED_PAGE_SIZE: u32 = 50;
pub const MAX_PAGE_TOKEN_LENGTH: usize = 1024;
}
pub fn validate_page_size(page_size: u32, field_name: &str) -> ValidationResult {
if page_size < pagination_limits::MIN_PAGE_SIZE {
return ValidationResult::Invalid(format!(
"{}: page size must be at least {}",
field_name,
pagination_limits::MIN_PAGE_SIZE
));
}
if page_size > pagination_limits::MAX_PAGE_SIZE {
return ValidationResult::Invalid(format!(
"{}: page size must not exceed {} (recommended: {})",
field_name,
pagination_limits::MAX_PAGE_SIZE,
pagination_limits::RECOMMENDED_PAGE_SIZE
));
}
if page_size > pagination_limits::RECOMMENDED_PAGE_SIZE {
log::warn!(
"{}: page size {} is larger than recommended value {}. This may impact performance.",
field_name,
page_size,
pagination_limits::RECOMMENDED_PAGE_SIZE
);
}
ValidationResult::Valid
}
pub fn validate_page_token(page_token: &str, field_name: &str) -> ValidationResult {
if page_token.is_empty() {
return ValidationResult::Valid;
}
if page_token.len() > pagination_limits::MAX_PAGE_TOKEN_LENGTH {
return ValidationResult::Invalid(format!(
"{}: page token must not exceed {} characters",
field_name,
pagination_limits::MAX_PAGE_TOKEN_LENGTH
));
}
if !page_token.chars().all(|c| {
c.is_ascii_alphanumeric() || c == '/' || c == '+' || c == '=' || c == '-' || c == '_'
}) {
return ValidationResult::Invalid(format!(
"{}: page token contains invalid characters. Expected base64 format",
field_name
));
}
ValidationResult::Valid
}
pub fn validate_pagination_params(
page_size: Option<u32>,
page_token: Option<&str>,
field_prefix: &str,
) -> ValidationResult {
if let Some(size) = page_size {
match validate_page_size(size, &format!("{}_page_size", field_prefix)) {
ValidationResult::Valid => {}
ValidationResult::Warning(msg) => {
log::warn!("Pagination warning: {}", msg);
}
ValidationResult::Invalid(msg) => {
return ValidationResult::Invalid(msg);
}
}
}
if let Some(token) = page_token {
match validate_page_token(token, &format!("{}_page_token", field_prefix)) {
ValidationResult::Valid => {}
ValidationResult::Warning(msg) => {
log::warn!("Pagination warning: {}", msg);
}
ValidationResult::Invalid(msg) => {
return ValidationResult::Invalid(msg);
}
}
}
if page_token.is_some() && page_size.is_none() {
log::warn!(
"{}: page_token provided without page_size. Using default page size {}",
field_prefix,
pagination_limits::DEFAULT_PAGE_SIZE
);
}
ValidationResult::Valid
}
pub fn sanitize_tag(tag: &str) -> String {
tag.trim()
.replace(['_', '-'], "_") .to_lowercase()
}
pub mod sheets;
pub mod im;
pub mod hire;
pub mod calendar;
pub mod drive;
pub mod pagination;
pub use pagination::{PaginatedResponse, PaginationIterator, PaginationRequestBuilder};
pub use sheets::{
validate_cell_range, validate_data_matrix_consistency, validate_date_time_render_option,
validate_find_options, validate_merge_range, validate_value_render_option,
};
pub use im::{
validate_file_upload, validate_message_content, validate_message_forward,
validate_message_reaction, validate_message_read_status, validate_message_recall,
validate_message_receivers, validate_message_template, validate_message_type,
validate_receiver_id, validate_uuid, ValidateImBuilder,
};
pub use hire::{
validate_birthday as validate_hire_birthday, validate_candidate_basic_info,
validate_candidate_tags, validate_education_background, validate_hiring_requirement,
validate_hiring_status_transition, validate_interview_arrangement, validate_interview_feedback,
validate_job_position, validate_offer_info, validate_salary_range,
validate_work_experience as validate_hire_work_experience, ValidateHireBuilder,
};