1use log::error;
5
6fn is_chinese_char(c: char) -> bool {
16 let ranges = [
18 (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), ];
32
33 let code = c as u32;
34 ranges
35 .iter()
36 .any(|&(start, end)| code >= start && code <= end)
37}
38
39pub fn validate_string_length(input: String, max_len: usize, field_name: &str) -> String {
49 if input.len() > max_len {
50 error!(
51 "{} exceeds maximum length of {} characters: {}",
52 field_name,
53 max_len,
54 input.len()
55 );
56 input[..max_len].to_string()
57 } else {
58 input
59 }
60}
61
62pub fn validate_required(value: &str, field_name: &str) -> bool {
71 if value.is_empty() {
72 error!("{} is required but empty", field_name);
73 false
74 } else {
75 true
76 }
77}
78
79pub fn validate_content_size(content: &str, max_size: usize, content_type: &str) -> bool {
89 if content.len() > max_size {
90 error!(
91 "{} content exceeds maximum size of {} bytes: {}",
92 content_type,
93 max_size,
94 content.len()
95 );
96 false
97 } else {
98 true
99 }
100}
101
102#[derive(Debug, Clone, PartialEq)]
104pub enum ValidationResult {
105 Valid,
107 Warning(String),
109 Invalid(String),
111}
112
113impl ValidationResult {
114 pub fn is_valid(&self) -> bool {
116 matches!(self, ValidationResult::Valid | ValidationResult::Warning(_))
117 }
118
119 pub fn is_strictly_valid(&self) -> bool {
121 matches!(self, ValidationResult::Valid)
122 }
123
124 pub fn error(&self) -> Option<&str> {
126 match self {
127 ValidationResult::Invalid(msg) | ValidationResult::Warning(msg) => Some(msg),
128 ValidationResult::Valid => None,
129 }
130 }
131}
132
133pub trait ValidateBuilder {
137 fn validate(&self) -> ValidationResult;
142
143 fn validate_and_log(&self) -> bool {
150 match self.validate() {
151 ValidationResult::Valid => true,
152 ValidationResult::Warning(msg) => {
153 error!("Builder validation warning: {}", msg);
154 true
155 }
156 ValidationResult::Invalid(msg) => {
157 error!("Builder validation failed: {}", msg);
158 false
159 }
160 }
161 }
162}
163
164pub mod message_limits {
166 pub const TEXT_MESSAGE_MAX_SIZE: usize = 150 * 1024; pub const RICH_MESSAGE_MAX_SIZE: usize = 30 * 1024; }
171
172pub mod uuid_limits {
174 pub const MAX_LENGTH: usize = 50;
176}
177
178pub mod password_limits {
180 pub const MIN_LENGTH: usize = 8;
182 pub const MAX_LENGTH: usize = 128;
184 pub const REQUIRE_UPPERCASE: bool = true;
186 pub const REQUIRE_LOWERCASE: bool = true;
187 pub const REQUIRE_DIGIT: bool = true;
188 pub const REQUIRE_SPECIAL: bool = true;
189}
190
191pub mod file_limits {
193 pub const MAX_FILE_SIZE: usize = 100 * 1024 * 1024;
195 pub const IM_MAX_FILE_SIZE: usize = 50 * 1024 * 1024;
197 pub const MAX_IMAGE_SIZE: usize = 20 * 1024 * 1024;
199 pub const MAX_FILENAME_LENGTH: usize = 255;
201 pub const MAX_EXTENSION_LENGTH: usize = 10;
203 pub const ALLOWED_FILE_TYPES: &[&str] = &[
205 "pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "csv", "zip", "rar", "7z",
206 "tar", "gz", "jpg", "jpeg", "png", "gif", "bmp", "svg", "mp4", "avi", "mov", "wmv", "flv",
207 "mkv", "mp3", "wav", "flac", "aac", "ogg", "json", "xml", "html", "css", "js", "py", "rs",
208 "go",
209 ];
210 pub const ALLOWED_IMAGE_TYPES: &[&str] = &["jpg", "jpeg", "png", "gif", "bmp", "svg", "webp"];
212}
213
214pub fn validate_password_strength(password: &str) -> ValidationResult {
222 if password.len() < password_limits::MIN_LENGTH {
224 return ValidationResult::Invalid(format!(
225 "Password must be at least {} characters long",
226 password_limits::MIN_LENGTH
227 ));
228 }
229
230 if password.len() > password_limits::MAX_LENGTH {
231 return ValidationResult::Invalid(format!(
232 "Password must not exceed {} characters",
233 password_limits::MAX_LENGTH
234 ));
235 }
236
237 let mut has_uppercase = false;
238 let mut has_lowercase = false;
239 let mut has_digit = false;
240 let mut has_special = false;
241
242 for ch in password.chars() {
243 if ch.is_uppercase() {
244 has_uppercase = true;
245 } else if ch.is_lowercase() {
246 has_lowercase = true;
247 } else if ch.is_ascii_digit() {
248 has_digit = true;
249 } else if ch.is_ascii_punctuation() || ch.is_ascii_whitespace() {
250 has_special = true;
251 }
252 }
253
254 let mut missing_requirements = Vec::new();
255
256 if password_limits::REQUIRE_UPPERCASE && !has_uppercase {
257 missing_requirements.push("uppercase letter");
258 }
259
260 if password_limits::REQUIRE_LOWERCASE && !has_lowercase {
261 missing_requirements.push("lowercase letter");
262 }
263
264 if password_limits::REQUIRE_DIGIT && !has_digit {
265 missing_requirements.push("digit");
266 }
267
268 if password_limits::REQUIRE_SPECIAL && !has_special {
269 missing_requirements.push("special character");
270 }
271
272 if !missing_requirements.is_empty() {
273 return ValidationResult::Invalid(format!(
274 "Password is missing required character types: {}",
275 missing_requirements.join(", ")
276 ));
277 }
278
279 ValidationResult::Valid
280}
281
282pub fn validate_and_sanitize_password(
291 password: String,
292 field_name: &str,
293) -> (String, ValidationResult) {
294 let password = password.trim().to_string();
296
297 let result = validate_password_strength(&password);
299
300 if let ValidationResult::Invalid(msg) = &result {
301 error!("{} validation failed: {}", field_name, msg);
302 }
303
304 (password, result)
305}
306
307pub fn validate_file_size(file_size: usize, max_size: usize, file_name: &str) -> ValidationResult {
317 if file_size == 0 {
318 return ValidationResult::Invalid("File size cannot be zero".to_string());
319 }
320
321 if file_size > max_size {
322 return ValidationResult::Invalid(format!(
323 "File '{}' exceeds maximum size of {} bytes (actual: {} bytes)",
324 file_name, max_size, file_size
325 ));
326 }
327
328 ValidationResult::Valid
329}
330
331pub fn validate_file_name(file_name: &str) -> (String, ValidationResult) {
339 let cleaned_name = file_name.trim();
340
341 if cleaned_name.is_empty() {
343 return (
344 String::new(),
345 ValidationResult::Invalid("File name cannot be empty".to_string()),
346 );
347 }
348
349 if cleaned_name.len() > file_limits::MAX_FILENAME_LENGTH {
351 return (
352 String::new(),
353 ValidationResult::Invalid(format!(
354 "File name exceeds maximum length of {} characters",
355 file_limits::MAX_FILENAME_LENGTH
356 )),
357 );
358 }
359
360 let invalid_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|'];
362 for ch in cleaned_name.chars() {
363 if invalid_chars.contains(&ch) {
364 return (
365 String::new(),
366 ValidationResult::Invalid(format!(
367 "File name contains invalid character: '{}'",
368 ch
369 )),
370 );
371 }
372 }
373
374 let reserved_names = [
376 "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8",
377 "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
378 ];
379
380 let upper_name = cleaned_name.to_uppercase();
381 let name_without_ext = if let Some(dot_pos) = upper_name.find('.') {
382 &upper_name[..dot_pos]
383 } else {
384 &upper_name
385 };
386
387 if reserved_names.contains(&name_without_ext) {
388 return (
389 String::new(),
390 ValidationResult::Invalid(format!("'{}' is a reserved file name", cleaned_name)),
391 );
392 }
393
394 (cleaned_name.to_string(), ValidationResult::Valid)
395}
396
397pub fn validate_file_extension(
406 file_name: &str,
407 allowed_types: &[&str],
408) -> (Option<String>, ValidationResult) {
409 let (_, validation_result) = validate_file_name(file_name);
410 if !validation_result.is_valid() {
411 return (None, validation_result);
412 }
413
414 let extension = if let Some(dot_pos) = file_name.rfind('.') {
416 let ext = &file_name[dot_pos + 1..];
417 if ext.is_empty() {
418 return (
419 None,
420 ValidationResult::Invalid("File extension cannot be empty".to_string()),
421 );
422 }
423
424 if ext.len() > file_limits::MAX_EXTENSION_LENGTH {
426 return (
427 None,
428 ValidationResult::Invalid(format!(
429 "File extension exceeds maximum length of {} characters",
430 file_limits::MAX_EXTENSION_LENGTH
431 )),
432 );
433 }
434
435 Some(ext.to_lowercase())
436 } else {
437 return (
438 None,
439 ValidationResult::Invalid("File must have an extension".to_string()),
440 );
441 };
442
443 if let Some(ref ext) = extension {
445 if !allowed_types.contains(&ext.as_str()) {
446 return (
447 None,
448 ValidationResult::Invalid(format!(
449 "File extension '.{}' is not allowed. Allowed types: {}",
450 ext,
451 allowed_types.join(", ")
452 )),
453 );
454 }
455 }
456
457 (extension, ValidationResult::Valid)
458}
459
460pub fn validate_image_file(file_data: &[u8], file_name: &str) -> ValidationResult {
469 let size_result = validate_file_size(file_data.len(), file_limits::MAX_IMAGE_SIZE, file_name);
471 if !size_result.is_valid() {
472 return size_result;
473 }
474
475 let (_, ext_result) = validate_file_extension(file_name, file_limits::ALLOWED_IMAGE_TYPES);
477 if !ext_result.is_valid() {
478 return ext_result;
479 }
480
481 ValidationResult::Valid
486}
487
488pub fn validate_upload_file(
498 file_data: &[u8],
499 file_name: &str,
500 is_im_upload: bool,
501) -> ValidationResult {
502 let max_size = if is_im_upload {
504 file_limits::IM_MAX_FILE_SIZE
505 } else {
506 file_limits::MAX_FILE_SIZE
507 };
508
509 let size_result = validate_file_size(file_data.len(), max_size, file_name);
510 if !size_result.is_valid() {
511 return size_result;
512 }
513
514 let (_, name_result) = validate_file_name(file_name);
516 if !name_result.is_valid() {
517 return name_result;
518 }
519
520 let (_, ext_result) = validate_file_extension(file_name, file_limits::ALLOWED_FILE_TYPES);
522 if !ext_result.is_valid() {
523 return ext_result;
524 }
525
526 ValidationResult::Valid
527}
528
529#[cfg(test)]
530mod tests {
531 use super::*;
532
533 #[test]
534 fn test_validate_string_length() {
535 let input = "hello".to_string();
537 let result = validate_string_length(input, 10, "test_field");
538 assert_eq!(result, "hello");
539
540 let input = "hello world".to_string();
542 let result = validate_string_length(input, 5, "test_field");
543 assert_eq!(result, "hello");
544 }
545
546 #[test]
547 fn test_validate_required() {
548 assert!(validate_required("hello", "test_field"));
550
551 assert!(!validate_required("", "test_field"));
553 }
554
555 #[test]
556 fn test_validate_content_size() {
557 assert!(validate_content_size("hello", 10, "test_content"));
559
560 assert!(!validate_content_size("hello world", 5, "test_content"));
562 }
563
564 #[test]
565 fn test_validation_result() {
566 let valid = ValidationResult::Valid;
567 assert!(valid.is_valid());
568 assert!(valid.is_strictly_valid());
569 assert!(valid.error().is_none());
570
571 let warning = ValidationResult::Warning("test warning".to_string());
572 assert!(warning.is_valid());
573 assert!(!warning.is_strictly_valid());
574 assert_eq!(warning.error(), Some("test warning"));
575
576 let invalid = ValidationResult::Invalid("test error".to_string());
577 assert!(!invalid.is_valid());
578 assert!(!invalid.is_strictly_valid());
579 assert_eq!(invalid.error(), Some("test error"));
580 }
581
582 #[test]
583 fn test_validate_password_strength() {
584 let valid_password = "SecurePass123!";
586 assert!(matches!(
587 validate_password_strength(valid_password),
588 ValidationResult::Valid
589 ));
590
591 let short_password = "Short1!";
593 assert!(matches!(
594 validate_password_strength(short_password),
595 ValidationResult::Invalid(_)
596 ));
597
598 let long_password = "a".repeat(password_limits::MAX_LENGTH + 1);
600 assert!(matches!(
601 validate_password_strength(&long_password),
602 ValidationResult::Invalid(_)
603 ));
604
605 let no_upper = "lowercase123!";
607 assert!(matches!(
608 validate_password_strength(no_upper),
609 ValidationResult::Invalid(_)
610 ));
611
612 let no_lower = "UPPERCASE123!";
614 assert!(matches!(
615 validate_password_strength(no_lower),
616 ValidationResult::Invalid(_)
617 ));
618
619 let no_digit = "NoDigitsHere!";
621 assert!(matches!(
622 validate_password_strength(no_digit),
623 ValidationResult::Invalid(_)
624 ));
625
626 let no_special = "NoSpecialChars123";
628 assert!(matches!(
629 validate_password_strength(no_special),
630 ValidationResult::Invalid(_)
631 ));
632 }
633
634 #[test]
635 fn test_validate_and_sanitize_password() {
636 let (password, result) =
638 validate_and_sanitize_password(" GoodPass123! ".to_string(), "test_password");
639 assert_eq!(password, "GoodPass123!");
640 assert!(matches!(result, ValidationResult::Valid));
641
642 let (password, result) =
644 validate_and_sanitize_password(" weak ".to_string(), "test_password");
645 assert_eq!(password, "weak");
646 assert!(matches!(result, ValidationResult::Invalid(_)));
647 }
648
649 #[test]
650 fn test_validate_file_size() {
651 assert!(matches!(
653 validate_file_size(1024, 2048, "test.txt"),
654 ValidationResult::Valid
655 ));
656
657 assert!(matches!(
659 validate_file_size(0, 2048, "test.txt"),
660 ValidationResult::Invalid(_)
661 ));
662
663 assert!(matches!(
665 validate_file_size(3000, 2048, "test.txt"),
666 ValidationResult::Invalid(_)
667 ));
668 }
669
670 #[test]
671 fn test_validate_file_name() {
672 let (name, result) = validate_file_name("document.pdf");
674 assert_eq!(name, "document.pdf");
675 assert!(matches!(result, ValidationResult::Valid));
676
677 let (name, result) = validate_file_name(" document.pdf ");
679 assert_eq!(name, "document.pdf");
680 assert!(matches!(result, ValidationResult::Valid));
681
682 let (name, result) = validate_file_name("");
684 assert_eq!(name, "");
685 assert!(matches!(result, ValidationResult::Invalid(_)));
686
687 let long_name = "a".repeat(file_limits::MAX_FILENAME_LENGTH + 1);
689 let (name, result) = validate_file_name(&long_name);
690 assert_eq!(name, "");
691 assert!(matches!(result, ValidationResult::Invalid(_)));
692
693 let invalid_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|'];
695 for ch in invalid_chars {
696 let (name, result) = validate_file_name(&format!("test{}.txt", ch));
697 assert_eq!(name, "");
698 assert!(matches!(result, ValidationResult::Invalid(_)));
699 }
700
701 let reserved_names = ["CON", "PRN", "AUX", "NUL", "COM1", "LPT1"];
703 for name in reserved_names {
704 let (cleaned, result) = validate_file_name(name);
705 assert_eq!(cleaned, "");
706 assert!(matches!(result, ValidationResult::Invalid(_)));
707 }
708 }
709
710 #[test]
711 fn test_validate_file_extension() {
712 let (ext, result) =
714 validate_file_extension("document.pdf", file_limits::ALLOWED_FILE_TYPES);
715 assert_eq!(ext, Some("pdf".to_string()));
716 assert!(matches!(result, ValidationResult::Valid));
717
718 let (ext, result) = validate_file_extension("document", file_limits::ALLOWED_FILE_TYPES);
720 assert_eq!(ext, None);
721 assert!(matches!(result, ValidationResult::Invalid(_)));
722
723 let (ext, result) = validate_file_extension("document.", file_limits::ALLOWED_FILE_TYPES);
725 assert_eq!(ext, None);
726 assert!(matches!(result, ValidationResult::Invalid(_)));
727
728 let (ext, result) =
730 validate_file_extension("document.exe", file_limits::ALLOWED_FILE_TYPES);
731 assert_eq!(ext, None);
732 assert!(matches!(result, ValidationResult::Invalid(_)));
733
734 let (ext, result) =
736 validate_file_extension("document.PDF", file_limits::ALLOWED_FILE_TYPES);
737 assert_eq!(ext, Some("pdf".to_string()));
738 assert!(matches!(result, ValidationResult::Valid));
739 }
740
741 #[test]
742 fn test_validate_image_file() {
743 let image_data = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; assert!(matches!(
746 validate_image_file(&image_data, "image.png"),
747 ValidationResult::Valid
748 ));
749
750 let large_image_data = vec![0u8; file_limits::MAX_IMAGE_SIZE + 1];
752 assert!(matches!(
753 validate_image_file(&large_image_data, "large.png"),
754 ValidationResult::Invalid(_)
755 ));
756
757 let image_data = vec![0x89, 0x50, 0x4E, 0x47];
759 assert!(matches!(
760 validate_image_file(&image_data, "image.tiff"),
761 ValidationResult::Invalid(_)
762 ));
763 }
764
765 #[test]
766 fn test_validate_upload_file() {
767 let file_data = vec![0u8; 1024];
769 assert!(matches!(
770 validate_upload_file(&file_data, "document.pdf", false),
771 ValidationResult::Valid
772 ));
773
774 let im_file_data = vec![0u8; file_limits::IM_MAX_FILE_SIZE + 1];
776 assert!(matches!(
777 validate_upload_file(&im_file_data, "large.pdf", true),
778 ValidationResult::Invalid(_)
779 ));
780
781 let normal_file_data = vec![0u8; file_limits::MAX_FILE_SIZE + 1];
783 assert!(matches!(
784 validate_upload_file(&normal_file_data, "large.pdf", false),
785 ValidationResult::Invalid(_)
786 ));
787 }
788
789 #[test]
790 fn test_is_chinese_char() {
791 assert!(is_chinese_char('中'));
793 assert!(is_chinese_char('文'));
794 assert!(is_chinese_char('字'));
795 assert!(is_chinese_char('符'));
796
797 assert!(is_chinese_char('。')); assert!(!is_chinese_char(',')); assert!(!is_chinese_char('a'));
806 assert!(!is_chinese_char('1'));
807 assert!(!is_chinese_char('!'));
808 assert!(!is_chinese_char(' '));
809 }
810
811 #[test]
812 fn test_validate_name() {
813 assert!(matches!(
815 validate_name("张三", "姓名"),
816 ValidationResult::Valid
817 ));
818
819 assert!(matches!(
820 validate_name("John Smith", "姓名"),
821 ValidationResult::Valid
822 ));
823
824 assert!(matches!(
826 validate_name("", "姓名"),
827 ValidationResult::Invalid(_)
828 ));
829
830 assert!(matches!(
832 validate_name("A", "姓名"),
833 ValidationResult::Invalid(_)
834 ));
835
836 let long_name = "A".repeat(employee_limits::NAME_MAX_LENGTH + 1);
838 assert!(matches!(
839 validate_name(&long_name, "姓名"),
840 ValidationResult::Invalid(_)
841 ));
842 }
843
844 #[test]
845 fn test_validate_email() {
846 assert!(matches!(
848 validate_email("test@example.com", "邮箱"),
849 ValidationResult::Valid
850 ));
851
852 assert!(matches!(
853 validate_email("user.name+tag@domain.co.uk", "邮箱"),
854 ValidationResult::Valid
855 ));
856
857 assert!(matches!(
859 validate_email("invalid-email", "邮箱"),
860 ValidationResult::Invalid(_)
861 ));
862
863 assert!(matches!(
864 validate_email("@example.com", "邮箱"),
865 ValidationResult::Invalid(_)
866 ));
867
868 assert!(matches!(
869 validate_email("test@", "邮箱"),
870 ValidationResult::Invalid(_)
871 ));
872
873 assert!(matches!(
875 validate_email("", "邮箱"),
876 ValidationResult::Invalid(_)
877 ));
878
879 let long_email = format!(
881 "{}@example.com",
882 "a".repeat(employee_limits::EMAIL_MAX_LENGTH)
883 );
884 assert!(matches!(
885 validate_email(&long_email, "邮箱"),
886 ValidationResult::Invalid(_)
887 ));
888 }
889
890 #[test]
891 fn test_validate_phone() {
892 assert!(matches!(
894 validate_phone("13812345678", "电话"),
895 ValidationResult::Valid
896 ));
897
898 assert!(matches!(
899 validate_phone("+86-138-1234-5678", "电话"),
900 ValidationResult::Valid
901 ));
902
903 assert!(matches!(
904 validate_phone("021-12345678", "电话"),
905 ValidationResult::Valid
906 ));
907
908 assert!(matches!(
910 validate_phone("123", "电话"),
911 ValidationResult::Invalid(_)
912 ));
913
914 assert!(matches!(
915 validate_phone("abc123def", "电话"),
916 ValidationResult::Invalid(_)
917 ));
918
919 assert!(matches!(
921 validate_phone("", "电话"),
922 ValidationResult::Valid
923 ));
924
925 let long_phone = "1".repeat(employee_limits::PHONE_MAX_LENGTH + 1);
927 assert!(matches!(
928 validate_phone(&long_phone, "电话"),
929 ValidationResult::Invalid(_)
930 ));
931 }
932
933 #[test]
934 fn test_validate_work_experience() {
935 assert!(matches!(
937 validate_work_experience(5, "工作年限"),
938 ValidationResult::Valid
939 ));
940
941 assert!(matches!(
942 validate_work_experience(0, "工作年限"),
943 ValidationResult::Valid
944 ));
945
946 assert!(matches!(
947 validate_work_experience(employee_limits::WORK_EXPERIENCE_MAX, "工作年限"),
948 ValidationResult::Valid
949 ));
950
951 assert!(matches!(
953 validate_work_experience(employee_limits::WORK_EXPERIENCE_MAX + 1, "工作年限"),
954 ValidationResult::Invalid(_)
955 ));
956 }
957
958 #[test]
959 fn test_validate_birthday() {
960 assert!(matches!(
962 validate_birthday(&Some("1990-01-01".to_string()), "生日"),
963 ValidationResult::Valid
964 ));
965
966 assert!(matches!(
967 validate_birthday(&Some("2000-12-31".to_string()), "生日"),
968 ValidationResult::Valid
969 ));
970
971 assert!(matches!(
973 validate_birthday(&None, "生日"),
974 ValidationResult::Valid
975 ));
976
977 assert!(matches!(
979 validate_birthday(&Some("invalid-date".to_string()), "生日"),
980 ValidationResult::Invalid(_)
981 ));
982
983 assert!(matches!(
984 validate_birthday(&Some("1990/01/01".to_string()), "生日"),
985 ValidationResult::Invalid(_)
986 ));
987
988 assert!(matches!(
989 validate_birthday(&Some("1990-13-01".to_string()), "生日"),
990 ValidationResult::Invalid(_)
991 ));
992 }
993
994 #[test]
995 fn test_validate_expected_salary() {
996 assert!(matches!(
998 validate_expected_salary(&Some("10000-15000".to_string()), "期望薪资"),
999 ValidationResult::Valid
1000 ));
1001
1002 assert!(matches!(
1003 validate_expected_salary(&Some("面议".to_string()), "期望薪资"),
1004 ValidationResult::Valid
1005 ));
1006
1007 assert!(matches!(
1009 validate_expected_salary(&None, "期望薪资"),
1010 ValidationResult::Valid
1011 ));
1012
1013 let long_salary = "1".repeat(employee_limits::EXPECTED_SALARY_MAX_LENGTH + 1);
1015 assert!(matches!(
1016 validate_expected_salary(&Some(long_salary), "期望薪资"),
1017 ValidationResult::Invalid(_)
1018 ));
1019 }
1020
1021 #[test]
1022 fn test_validate_tags() {
1023 let valid_tags = vec![
1025 "Java".to_string(),
1026 "Python".to_string(),
1027 "React".to_string(),
1028 ];
1029 assert!(matches!(
1030 validate_tags(&valid_tags, "技能标签"),
1031 ValidationResult::Valid
1032 ));
1033
1034 assert!(matches!(
1036 validate_tags(&[], "技能标签"),
1037 ValidationResult::Valid
1038 ));
1039
1040 let too_many_tags: Vec<String> = (0..employee_limits::MAX_TALENT_TAGS + 1)
1042 .map(|i| format!("tag{}", i))
1043 .collect();
1044 assert!(matches!(
1045 validate_tags(&too_many_tags, "技能标签"),
1046 ValidationResult::Invalid(_)
1047 ));
1048
1049 let tags_with_empty = vec!["Java".to_string(), "".to_string()];
1051 assert!(matches!(
1052 validate_tags(&tags_with_empty, "技能标签"),
1053 ValidationResult::Invalid(_)
1054 ));
1055
1056 let long_tag = "a".repeat(employee_limits::TAG_MAX_LENGTH + 1);
1058 let tags_with_long = vec![long_tag];
1059 assert!(matches!(
1060 validate_tags(&tags_with_long, "技能标签"),
1061 ValidationResult::Invalid(_)
1062 ));
1063 }
1064
1065 #[test]
1066 fn test_sanitize_name() {
1067 assert_eq!(sanitize_name(" 张三 "), "张三");
1069
1070 assert_eq!(sanitize_name("张 三"), "张 三");
1072
1073 assert_eq!(sanitize_name("John Smith"), "John Smith");
1075
1076 assert_eq!(sanitize_name(""), "");
1078 }
1079
1080 #[test]
1081 fn test_sanitize_tags() {
1082 let input_tags = vec![
1083 " Java ".to_string(),
1084 "Python".to_string(),
1085 " ".to_string(),
1086 "React JS".to_string(),
1087 ];
1088
1089 let sanitized = sanitize_tags(&input_tags);
1090 assert_eq!(sanitized, vec!["java", "python", "react js"]);
1091 }
1092
1093 #[test]
1094 fn test_sanitize_tag() {
1095 assert_eq!(sanitize_tag(" Java-Script "), "java_script");
1097 assert_eq!(sanitize_tag("Node.js"), "node.js");
1098 assert_eq!(sanitize_tag("C++"), "c++");
1099 assert_eq!(sanitize_tag("React_Native"), "react_native");
1100 }
1101
1102 #[test]
1103 fn test_validate_page_size() {
1104 assert!(matches!(
1106 validate_page_size(10, "页面大小"),
1107 ValidationResult::Valid
1108 ));
1109
1110 assert!(matches!(
1111 validate_page_size(pagination_limits::MAX_PAGE_SIZE, "页面大小"),
1112 ValidationResult::Valid
1113 ));
1114
1115 assert!(matches!(
1117 validate_page_size(0, "页面大小"),
1118 ValidationResult::Invalid(_)
1119 ));
1120
1121 assert!(matches!(
1122 validate_page_size(pagination_limits::MAX_PAGE_SIZE + 1, "页面大小"),
1123 ValidationResult::Invalid(_)
1124 ));
1125 }
1126
1127 #[test]
1128 fn test_validate_page_token() {
1129 assert!(matches!(
1131 validate_page_token("valid_token_123", "页面令牌"),
1132 ValidationResult::Valid
1133 ));
1134
1135 assert!(matches!(
1137 validate_page_token("", "页面令牌"),
1138 ValidationResult::Valid
1139 ));
1140
1141 let long_token = "a".repeat(pagination_limits::MAX_PAGE_TOKEN_LENGTH + 1);
1143 assert!(matches!(
1144 validate_page_token(&long_token, "页面令牌"),
1145 ValidationResult::Invalid(_)
1146 ));
1147 }
1148
1149 #[test]
1150 fn test_validate_pagination_params() {
1151 assert!(matches!(
1153 validate_pagination_params(Some(10), Some("token123"), "test"),
1154 ValidationResult::Valid
1155 ));
1156
1157 assert!(matches!(
1159 validate_pagination_params(Some(10), None, "test"),
1160 ValidationResult::Valid
1161 ));
1162
1163 assert!(matches!(
1165 validate_pagination_params(None, Some("token123"), "test"),
1166 ValidationResult::Valid
1167 ));
1168
1169 assert!(matches!(
1171 validate_pagination_params(None, None, "test"),
1172 ValidationResult::Valid
1173 ));
1174
1175 assert!(matches!(
1177 validate_pagination_params(Some(0), Some("token123"), "test"),
1178 ValidationResult::Invalid(_)
1179 ));
1180
1181 assert!(matches!(
1183 validate_pagination_params(Some(10), Some(""), "test"),
1184 ValidationResult::Valid
1185 ));
1186 }
1187
1188 #[test]
1189 fn test_validate_custom_fields() {
1190 use serde_json::Value;
1191 use std::collections::HashMap;
1192
1193 assert!(matches!(
1195 validate_custom_fields(&None, "自定义字段"),
1196 ValidationResult::Valid
1197 ));
1198
1199 let mut valid_fields = HashMap::new();
1201 valid_fields.insert(
1202 "skill_level".to_string(),
1203 Value::String("advanced".to_string()),
1204 );
1205 valid_fields.insert(
1206 "years_exp".to_string(),
1207 Value::Number(serde_json::Number::from(5)),
1208 );
1209 valid_fields.insert("is_certified".to_string(), Value::Bool(true));
1210 valid_fields.insert(
1211 "tags".to_string(),
1212 Value::Array(vec![Value::String("rust".to_string())]),
1213 );
1214 valid_fields.insert("nullable_field".to_string(), Value::Null);
1215
1216 assert!(matches!(
1217 validate_custom_fields(&Some(valid_fields), "自定义字段"),
1218 ValidationResult::Valid
1219 ));
1220
1221 let mut too_many_fields = HashMap::new();
1223 for i in 0..51 {
1224 too_many_fields.insert(format!("field_{}", i), Value::String("value".to_string()));
1225 }
1226 assert!(matches!(
1227 validate_custom_fields(&Some(too_many_fields), "自定义字段"),
1228 ValidationResult::Invalid(_)
1229 ));
1230
1231 let mut empty_key_fields = HashMap::new();
1233 empty_key_fields.insert("".to_string(), Value::String("value".to_string()));
1234 assert!(matches!(
1235 validate_custom_fields(&Some(empty_key_fields), "自定义字段"),
1236 ValidationResult::Invalid(_)
1237 ));
1238
1239 let mut long_key_fields = HashMap::new();
1241 let long_key = "a".repeat(employee_limits::CUSTOM_FIELD_KEY_MAX_LENGTH + 1);
1242 long_key_fields.insert(long_key, Value::String("value".to_string()));
1243 assert!(matches!(
1244 validate_custom_fields(&Some(long_key_fields), "自定义字段"),
1245 ValidationResult::Invalid(_)
1246 ));
1247
1248 let mut long_value_fields = HashMap::new();
1250 let long_value = "a".repeat(employee_limits::CUSTOM_FIELD_VALUE_MAX_LENGTH + 1);
1251 long_value_fields.insert("key".to_string(), Value::String(long_value));
1252 assert!(matches!(
1253 validate_custom_fields(&Some(long_value_fields), "自定义字段"),
1254 ValidationResult::Invalid(_)
1255 ));
1256
1257 let mut large_array_fields = HashMap::new();
1259 let large_array = (0..101)
1260 .map(|i| Value::Number(serde_json::Number::from(i)))
1261 .collect();
1262 large_array_fields.insert("large_array".to_string(), Value::Array(large_array));
1263 assert!(matches!(
1264 validate_custom_fields(&Some(large_array_fields), "自定义字段"),
1265 ValidationResult::Invalid(_)
1266 ));
1267
1268 let mut object_fields = HashMap::new();
1270 let mut nested_object = HashMap::new();
1271 nested_object.insert("nested".to_string(), Value::String("value".to_string()));
1272 object_fields.insert(
1273 "object_field".to_string(),
1274 Value::Object(serde_json::Map::from_iter(nested_object)),
1275 );
1276 assert!(matches!(
1277 validate_custom_fields(&Some(object_fields), "自定义字段"),
1278 ValidationResult::Invalid(_)
1279 ));
1280 }
1281
1282 #[test]
1283 fn test_validate_resume_attachment_ids() {
1284 let valid_ids = vec!["attachment_1".to_string(), "attachment_2".to_string()];
1286 assert!(matches!(
1287 validate_resume_attachment_ids(&valid_ids, "简历附件"),
1288 ValidationResult::Valid
1289 ));
1290
1291 assert!(matches!(
1293 validate_resume_attachment_ids(&[], "简历附件"),
1294 ValidationResult::Valid
1295 ));
1296
1297 let too_many_ids: Vec<String> = (0..employee_limits::MAX_RESUME_ATTACHMENTS + 1)
1299 .map(|i| format!("attachment_{}", i))
1300 .collect();
1301 assert!(matches!(
1302 validate_resume_attachment_ids(&too_many_ids, "简历附件"),
1303 ValidationResult::Invalid(_)
1304 ));
1305
1306 let empty_id_list = vec!["valid_id".to_string(), "".to_string()];
1308 assert!(matches!(
1309 validate_resume_attachment_ids(&empty_id_list, "简历附件"),
1310 ValidationResult::Invalid(_)
1311 ));
1312
1313 let short_id_list = vec!["short".to_string()];
1315 assert!(matches!(
1316 validate_resume_attachment_ids(&short_id_list, "简历附件"),
1317 ValidationResult::Invalid(_)
1318 ));
1319
1320 let long_id_list = vec!["a".repeat(101)];
1321 assert!(matches!(
1322 validate_resume_attachment_ids(&long_id_list, "简历附件"),
1323 ValidationResult::Invalid(_)
1324 ));
1325 }
1326
1327 #[test]
1328 fn test_validate_talent_tag() {
1329 assert!(matches!(
1331 validate_talent_tag("rust", "技能标签"),
1332 ValidationResult::Valid
1333 ));
1334
1335 assert!(matches!(
1336 validate_talent_tag("Java_Spring", "技能标签"),
1337 ValidationResult::Valid
1338 ));
1339
1340 assert!(matches!(
1341 validate_talent_tag("前端开发", "技能标签"),
1342 ValidationResult::Valid
1343 ));
1344
1345 assert!(matches!(
1346 validate_talent_tag("Node_js", "技能标签"),
1347 ValidationResult::Valid
1348 ));
1349
1350 assert!(matches!(
1352 validate_talent_tag("", "技能标签"),
1353 ValidationResult::Invalid(_)
1354 ));
1355
1356 let long_tag = "a".repeat(51);
1358 assert!(matches!(
1359 validate_talent_tag(&long_tag, "技能标签"),
1360 ValidationResult::Invalid(_)
1361 ));
1362
1363 assert!(matches!(
1365 validate_talent_tag("tag@domain", "技能标签"),
1366 ValidationResult::Invalid(_)
1367 ));
1368
1369 assert!(matches!(
1370 validate_talent_tag("tag*wildcard", "技能标签"),
1371 ValidationResult::Invalid(_)
1372 ));
1373 }
1374
1375 #[test]
1376 fn test_validate_talent_tags() {
1377 let valid_tags = vec![
1379 "rust".to_string(),
1380 "javascript".to_string(),
1381 "前端开发".to_string(),
1382 ];
1383 assert!(matches!(
1384 validate_talent_tags(&valid_tags),
1385 ValidationResult::Valid
1386 ));
1387
1388 assert!(matches!(validate_talent_tags(&[]), ValidationResult::Valid));
1390
1391 let too_many_tags: Vec<String> = (0..21).map(|i| format!("tag_{}", i)).collect();
1393 assert!(matches!(
1394 validate_talent_tags(&too_many_tags),
1395 ValidationResult::Invalid(_)
1396 ));
1397
1398 let invalid_tags = vec!["valid_tag".to_string(), "invalid@tag".to_string()];
1400 assert!(matches!(
1401 validate_talent_tags(&invalid_tags),
1402 ValidationResult::Invalid(_)
1403 ));
1404 }
1405
1406 #[test]
1407 fn test_validate_resume_attachment() {
1408 assert!(matches!(
1410 validate_resume_attachment("valid_attachment_123", "附件ID"),
1411 ValidationResult::Valid
1412 ));
1413
1414 assert!(matches!(
1415 validate_resume_attachment("attachment-with-hyphens", "附件ID"),
1416 ValidationResult::Valid
1417 ));
1418
1419 assert!(matches!(
1420 validate_resume_attachment("attachment_with_underscores", "附件ID"),
1421 ValidationResult::Valid
1422 ));
1423
1424 assert!(matches!(
1426 validate_resume_attachment("", "附件ID"),
1427 ValidationResult::Invalid(_)
1428 ));
1429
1430 let long_id = "a".repeat(101);
1432 assert!(matches!(
1433 validate_resume_attachment(&long_id, "附件ID"),
1434 ValidationResult::Invalid(_)
1435 ));
1436
1437 assert!(matches!(
1439 validate_resume_attachment("attachment@domain", "附件ID"),
1440 ValidationResult::Invalid(_)
1441 ));
1442
1443 assert!(matches!(
1444 validate_resume_attachment("attachment with spaces", "附件ID"),
1445 ValidationResult::Invalid(_)
1446 ));
1447
1448 assert!(matches!(
1449 validate_resume_attachment("attachment/with/slashes", "附件ID"),
1450 ValidationResult::Invalid(_)
1451 ));
1452 }
1453
1454 #[test]
1455 fn test_sanitize_name_edge_cases() {
1456 assert_eq!(sanitize_name(" "), "");
1458
1459 assert_eq!(sanitize_name(" 张\t三 \n"), "张 三");
1461
1462 assert_eq!(sanitize_name("John Smith"), "John Smith");
1464
1465 assert_eq!(sanitize_name(" Mary Jane "), "Mary Jane");
1467
1468 assert_eq!(sanitize_name("Test\t\tName\n\n"), "Test Name");
1470
1471 assert_eq!(sanitize_name("A"), "A");
1473
1474 assert_eq!(sanitize_name(" 李 小 明 "), "李 小 明");
1476 }
1477
1478 #[test]
1479 fn test_sanitize_tags_edge_cases() {
1480 let input_tags = vec!["java".to_string(), "JAVA".to_string(), "Java".to_string()];
1482 let sanitized = sanitize_tags(&input_tags);
1483 assert_eq!(sanitized, vec!["java"]);
1484
1485 let input_tags = vec![
1487 "valid".to_string(),
1488 "".to_string(),
1489 " ".to_string(),
1490 "another".to_string(),
1491 ];
1492 let sanitized = sanitize_tags(&input_tags);
1493 assert_eq!(sanitized, vec!["valid", "another"]);
1494
1495 let input_tags = vec![
1497 "node-js".to_string(),
1498 "react_native".to_string(),
1499 "vue.js".to_string(),
1500 ];
1501 let sanitized = sanitize_tags(&input_tags);
1502 assert_eq!(sanitized, vec!["node_js", "react_native", "vue.js"]);
1503
1504 let sanitized = sanitize_tags(&[]);
1506 assert!(sanitized.is_empty());
1507 }
1508
1509 #[test]
1510 fn test_sanitize_tag_individual() {
1511 assert_eq!(sanitize_tag(" JavaScript "), "javascript");
1513
1514 assert_eq!(sanitize_tag("Node-JS"), "node_js");
1516
1517 assert_eq!(sanitize_tag("React_Native"), "react_native");
1519
1520 assert_eq!(sanitize_tag("Vue-Router_Plugin"), "vue_router_plugin");
1522
1523 assert_eq!(sanitize_tag(""), "");
1525
1526 assert_eq!(sanitize_tag(" "), "");
1528
1529 assert_eq!(sanitize_tag("lowercase"), "lowercase");
1531 }
1532
1533 #[test]
1534 fn test_validate_page_size_warnings() {
1535 assert!(matches!(
1537 validate_page_size(pagination_limits::MIN_PAGE_SIZE, "页面大小"),
1538 ValidationResult::Valid
1539 ));
1540
1541 assert!(matches!(
1542 validate_page_size(pagination_limits::MAX_PAGE_SIZE, "页面大小"),
1543 ValidationResult::Valid
1544 ));
1545
1546 assert!(matches!(
1547 validate_page_size(pagination_limits::RECOMMENDED_PAGE_SIZE, "页面大小"),
1548 ValidationResult::Valid
1549 ));
1550
1551 assert!(matches!(
1553 validate_page_size(pagination_limits::RECOMMENDED_PAGE_SIZE + 1, "页面大小"),
1554 ValidationResult::Valid
1555 ));
1556 }
1557
1558 #[test]
1559 fn test_validate_page_token_formats() {
1560 assert!(matches!(
1562 validate_page_token("dGVzdA==", "页面令牌"),
1563 ValidationResult::Valid
1564 ));
1565
1566 assert!(matches!(
1567 validate_page_token("YWJjZGVmZw", "页面令牌"),
1568 ValidationResult::Valid
1569 ));
1570
1571 assert!(matches!(
1573 validate_page_token("abc-def_123", "页面令牌"),
1574 ValidationResult::Valid
1575 ));
1576
1577 assert!(matches!(
1579 validate_page_token("invalid@token", "页面令牌"),
1580 ValidationResult::Invalid(_)
1581 ));
1582
1583 assert!(matches!(
1584 validate_page_token("token with spaces", "页面令牌"),
1585 ValidationResult::Invalid(_)
1586 ));
1587
1588 let long_token = "a".repeat(pagination_limits::MAX_PAGE_TOKEN_LENGTH + 1);
1590 assert!(matches!(
1591 validate_page_token(&long_token, "页面令牌"),
1592 ValidationResult::Invalid(_)
1593 ));
1594 }
1595
1596 #[test]
1597 fn test_validation_result_error_method() {
1598 let valid = ValidationResult::Valid;
1599 assert_eq!(valid.error(), None);
1600
1601 let warning = ValidationResult::Warning("warning message".to_string());
1602 assert_eq!(warning.error(), Some("warning message"));
1603
1604 let invalid = ValidationResult::Invalid("error message".to_string());
1605 assert_eq!(invalid.error(), Some("error message"));
1606 }
1607
1608 #[test]
1609 fn test_is_chinese_char_comprehensive() {
1610 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'));
1632 assert!(!is_chinese_char('A'));
1633 assert!(!is_chinese_char('1'));
1634 assert!(!is_chinese_char('!'));
1635 assert!(!is_chinese_char(' '));
1636 assert!(!is_chinese_char('\n'));
1637 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}')); }
1646
1647 #[test]
1648 fn test_validate_content_size_edge_cases() {
1649 assert!(validate_content_size("", 100, "测试内容"));
1651
1652 let content = "a".repeat(100);
1654 assert!(validate_content_size(&content, 100, "测试内容"));
1655
1656 let content = "a".repeat(101);
1658 assert!(!validate_content_size(&content, 100, "测试内容"));
1659
1660 let chinese_content = "中文测试内容";
1662 assert!(validate_content_size(chinese_content, 50, "中文内容"));
1664 assert!(!validate_content_size(chinese_content, 10, "中文内容"));
1665 }
1666
1667 #[test]
1668 fn test_validate_string_length_utf8() {
1669 let chinese_input = "中文测试".to_string(); let result = validate_string_length(chinese_input, 6, "中文字段");
1672 assert_eq!(result.len(), 6);
1674
1675 let english_input = "hello world".to_string();
1677 let result = validate_string_length(english_input, 5, "英文字段");
1678 assert_eq!(result, "hello");
1679 }
1680
1681 #[test]
1682 fn test_validate_builder_trait() {
1683 struct TestBuilder {
1685 result: ValidationResult,
1686 }
1687
1688 impl ValidateBuilder for TestBuilder {
1689 fn validate(&self) -> ValidationResult {
1690 self.result.clone()
1691 }
1692 }
1693
1694 let valid_builder = TestBuilder {
1696 result: ValidationResult::Valid,
1697 };
1698 assert!(valid_builder.validate_and_log());
1699
1700 let warning_builder = TestBuilder {
1702 result: ValidationResult::Warning("test warning".to_string()),
1703 };
1704 assert!(warning_builder.validate_and_log());
1705
1706 let invalid_builder = TestBuilder {
1708 result: ValidationResult::Invalid("test error".to_string()),
1709 };
1710 assert!(!invalid_builder.validate_and_log());
1711 }
1712}
1713
1714pub mod employee_limits {
1720 pub const NAME_MIN_LENGTH: usize = 2;
1722 pub const NAME_MAX_LENGTH: usize = 100;
1724 pub const EMAIL_MAX_LENGTH: usize = 254;
1726 pub const PHONE_MAX_LENGTH: usize = 20;
1728 pub const PHONE_MIN_LENGTH: usize = 7;
1730 pub const WORK_EXPERIENCE_MIN: u32 = 0;
1732 pub const WORK_EXPERIENCE_MAX: u32 = 50;
1734 pub const MAX_RESUME_ATTACHMENTS: usize = 10;
1736 pub const MAX_TALENT_TAGS: usize = 20;
1738 pub const TAG_MAX_LENGTH: usize = 50;
1740 pub const CUSTOM_FIELD_KEY_MAX_LENGTH: usize = 100;
1742 pub const CUSTOM_FIELD_VALUE_MAX_LENGTH: usize = 1000;
1744 pub const EXPECTED_SALARY_MAX_LENGTH: usize = 100;
1746}
1747
1748pub fn validate_name(name: &str, field_name: &str) -> ValidationResult {
1754 if name.is_empty() {
1755 return ValidationResult::Invalid(format!("{} cannot be empty", field_name));
1756 }
1757
1758 let char_count = name.chars().count();
1760
1761 if char_count < employee_limits::NAME_MIN_LENGTH {
1762 return ValidationResult::Invalid(format!(
1763 "{} must be at least {} characters long",
1764 field_name,
1765 employee_limits::NAME_MIN_LENGTH
1766 ));
1767 }
1768
1769 if char_count > employee_limits::NAME_MAX_LENGTH {
1770 return ValidationResult::Invalid(format!(
1771 "{} must not exceed {} characters",
1772 field_name,
1773 employee_limits::NAME_MAX_LENGTH
1774 ));
1775 }
1776
1777 if !name
1779 .chars()
1780 .all(|c| c.is_alphanumeric() || c.is_whitespace() || is_chinese_char(c) || "-.".contains(c))
1781 {
1782 return ValidationResult::Invalid(format!(
1783 "{} contains invalid characters. Only letters, numbers, Chinese characters, spaces, hyphens and periods are allowed",
1784 field_name
1785 ));
1786 }
1787
1788 ValidationResult::Valid
1789}
1790
1791pub fn validate_email(email: &str, field_name: &str) -> ValidationResult {
1793 if email.is_empty() {
1794 return ValidationResult::Invalid(format!("{} cannot be empty", field_name));
1795 }
1796
1797 if email.len() > employee_limits::EMAIL_MAX_LENGTH {
1798 return ValidationResult::Invalid(format!(
1799 "{} must not exceed {} characters",
1800 field_name,
1801 employee_limits::EMAIL_MAX_LENGTH
1802 ));
1803 }
1804
1805 let email_regex =
1807 regex::Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap();
1808 if !email_regex.is_match(email) {
1809 return ValidationResult::Invalid(format!("{} must be a valid email address", field_name));
1810 }
1811
1812 ValidationResult::Valid
1813}
1814
1815pub fn validate_phone(phone: &str, field_name: &str) -> ValidationResult {
1817 if phone.is_empty() {
1818 return ValidationResult::Valid; }
1820
1821 if phone.len() < employee_limits::PHONE_MIN_LENGTH {
1822 return ValidationResult::Invalid(format!(
1823 "{} must be at least {} characters long (got {})",
1824 field_name,
1825 employee_limits::PHONE_MIN_LENGTH,
1826 phone.len()
1827 ));
1828 }
1829
1830 if phone.len() > employee_limits::PHONE_MAX_LENGTH {
1831 return ValidationResult::Invalid(format!(
1832 "{} must not exceed {} characters",
1833 field_name,
1834 employee_limits::PHONE_MAX_LENGTH
1835 ));
1836 }
1837
1838 if !phone
1840 .chars()
1841 .all(|c| c.is_ascii_digit() || c == '+' || c == ' ' || c == '-')
1842 {
1843 return ValidationResult::Invalid(format!(
1844 "{} contains invalid characters. Only numbers, +, spaces and hyphens are allowed",
1845 field_name
1846 ));
1847 }
1848
1849 ValidationResult::Valid
1850}
1851
1852pub fn validate_work_experience(years: u32, field_name: &str) -> ValidationResult {
1854 if years > employee_limits::WORK_EXPERIENCE_MAX {
1855 return ValidationResult::Invalid(format!(
1856 "{} must not exceed {} years",
1857 field_name,
1858 employee_limits::WORK_EXPERIENCE_MAX
1859 ));
1860 }
1861
1862 ValidationResult::Valid
1863}
1864
1865pub fn validate_birthday(birthday: &Option<String>, field_name: &str) -> ValidationResult {
1867 if let Some(bday) = birthday {
1868 if bday.is_empty() {
1869 return ValidationResult::Valid;
1870 }
1871
1872 let date_regex = regex::Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap();
1874 if !date_regex.is_match(bday) {
1875 return ValidationResult::Invalid(format!(
1876 "{} must be in YYYY-MM-DD format",
1877 field_name
1878 ));
1879 }
1880
1881 if chrono::NaiveDate::parse_from_str(bday, "%Y-%m-%d").is_err() {
1883 return ValidationResult::Invalid(format!(
1884 "{} must be a valid date in YYYY-MM-DD format",
1885 field_name
1886 ));
1887 }
1888 }
1889
1890 ValidationResult::Valid
1891}
1892
1893pub fn validate_expected_salary(salary: &Option<String>, field_name: &str) -> ValidationResult {
1895 if let Some(sal) = salary {
1896 if sal.is_empty() {
1897 return ValidationResult::Valid;
1898 }
1899
1900 if sal.len() > employee_limits::EXPECTED_SALARY_MAX_LENGTH {
1901 return ValidationResult::Invalid(format!(
1902 "{} must not exceed {} characters",
1903 field_name,
1904 employee_limits::EXPECTED_SALARY_MAX_LENGTH
1905 ));
1906 }
1907
1908 let salary_regex = regex::Regex::new(r"^(\d+(-\d+)?[Kk万]?([+-])?|面议)$").unwrap();
1910 if !salary_regex.is_match(sal) {
1911 return ValidationResult::Invalid(format!(
1912 "{} must be a valid salary format (e.g., 10-20K, 50-80万, 100万+)",
1913 field_name
1914 ));
1915 }
1916
1917 if let Some(captures) = salary_regex.captures(sal) {
1919 if let Some(num_str) = captures.get(1) {
1920 let num_part = num_str.as_str();
1921 let num_regex = regex::Regex::new(r"^(\d+)").unwrap();
1923 if let Some(num_captures) = num_regex.captures(num_part) {
1924 if let Some(num_match) = num_captures.get(1) {
1925 if let Ok(num) = num_match.as_str().parse::<u32>() {
1926 let is_k = sal.contains('K') || sal.contains('k');
1928 let is_wan = sal.contains('万');
1930
1931 let actual_num = if is_k {
1932 num
1933 } else if is_wan {
1934 num * 10 } else {
1936 num / 1000 };
1938
1939 if actual_num > 500 {
1941 return ValidationResult::Invalid(format!(
1942 "{}: salary range is unreasonably high",
1943 field_name
1944 ));
1945 }
1946 }
1947 }
1948 }
1949 }
1950 }
1951 }
1952
1953 ValidationResult::Valid
1954}
1955
1956pub fn validate_tags(tags: &[String], field_name: &str) -> ValidationResult {
1958 if tags.len() > employee_limits::MAX_TALENT_TAGS {
1959 return ValidationResult::Invalid(format!(
1960 "{} cannot have more than {} tags",
1961 field_name,
1962 employee_limits::MAX_TALENT_TAGS
1963 ));
1964 }
1965
1966 for (i, tag) in tags.iter().enumerate() {
1967 if tag.is_empty() {
1968 return ValidationResult::Invalid(format!(
1969 "{} tag at index {} cannot be empty",
1970 field_name, i
1971 ));
1972 }
1973
1974 if tag.len() > employee_limits::TAG_MAX_LENGTH {
1975 return ValidationResult::Invalid(format!(
1976 "{} tag '{}' exceeds maximum length of {} characters",
1977 field_name,
1978 tag,
1979 employee_limits::TAG_MAX_LENGTH
1980 ));
1981 }
1982
1983 if !tag
1985 .chars()
1986 .all(|c| c.is_alphanumeric() || c == '_' || c == '-' || is_chinese_char(c))
1987 {
1988 return ValidationResult::Invalid(format!(
1989 "{} tag '{}' contains invalid characters. Only letters, numbers, Chinese characters, underscores and hyphens are allowed",
1990 field_name,
1991 tag
1992 ));
1993 }
1994 }
1995
1996 ValidationResult::Valid
1997}
1998
1999pub fn validate_custom_fields(
2001 fields: &Option<std::collections::HashMap<String, serde_json::Value>>,
2002 field_name: &str,
2003) -> ValidationResult {
2004 if let Some(custom_fields) = fields {
2005 if custom_fields.len() > 50 {
2006 return ValidationResult::Invalid(format!(
2008 "{} cannot have more than 50 custom fields",
2009 field_name
2010 ));
2011 }
2012
2013 for (key, value) in custom_fields {
2014 if key.is_empty() {
2016 return ValidationResult::Invalid(format!(
2017 "{} custom field key cannot be empty",
2018 field_name
2019 ));
2020 }
2021
2022 if key.len() > employee_limits::CUSTOM_FIELD_KEY_MAX_LENGTH {
2023 return ValidationResult::Invalid(format!(
2024 "{} custom field key '{}' exceeds maximum length of {} characters",
2025 field_name,
2026 key,
2027 employee_limits::CUSTOM_FIELD_KEY_MAX_LENGTH
2028 ));
2029 }
2030
2031 match value {
2033 serde_json::Value::String(s) => {
2034 if s.len() > employee_limits::CUSTOM_FIELD_VALUE_MAX_LENGTH {
2035 return ValidationResult::Invalid(format!(
2036 "{} custom field value for key '{}' exceeds maximum length of {} characters",
2037 field_name,
2038 key,
2039 employee_limits::CUSTOM_FIELD_VALUE_MAX_LENGTH
2040 ));
2041 }
2042 }
2043 serde_json::Value::Number(n) => {
2044 if !n.is_i64() && !n.is_u64() && !n.is_f64() {
2045 return ValidationResult::Invalid(format!(
2046 "{} custom field value for key '{}' is not a valid number",
2047 field_name, key
2048 ));
2049 }
2050 }
2051 serde_json::Value::Bool(_) => {
2052 }
2054 serde_json::Value::Array(arr) => {
2055 if arr.len() > 100 {
2056 return ValidationResult::Invalid(format!(
2057 "{} custom field array for key '{}' cannot have more than 100 items",
2058 field_name, key
2059 ));
2060 }
2061 }
2062 serde_json::Value::Object(_) => {
2063 return ValidationResult::Invalid(format!(
2064 "{} custom field value for key '{}' cannot be an object",
2065 field_name, key
2066 ));
2067 }
2068 serde_json::Value::Null => {
2069 }
2071 }
2072 }
2073 }
2074
2075 ValidationResult::Valid
2076}
2077
2078pub fn validate_resume_attachment_ids(
2080 attachment_ids: &[String],
2081 field_name: &str,
2082) -> ValidationResult {
2083 if attachment_ids.len() > employee_limits::MAX_RESUME_ATTACHMENTS {
2084 return ValidationResult::Invalid(format!(
2085 "{} cannot have more than {} resume attachments",
2086 field_name,
2087 employee_limits::MAX_RESUME_ATTACHMENTS
2088 ));
2089 }
2090
2091 for (i, id) in attachment_ids.iter().enumerate() {
2092 if id.is_empty() {
2093 return ValidationResult::Invalid(format!(
2094 "{} attachment ID at index {} cannot be empty",
2095 field_name, i
2096 ));
2097 }
2098
2099 if id.len() < 10 || id.len() > 100 {
2101 return ValidationResult::Invalid(format!(
2102 "{} attachment ID at index {} has invalid length",
2103 field_name, i
2104 ));
2105 }
2106 }
2107
2108 ValidationResult::Valid
2109}
2110
2111pub fn sanitize_name(name: &str) -> String {
2113 let trimmed = name.trim();
2115
2116 let normalized = trimmed.chars().collect::<Vec<_>>();
2118 let mut result = Vec::new();
2119 let mut prev_was_space = false;
2120
2121 for c in normalized {
2122 if c.is_whitespace() {
2123 if !prev_was_space {
2124 result.push(' ');
2125 prev_was_space = true;
2126 }
2127 } else {
2128 result.push(c);
2129 prev_was_space = false;
2130 }
2131 }
2132
2133 result.into_iter().collect()
2134}
2135
2136pub fn sanitize_tags(tags: &[String]) -> Vec<String> {
2138 let mut result = Vec::new();
2139
2140 for tag in tags {
2141 let sanitized = tag
2142 .trim()
2143 .replace(['_', '-'], "_") .to_lowercase();
2145
2146 if !sanitized.is_empty() && !result.contains(&sanitized) {
2147 result.push(sanitized);
2148 }
2149 }
2150
2151 result
2152}
2153
2154pub fn validate_talent_tag(tag: &str, field_name: &str) -> ValidationResult {
2163 if tag.is_empty() {
2164 return ValidationResult::Invalid(format!("{}: tag cannot be empty", field_name));
2165 }
2166
2167 if tag.len() > 50 {
2168 return ValidationResult::Invalid(format!(
2169 "{}: tag must not exceed 50 characters (got {})",
2170 field_name,
2171 tag.len()
2172 ));
2173 }
2174
2175 for c in tag.chars() {
2177 if !(c.is_alphanumeric() || c == '_' || c == '-' || c == ' ' || is_chinese_char(c)) {
2178 return ValidationResult::Invalid(format!(
2179 "{}: tag contains invalid character '{}'. Only letters, numbers, spaces, hyphens, underscores and Chinese characters are allowed",
2180 field_name, c
2181 ));
2182 }
2183 }
2184
2185 ValidationResult::Valid
2186}
2187
2188pub fn validate_talent_tags(tags: &[String]) -> ValidationResult {
2196 if tags.len() > 20 {
2197 return ValidationResult::Invalid(format!(
2198 "Invalid tags: maximum number of tags is 20 (got {})",
2199 tags.len()
2200 ));
2201 }
2202
2203 for (index, tag) in tags.iter().enumerate() {
2204 match validate_talent_tag(tag, &format!("tags[{}]", index)) {
2205 ValidationResult::Valid => {}
2206 ValidationResult::Warning(msg) => {
2207 return ValidationResult::Warning(msg);
2208 }
2209 ValidationResult::Invalid(msg) => {
2210 return ValidationResult::Invalid(msg);
2211 }
2212 }
2213 }
2214
2215 ValidationResult::Valid
2216}
2217
2218pub fn validate_resume_attachment(attachment_id: &str, field_name: &str) -> ValidationResult {
2227 if attachment_id.is_empty() {
2228 return ValidationResult::Invalid(format!("{}: attachment ID cannot be empty", field_name));
2229 }
2230
2231 if attachment_id.len() > 100 {
2232 return ValidationResult::Invalid(format!(
2233 "{}: attachment ID must not exceed 100 characters (got {})",
2234 field_name,
2235 attachment_id.len()
2236 ));
2237 }
2238
2239 if !attachment_id
2241 .chars()
2242 .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
2243 {
2244 return ValidationResult::Invalid(format!(
2245 "{}: attachment ID can only contain letters, numbers, hyphens and underscores",
2246 field_name
2247 ));
2248 }
2249
2250 ValidationResult::Valid
2251}
2252
2253pub mod pagination_limits {
2255 pub const DEFAULT_PAGE_SIZE: u32 = 20;
2257 pub const MIN_PAGE_SIZE: u32 = 1;
2259 pub const MAX_PAGE_SIZE: u32 = 500;
2261 pub const RECOMMENDED_PAGE_SIZE: u32 = 50;
2263 pub const MAX_PAGE_TOKEN_LENGTH: usize = 1024;
2265}
2266
2267pub fn validate_page_size(page_size: u32, field_name: &str) -> ValidationResult {
2276 if page_size < pagination_limits::MIN_PAGE_SIZE {
2277 return ValidationResult::Invalid(format!(
2278 "{}: page size must be at least {}",
2279 field_name,
2280 pagination_limits::MIN_PAGE_SIZE
2281 ));
2282 }
2283
2284 if page_size > pagination_limits::MAX_PAGE_SIZE {
2285 return ValidationResult::Invalid(format!(
2286 "{}: page size must not exceed {} (recommended: {})",
2287 field_name,
2288 pagination_limits::MAX_PAGE_SIZE,
2289 pagination_limits::RECOMMENDED_PAGE_SIZE
2290 ));
2291 }
2292
2293 if page_size > pagination_limits::RECOMMENDED_PAGE_SIZE {
2295 log::warn!(
2296 "{}: page size {} is larger than recommended value {}. This may impact performance.",
2297 field_name,
2298 page_size,
2299 pagination_limits::RECOMMENDED_PAGE_SIZE
2300 );
2301 }
2302
2303 ValidationResult::Valid
2304}
2305
2306pub fn validate_page_token(page_token: &str, field_name: &str) -> ValidationResult {
2315 if page_token.is_empty() {
2316 return ValidationResult::Valid;
2318 }
2319
2320 if page_token.len() > pagination_limits::MAX_PAGE_TOKEN_LENGTH {
2321 return ValidationResult::Invalid(format!(
2322 "{}: page token must not exceed {} characters",
2323 field_name,
2324 pagination_limits::MAX_PAGE_TOKEN_LENGTH
2325 ));
2326 }
2327
2328 if !page_token.chars().all(|c| {
2330 c.is_ascii_alphanumeric() || c == '/' || c == '+' || c == '=' || c == '-' || c == '_'
2331 }) {
2332 return ValidationResult::Invalid(format!(
2333 "{}: page token contains invalid characters. Expected base64 format",
2334 field_name
2335 ));
2336 }
2337
2338 ValidationResult::Valid
2339}
2340
2341pub fn validate_pagination_params(
2351 page_size: Option<u32>,
2352 page_token: Option<&str>,
2353 field_prefix: &str,
2354) -> ValidationResult {
2355 if let Some(size) = page_size {
2357 match validate_page_size(size, &format!("{}_page_size", field_prefix)) {
2358 ValidationResult::Valid => {}
2359 ValidationResult::Warning(msg) => {
2360 log::warn!("Pagination warning: {}", msg);
2361 }
2362 ValidationResult::Invalid(msg) => {
2363 return ValidationResult::Invalid(msg);
2364 }
2365 }
2366 }
2367
2368 if let Some(token) = page_token {
2370 match validate_page_token(token, &format!("{}_page_token", field_prefix)) {
2371 ValidationResult::Valid => {}
2372 ValidationResult::Warning(msg) => {
2373 log::warn!("Pagination warning: {}", msg);
2374 }
2375 ValidationResult::Invalid(msg) => {
2376 return ValidationResult::Invalid(msg);
2377 }
2378 }
2379 }
2380
2381 if page_token.is_some() && page_size.is_none() {
2383 log::warn!(
2384 "{}: page_token provided without page_size. Using default page size {}",
2385 field_prefix,
2386 pagination_limits::DEFAULT_PAGE_SIZE
2387 );
2388 }
2389
2390 ValidationResult::Valid
2391}
2392
2393pub fn sanitize_tag(tag: &str) -> String {
2401 tag.trim()
2402 .replace(['_', '-'], "_") .to_lowercase()
2404}
2405
2406pub mod sheets;
2408
2409pub mod im;
2411
2412pub mod hire;
2414
2415pub mod calendar;
2417pub mod drive;
2419
2420pub mod pagination;
2422
2423pub use pagination::{PaginatedResponse, PaginationIterator, PaginationRequestBuilder};
2425
2426pub use sheets::{
2428 validate_cell_range, validate_data_matrix_consistency, validate_date_time_render_option,
2429 validate_find_options, validate_merge_range, validate_value_render_option,
2430};
2431
2432pub use im::{
2434 validate_file_upload, validate_message_content, validate_message_forward,
2435 validate_message_reaction, validate_message_read_status, validate_message_recall,
2436 validate_message_receivers, validate_message_template, validate_message_type,
2437 validate_receiver_id, validate_uuid, ValidateImBuilder,
2438};
2439
2440pub use hire::{
2442 validate_birthday as validate_hire_birthday, validate_candidate_basic_info,
2443 validate_candidate_tags, validate_education_background, validate_hiring_requirement,
2444 validate_hiring_status_transition, validate_interview_arrangement, validate_interview_feedback,
2445 validate_job_position, validate_offer_info, validate_salary_range,
2446 validate_work_experience as validate_hire_work_experience, ValidateHireBuilder,
2447};