1use serde::{Deserialize, Serialize};
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
43#[non_exhaustive]
44pub enum IDPolicy {
45 #[serde(rename = "uuid")]
47 #[default]
48 UUID,
49
50 #[serde(rename = "opaque")]
52 OPAQUE,
53}
54
55impl IDPolicy {
56 #[must_use]
58 pub fn enforces_uuid(self) -> bool {
59 self == Self::UUID
60 }
61
62 #[must_use]
64 pub const fn as_str(self) -> &'static str {
65 match self {
66 Self::UUID => "uuid",
67 Self::OPAQUE => "opaque",
68 }
69 }
70}
71
72impl std::fmt::Display for IDPolicy {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 write!(f, "{}", self.as_str())
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct IDValidationError {
81 pub value: String,
83 pub policy: IDPolicy,
85 pub message: String,
87}
88
89impl std::fmt::Display for IDValidationError {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 write!(f, "{}", self.message)
92 }
93}
94
95impl std::error::Error for IDValidationError {}
96
97pub fn validate_id(id: &str, policy: IDPolicy) -> Result<(), IDValidationError> {
144 match policy {
145 IDPolicy::UUID => validate_uuid_format(id),
146 IDPolicy::OPAQUE => Ok(()), }
148}
149
150fn validate_uuid_format(id: &str) -> Result<(), IDValidationError> {
169 if id.len() != 36 {
171 return Err(IDValidationError {
172 value: id.to_string(),
173 policy: IDPolicy::UUID,
174 message: format!(
175 "ID must be a valid UUID (36 characters), got {} characters",
176 id.len()
177 ),
178 });
179 }
180
181 let parts: Vec<&str> = id.split('-').collect();
183 if parts.len() != 5 {
184 return Err(IDValidationError {
185 value: id.to_string(),
186 policy: IDPolicy::UUID,
187 message: "ID must be a valid UUID with format XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
188 .to_string(),
189 });
190 }
191
192 let expected_lengths = [8, 4, 4, 4, 12];
194 for (i, (part, &expected_len)) in parts.iter().zip(&expected_lengths).enumerate() {
195 if part.len() != expected_len {
196 return Err(IDValidationError {
197 value: id.to_string(),
198 policy: IDPolicy::UUID,
199 message: format!(
200 "UUID segment {} has invalid length: expected {}, got {}",
201 i,
202 expected_len,
203 part.len()
204 ),
205 });
206 }
207 }
208
209 for (i, part) in parts.iter().enumerate() {
211 if !part.chars().all(|c| c.is_ascii_hexdigit()) {
212 return Err(IDValidationError {
213 value: id.to_string(),
214 policy: IDPolicy::UUID,
215 message: format!("UUID segment {i} contains non-hexadecimal characters: '{part}'"),
216 });
217 }
218 }
219
220 Ok(())
221}
222
223#[allow(dead_code)] pub fn validate_ids(ids: &[&str], policy: IDPolicy) -> Result<(), IDValidationError> {
255 for id in ids {
256 validate_id(id, policy)?;
257 }
258 Ok(())
259}
260
261pub trait IdValidator: Send + Sync {
281 fn validate(&self, value: &str) -> Result<(), IDValidationError>;
283
284 fn format_name(&self) -> &'static str;
286}
287
288#[derive(Debug, Clone, Copy)]
290pub struct UuidIdValidator;
291
292impl IdValidator for UuidIdValidator {
293 fn validate(&self, value: &str) -> Result<(), IDValidationError> {
294 validate_uuid_format(value)
295 }
296
297 fn format_name(&self) -> &'static str {
298 "UUID"
299 }
300}
301
302#[derive(Debug, Clone, Copy)]
304pub struct NumericIdValidator;
305
306impl IdValidator for NumericIdValidator {
307 fn validate(&self, value: &str) -> Result<(), IDValidationError> {
308 value.parse::<i64>().map_err(|_| IDValidationError {
309 value: value.to_string(),
310 policy: IDPolicy::OPAQUE,
311 message: format!(
312 "ID must be a valid {} (parseable as 64-bit integer)",
313 self.format_name()
314 ),
315 })?;
316 Ok(())
317 }
318
319 fn format_name(&self) -> &'static str {
320 "integer"
321 }
322}
323
324#[derive(Debug, Clone, Copy)]
329pub struct UlidIdValidator;
330
331impl IdValidator for UlidIdValidator {
332 fn validate(&self, value: &str) -> Result<(), IDValidationError> {
333 if value.len() != 26 {
334 return Err(IDValidationError {
335 value: value.to_string(),
336 policy: IDPolicy::OPAQUE,
337 message: format!(
338 "ID must be a valid {} ({} characters), got {}",
339 self.format_name(),
340 26,
341 value.len()
342 ),
343 });
344 }
345
346 if !value.chars().all(|c| {
348 c.is_ascii_digit()
349 || (c.is_ascii_uppercase() && c != 'I' && c != 'L' && c != 'O' && c != 'U')
350 }) {
351 return Err(IDValidationError {
352 value: value.to_string(),
353 policy: IDPolicy::OPAQUE,
354 message: format!(
355 "ID must be a valid {} (Crockford base32: 0-9, A-Z except I, L, O, U)",
356 self.format_name()
357 ),
358 });
359 }
360
361 Ok(())
362 }
363
364 fn format_name(&self) -> &'static str {
365 "ULID"
366 }
367}
368
369#[derive(Debug, Clone, Copy)]
371pub struct OpaqueIdValidator;
372
373impl IdValidator for OpaqueIdValidator {
374 fn validate(&self, _value: &str) -> Result<(), IDValidationError> {
375 Ok(()) }
377
378 fn format_name(&self) -> &'static str {
379 "opaque"
380 }
381}
382
383#[derive(Debug, Clone)]
395pub struct IDValidationProfile {
396 pub name: String,
398
399 pub validator: ValidationProfileType,
401}
402
403#[derive(Debug, Clone)]
405#[non_exhaustive]
406pub enum ValidationProfileType {
407 Uuid(UuidIdValidator),
409
410 Numeric(NumericIdValidator),
412
413 Ulid(UlidIdValidator),
415
416 Opaque(OpaqueIdValidator),
418}
419
420impl ValidationProfileType {
421 pub fn as_validator(&self) -> &dyn IdValidator {
423 match self {
424 Self::Uuid(v) => v,
425 Self::Numeric(v) => v,
426 Self::Ulid(v) => v,
427 Self::Opaque(v) => v,
428 }
429 }
430}
431
432impl IDValidationProfile {
433 #[must_use]
435 pub fn uuid() -> Self {
436 Self {
437 name: "uuid".to_string(),
438 validator: ValidationProfileType::Uuid(UuidIdValidator),
439 }
440 }
441
442 #[must_use]
444 pub fn numeric() -> Self {
445 Self {
446 name: "numeric".to_string(),
447 validator: ValidationProfileType::Numeric(NumericIdValidator),
448 }
449 }
450
451 #[must_use]
453 pub fn ulid() -> Self {
454 Self {
455 name: "ulid".to_string(),
456 validator: ValidationProfileType::Ulid(UlidIdValidator),
457 }
458 }
459
460 #[must_use]
462 pub fn opaque() -> Self {
463 Self {
464 name: "opaque".to_string(),
465 validator: ValidationProfileType::Opaque(OpaqueIdValidator),
466 }
467 }
468
469 #[must_use]
480 pub fn by_name(name: &str) -> Option<Self> {
481 match name.to_lowercase().as_str() {
482 "uuid" => Some(Self::uuid()),
483 "numeric" | "integer" => Some(Self::numeric()),
484 "ulid" => Some(Self::ulid()),
485 "opaque" | "string" => Some(Self::opaque()),
486 _ => None,
487 }
488 }
489
490 pub fn validate(&self, value: &str) -> Result<(), IDValidationError> {
497 self.validator.as_validator().validate(value)
498 }
499}
500
501#[cfg(test)]
502mod tests {
503 #![allow(clippy::unwrap_used)] use super::*;
506
507 #[test]
510 fn test_validate_valid_uuid() {
511 let result = validate_id("550e8400-e29b-41d4-a716-446655440000", IDPolicy::UUID);
513 result.unwrap_or_else(|e| panic!("valid UUID should pass: {e}"));
514 }
515
516 #[test]
517 fn test_validate_valid_uuid_uppercase() {
518 let result = validate_id("550E8400-E29B-41D4-A716-446655440000", IDPolicy::UUID);
520 result.unwrap_or_else(|e| panic!("uppercase UUID should pass: {e}"));
521 }
522
523 #[test]
524 fn test_validate_valid_uuid_mixed_case() {
525 let result = validate_id("550e8400-E29b-41d4-A716-446655440000", IDPolicy::UUID);
526 result.unwrap_or_else(|e| panic!("mixed-case UUID should pass: {e}"));
527 }
528
529 #[test]
530 fn test_validate_nil_uuid() {
531 let result = validate_id("00000000-0000-0000-0000-000000000000", IDPolicy::UUID);
533 result.unwrap_or_else(|e| panic!("nil UUID should pass: {e}"));
534 }
535
536 #[test]
537 fn test_validate_max_uuid() {
538 let result = validate_id("ffffffff-ffff-ffff-ffff-ffffffffffff", IDPolicy::UUID);
540 result.unwrap_or_else(|e| panic!("max UUID should pass: {e}"));
541 }
542
543 #[test]
544 fn test_validate_uuid_wrong_length() {
545 let result = validate_id("550e8400-e29b-41d4-a716", IDPolicy::UUID);
546 assert!(
547 matches!(
548 result,
549 Err(IDValidationError {
550 policy: IDPolicy::UUID,
551 ..
552 })
553 ),
554 "short UUID string should fail with Validation error, got: {result:?}"
555 );
556 let err = result.unwrap_err();
557 assert_eq!(err.policy, IDPolicy::UUID);
558 assert!(err.message.contains("36 characters"));
559 }
560
561 #[test]
562 fn test_validate_uuid_extra_chars() {
563 let result = validate_id("550e8400-e29b-41d4-a716-446655440000x", IDPolicy::UUID);
564 assert!(
565 matches!(
566 result,
567 Err(IDValidationError {
568 policy: IDPolicy::UUID,
569 ..
570 })
571 ),
572 "extra chars should fail UUID validation, got: {result:?}"
573 );
574 }
575
576 #[test]
577 fn test_validate_uuid_missing_hyphens() {
578 let result = validate_id("550e8400e29b41d4a716446655440000", IDPolicy::UUID);
580 assert!(
581 matches!(
582 result,
583 Err(IDValidationError {
584 policy: IDPolicy::UUID,
585 ..
586 })
587 ),
588 "UUID without hyphens should fail, got: {result:?}"
589 );
590 let err = result.unwrap_err();
591 assert!(err.message.contains("36 characters"));
593 }
594
595 #[test]
596 fn test_validate_uuid_wrong_segment_lengths() {
597 let result = validate_id("550e840-e29b-41d4-a716-4466554400001", IDPolicy::UUID);
600 assert!(
601 matches!(
602 result,
603 Err(IDValidationError {
604 policy: IDPolicy::UUID,
605 ..
606 })
607 ),
608 "UUID with wrong segment lengths should fail, got: {result:?}"
609 );
610 let err = result.unwrap_err();
611 assert!(err.message.contains("segment"));
612 }
613
614 #[test]
615 fn test_validate_uuid_non_hex_chars() {
616 let result = validate_id("550e8400-e29b-41d4-a716-44665544000g", IDPolicy::UUID);
617 assert!(
618 matches!(
619 result,
620 Err(IDValidationError {
621 policy: IDPolicy::UUID,
622 ..
623 })
624 ),
625 "UUID with non-hex chars should fail, got: {result:?}"
626 );
627 let err = result.unwrap_err();
628 assert!(err.message.contains("non-hexadecimal"));
629 }
630
631 #[test]
632 fn test_validate_uuid_special_chars() {
633 let result = validate_id("550e8400-e29b-41d4-a716-4466554400@0", IDPolicy::UUID);
634 assert!(
635 matches!(
636 result,
637 Err(IDValidationError {
638 policy: IDPolicy::UUID,
639 ..
640 })
641 ),
642 "special chars should fail UUID validation, got: {result:?}"
643 );
644 }
645
646 #[test]
647 fn test_validate_uuid_empty_string() {
648 let result = validate_id("", IDPolicy::UUID);
649 assert!(
650 matches!(
651 result,
652 Err(IDValidationError {
653 policy: IDPolicy::UUID,
654 ..
655 })
656 ),
657 "empty string should fail UUID validation, got: {result:?}"
658 );
659 }
660
661 #[test]
664 fn test_opaque_accepts_any_string() {
665 validate_id("not-a-uuid", IDPolicy::OPAQUE)
666 .unwrap_or_else(|e| panic!("opaque should accept any string: {e}"));
667 validate_id("anything", IDPolicy::OPAQUE)
668 .unwrap_or_else(|e| panic!("opaque should accept any string: {e}"));
669 validate_id("12345", IDPolicy::OPAQUE)
670 .unwrap_or_else(|e| panic!("opaque should accept any string: {e}"));
671 validate_id("special@chars!#$%", IDPolicy::OPAQUE)
672 .unwrap_or_else(|e| panic!("opaque should accept any string: {e}"));
673 }
674
675 #[test]
676 fn test_opaque_accepts_empty_string() {
677 validate_id("", IDPolicy::OPAQUE)
678 .unwrap_or_else(|e| panic!("opaque should accept empty string: {e}"));
679 }
680
681 #[test]
682 fn test_opaque_accepts_uuid() {
683 validate_id("550e8400-e29b-41d4-a716-446655440000", IDPolicy::OPAQUE)
684 .unwrap_or_else(|e| panic!("opaque should accept UUID string: {e}"));
685 }
686
687 #[test]
690 fn test_validate_multiple_valid_uuids() {
691 let ids = vec![
692 "550e8400-e29b-41d4-a716-446655440000",
693 "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
694 "6ba7b811-9dad-11d1-80b4-00c04fd430c8",
695 ];
696 validate_ids(&ids, IDPolicy::UUID)
697 .unwrap_or_else(|e| panic!("all valid UUIDs should pass: {e}"));
698 }
699
700 #[test]
701 fn test_validate_multiple_fails_on_first_invalid() {
702 let ids = vec![
703 "550e8400-e29b-41d4-a716-446655440000",
704 "invalid-id",
705 "6ba7b811-9dad-11d1-80b4-00c04fd430c8",
706 ];
707 let result = validate_ids(&ids, IDPolicy::UUID);
708 assert!(
709 matches!(
710 result,
711 Err(IDValidationError {
712 policy: IDPolicy::UUID,
713 ..
714 })
715 ),
716 "batch with invalid ID should fail, got: {result:?}"
717 );
718 assert_eq!(result.unwrap_err().value, "invalid-id");
719 }
720
721 #[test]
722 fn test_validate_multiple_opaque_all_pass() {
723 let ids = vec!["anything", "goes", "here", "12345"];
724 validate_ids(&ids, IDPolicy::OPAQUE)
725 .unwrap_or_else(|e| panic!("opaque should accept all strings: {e}"));
726 }
727
728 #[test]
731 fn test_policy_enforces_uuid() {
732 assert!(IDPolicy::UUID.enforces_uuid());
733 assert!(!IDPolicy::OPAQUE.enforces_uuid());
734 }
735
736 #[test]
737 fn test_policy_as_str() {
738 assert_eq!(IDPolicy::UUID.as_str(), "uuid");
739 assert_eq!(IDPolicy::OPAQUE.as_str(), "opaque");
740 }
741
742 #[test]
743 fn test_policy_default() {
744 assert_eq!(IDPolicy::default(), IDPolicy::UUID);
745 }
746
747 #[test]
748 fn test_policy_display() {
749 assert_eq!(format!("{}", IDPolicy::UUID), "uuid");
750 assert_eq!(format!("{}", IDPolicy::OPAQUE), "opaque");
751 }
752
753 #[test]
756 fn test_security_prevent_sql_injection_via_uuid() {
757 let result = validate_id("'; DROP TABLE users; --", IDPolicy::UUID);
759 assert!(
760 matches!(
761 result,
762 Err(IDValidationError {
763 policy: IDPolicy::UUID,
764 ..
765 })
766 ),
767 "SQL injection string should fail UUID validation, got: {result:?}"
768 );
769 }
770
771 #[test]
772 fn test_security_prevent_path_traversal_via_uuid() {
773 let result = validate_id("../../etc/passwd", IDPolicy::UUID);
774 assert!(
775 matches!(
776 result,
777 Err(IDValidationError {
778 policy: IDPolicy::UUID,
779 ..
780 })
781 ),
782 "path traversal string should fail UUID validation, got: {result:?}"
783 );
784 }
785
786 #[test]
787 fn test_security_opaque_policy_accepts_any_format() {
788 validate_id("'; DROP TABLE users; --", IDPolicy::OPAQUE)
791 .unwrap_or_else(|e| panic!("opaque should accept SQL injection string: {e}"));
792 validate_id("../../etc/passwd", IDPolicy::OPAQUE)
793 .unwrap_or_else(|e| panic!("opaque should accept path traversal string: {e}"));
794 }
795
796 #[test]
797 fn test_validation_error_contains_policy_info() {
798 let err = validate_id("invalid", IDPolicy::UUID).unwrap_err();
799 assert_eq!(err.policy, IDPolicy::UUID);
800 assert_eq!(err.value, "invalid");
801 assert!(!err.message.is_empty());
802 }
803
804 #[test]
807 fn test_uuid_validator_valid() {
808 let validator = UuidIdValidator;
809 let result = validator.validate("550e8400-e29b-41d4-a716-446655440000");
810 result.unwrap_or_else(|e| panic!("valid UUID should pass UuidIdValidator: {e}"));
811 }
812
813 #[test]
814 fn test_uuid_validator_invalid() {
815 let validator = UuidIdValidator;
816 let result = validator.validate("not-a-uuid");
817 assert!(
818 matches!(
819 result,
820 Err(IDValidationError {
821 policy: IDPolicy::UUID,
822 ..
823 })
824 ),
825 "invalid string should fail UuidIdValidator, got: {result:?}"
826 );
827 assert_eq!(result.unwrap_err().value, "not-a-uuid");
828 }
829
830 #[test]
831 fn test_uuid_validator_format_name() {
832 let validator = UuidIdValidator;
833 assert_eq!(validator.format_name(), "UUID");
834 }
835
836 #[test]
837 fn test_uuid_validator_nil_uuid() {
838 let validator = UuidIdValidator;
839 validator
840 .validate("00000000-0000-0000-0000-000000000000")
841 .unwrap_or_else(|e| panic!("nil UUID should pass UuidIdValidator: {e}"));
842 }
843
844 #[test]
845 fn test_uuid_validator_uppercase() {
846 let validator = UuidIdValidator;
847 validator
848 .validate("550E8400-E29B-41D4-A716-446655440000")
849 .unwrap_or_else(|e| panic!("uppercase UUID should pass UuidIdValidator: {e}"));
850 }
851
852 #[test]
855 fn test_numeric_validator_valid_positive() {
856 let validator = NumericIdValidator;
857 validator
858 .validate("12345")
859 .unwrap_or_else(|e| panic!("positive int should pass: {e}"));
860 validator.validate("0").unwrap_or_else(|e| panic!("zero should pass: {e}"));
861 validator
862 .validate("9223372036854775807")
863 .unwrap_or_else(|e| panic!("i64::MAX should pass: {e}"));
864 }
865
866 #[test]
867 fn test_numeric_validator_valid_negative() {
868 let validator = NumericIdValidator;
869 validator
870 .validate("-1")
871 .unwrap_or_else(|e| panic!("negative int should pass: {e}"));
872 validator
873 .validate("-12345")
874 .unwrap_or_else(|e| panic!("negative int should pass: {e}"));
875 validator
876 .validate("-9223372036854775808")
877 .unwrap_or_else(|e| panic!("i64::MIN should pass: {e}"));
878 }
879
880 #[test]
881 fn test_numeric_validator_invalid_float() {
882 let validator = NumericIdValidator;
883 let result = validator.validate("123.45");
884 assert!(
885 matches!(result, Err(IDValidationError { .. })),
886 "float string should fail NumericIdValidator, got: {result:?}"
887 );
888 let err = result.unwrap_err();
889 assert_eq!(err.value, "123.45");
890 }
891
892 #[test]
893 fn test_numeric_validator_invalid_non_numeric() {
894 let validator = NumericIdValidator;
895 let result = validator.validate("abc123");
896 assert!(
897 matches!(result, Err(IDValidationError { .. })),
898 "non-numeric string should fail NumericIdValidator, got: {result:?}"
899 );
900 }
901
902 #[test]
903 fn test_numeric_validator_overflow() {
904 let validator = NumericIdValidator;
905 let result = validator.validate("9223372036854775808");
907 assert!(
908 matches!(result, Err(IDValidationError { .. })),
909 "i64 overflow should fail NumericIdValidator, got: {result:?}"
910 );
911 }
912
913 #[test]
914 fn test_numeric_validator_empty_string() {
915 let validator = NumericIdValidator;
916 let result = validator.validate("");
917 assert!(
918 matches!(result, Err(IDValidationError { .. })),
919 "empty string should fail NumericIdValidator, got: {result:?}"
920 );
921 }
922
923 #[test]
924 fn test_numeric_validator_format_name() {
925 let validator = NumericIdValidator;
926 assert_eq!(validator.format_name(), "integer");
927 }
928
929 #[test]
932 fn test_ulid_validator_valid() {
933 let validator = UlidIdValidator;
934 validator
936 .validate("01ARZ3NDEKTSV4RRFFQ69G5FAV")
937 .unwrap_or_else(|e| panic!("valid ULID should pass: {e}"));
938 }
939
940 #[test]
941 fn test_ulid_validator_valid_all_digits() {
942 let validator = UlidIdValidator;
943 validator
945 .validate("01234567890123456789012345")
946 .unwrap_or_else(|e| panic!("all-digit ULID should pass: {e}"));
947 }
948
949 #[test]
950 fn test_ulid_validator_valid_all_uppercase() {
951 let validator = UlidIdValidator;
952 validator
954 .validate("ABCDEFGHJKMNPQRSTVWXYZ0123")
955 .unwrap_or_else(|e| panic!("all-uppercase ULID should pass: {e}"));
956 }
957
958 #[test]
959 fn test_ulid_validator_invalid_length_short() {
960 let validator = UlidIdValidator;
961 let result = validator.validate("01ARZ3NDEKTSV4RRFFQ69G5F");
962 assert!(
963 matches!(result, Err(IDValidationError { .. })),
964 "short ULID should fail UlidIdValidator, got: {result:?}"
965 );
966 let err = result.unwrap_err();
967 assert!(err.message.contains("26 characters"));
968 }
969
970 #[test]
971 fn test_ulid_validator_invalid_length_long() {
972 let validator = UlidIdValidator;
973 let result = validator.validate("01ARZ3NDEKTSV4RRFFQ69G5FAVA");
974 assert!(
975 matches!(result, Err(IDValidationError { .. })),
976 "long ULID should fail UlidIdValidator, got: {result:?}"
977 );
978 let err = result.unwrap_err();
979 assert!(err.message.contains("26 characters"));
980 }
981
982 #[test]
983 fn test_ulid_validator_invalid_lowercase() {
984 let validator = UlidIdValidator;
985 let result = validator.validate("01arz3ndektsv4rrffq69g5fav");
986 assert!(
987 matches!(result, Err(IDValidationError { .. })),
988 "lowercase should fail UlidIdValidator, got: {result:?}"
989 );
990 }
991
992 #[test]
993 fn test_ulid_validator_invalid_char_i() {
994 let validator = UlidIdValidator;
995 let result = validator.validate("01ARZ3NDEKTSV4RRFFQ69G5FAI");
996 assert!(
997 matches!(result, Err(IDValidationError { .. })),
998 "char 'I' should fail UlidIdValidator, got: {result:?}"
999 );
1000 let err = result.unwrap_err();
1001 assert!(err.message.contains("Crockford base32"));
1002 }
1003
1004 #[test]
1005 fn test_ulid_validator_invalid_char_l() {
1006 let validator = UlidIdValidator;
1007 let result = validator.validate("01ARZ3NDEKTSV4RRFFQ69G5FAL");
1008 assert!(
1009 matches!(result, Err(IDValidationError { .. })),
1010 "char 'L' should fail UlidIdValidator, got: {result:?}"
1011 );
1012 }
1013
1014 #[test]
1015 fn test_ulid_validator_invalid_char_o() {
1016 let validator = UlidIdValidator;
1017 let result = validator.validate("01ARZ3NDEKTSV4RRFFQ69G5FAO");
1018 assert!(
1019 matches!(result, Err(IDValidationError { .. })),
1020 "char 'O' should fail UlidIdValidator, got: {result:?}"
1021 );
1022 }
1023
1024 #[test]
1025 fn test_ulid_validator_invalid_char_u() {
1026 let validator = UlidIdValidator;
1027 let result = validator.validate("01ARZ3NDEKTSV4RRFFQ69G5FAU");
1028 assert!(
1029 matches!(result, Err(IDValidationError { .. })),
1030 "char 'U' should fail UlidIdValidator, got: {result:?}"
1031 );
1032 }
1033
1034 #[test]
1035 fn test_ulid_validator_invalid_special_chars() {
1036 let validator = UlidIdValidator;
1037 let result = validator.validate("01ARZ3NDEKTSV4RRFFQ69G5FA-");
1038 assert!(
1039 matches!(result, Err(IDValidationError { .. })),
1040 "special char should fail UlidIdValidator, got: {result:?}"
1041 );
1042 }
1043
1044 #[test]
1045 fn test_ulid_validator_empty_string() {
1046 let validator = UlidIdValidator;
1047 let result = validator.validate("");
1048 assert!(
1049 matches!(result, Err(IDValidationError { .. })),
1050 "empty string should fail UlidIdValidator, got: {result:?}"
1051 );
1052 }
1053
1054 #[test]
1055 fn test_ulid_validator_format_name() {
1056 let validator = UlidIdValidator;
1057 assert_eq!(validator.format_name(), "ULID");
1058 }
1059
1060 #[test]
1063 fn test_opaque_validator_any_string() {
1064 let validator = OpaqueIdValidator;
1065 validator
1066 .validate("anything")
1067 .unwrap_or_else(|e| panic!("opaque should accept any string: {e}"));
1068 validator
1069 .validate("12345")
1070 .unwrap_or_else(|e| panic!("opaque should accept digits: {e}"));
1071 validator
1072 .validate("special@chars!#$%")
1073 .unwrap_or_else(|e| panic!("opaque should accept special chars: {e}"));
1074 validator
1075 .validate("")
1076 .unwrap_or_else(|e| panic!("opaque should accept empty string: {e}"));
1077 }
1078
1079 #[test]
1080 fn test_opaque_validator_malicious_strings() {
1081 let validator = OpaqueIdValidator;
1082 validator
1084 .validate("'; DROP TABLE users; --")
1085 .unwrap_or_else(|e| panic!("opaque should accept SQL injection: {e}"));
1086 validator
1087 .validate("../../etc/passwd")
1088 .unwrap_or_else(|e| panic!("opaque should accept path traversal: {e}"));
1089 validator
1090 .validate("<script>alert('xss')</script>")
1091 .unwrap_or_else(|e| panic!("opaque should accept XSS: {e}"));
1092 }
1093
1094 #[test]
1095 fn test_opaque_validator_uuid() {
1096 let validator = OpaqueIdValidator;
1097 validator
1098 .validate("550e8400-e29b-41d4-a716-446655440000")
1099 .unwrap_or_else(|e| panic!("opaque should accept UUID: {e}"));
1100 }
1101
1102 #[test]
1103 fn test_opaque_validator_format_name() {
1104 let validator = OpaqueIdValidator;
1105 assert_eq!(validator.format_name(), "opaque");
1106 }
1107
1108 #[test]
1111 fn test_validators_trait_object() {
1112 let validators: Vec<Box<dyn IdValidator>> = vec![
1113 Box::new(UuidIdValidator),
1114 Box::new(NumericIdValidator),
1115 Box::new(UlidIdValidator),
1116 Box::new(OpaqueIdValidator),
1117 ];
1118
1119 for validator in validators {
1120 let name = validator.format_name();
1122 assert!(!name.is_empty());
1123 }
1124 }
1125
1126 #[test]
1127 fn test_validator_selection_by_id_format() {
1128 let uuid = "550e8400-e29b-41d4-a716-446655440000";
1130 let numeric = "12345";
1131 let ulid = "01ARZ3NDEKTSV4RRFFQ69G5FAV";
1132
1133 let uuid_validator = UuidIdValidator;
1134 let numeric_validator = NumericIdValidator;
1135 let ulid_validator = UlidIdValidator;
1136
1137 uuid_validator
1138 .validate(uuid)
1139 .unwrap_or_else(|e| panic!("UUID validator should accept UUID: {e}"));
1140 numeric_validator
1141 .validate(numeric)
1142 .unwrap_or_else(|e| panic!("numeric validator should accept number: {e}"));
1143 ulid_validator
1144 .validate(ulid)
1145 .unwrap_or_else(|e| panic!("ULID validator should accept ULID: {e}"));
1146
1147 assert!(
1149 matches!(uuid_validator.validate(numeric), Err(IDValidationError { .. })),
1150 "UUID validator should reject numeric ID"
1151 );
1152 assert!(
1153 matches!(numeric_validator.validate(uuid), Err(IDValidationError { .. })),
1154 "numeric validator should reject UUID"
1155 );
1156 assert!(
1157 matches!(ulid_validator.validate(numeric), Err(IDValidationError { .. })),
1158 "ULID validator should reject numeric ID"
1159 );
1160 }
1161
1162 #[test]
1165 fn test_id_validation_profile_uuid() {
1166 let profile = IDValidationProfile::uuid();
1167 assert_eq!(profile.name, "uuid");
1168 profile
1169 .validate("550e8400-e29b-41d4-a716-446655440000")
1170 .unwrap_or_else(|e| panic!("UUID profile should accept valid UUID: {e}"));
1171 assert!(
1172 matches!(profile.validate("not-a-uuid"), Err(IDValidationError { .. })),
1173 "UUID profile should reject invalid string"
1174 );
1175 }
1176
1177 #[test]
1178 fn test_id_validation_profile_numeric() {
1179 let profile = IDValidationProfile::numeric();
1180 assert_eq!(profile.name, "numeric");
1181 profile
1182 .validate("12345")
1183 .unwrap_or_else(|e| panic!("numeric profile should accept number: {e}"));
1184 assert!(
1185 matches!(profile.validate("not-a-number"), Err(IDValidationError { .. })),
1186 "numeric profile should reject non-number"
1187 );
1188 }
1189
1190 #[test]
1191 fn test_id_validation_profile_ulid() {
1192 let profile = IDValidationProfile::ulid();
1193 assert_eq!(profile.name, "ulid");
1194 profile
1195 .validate("01ARZ3NDEKTSV4RRFFQ69G5FAV")
1196 .unwrap_or_else(|e| panic!("ULID profile should accept valid ULID: {e}"));
1197 assert!(
1198 matches!(profile.validate("not-a-ulid"), Err(IDValidationError { .. })),
1199 "ULID profile should reject invalid string"
1200 );
1201 }
1202
1203 #[test]
1204 fn test_id_validation_profile_opaque() {
1205 let profile = IDValidationProfile::opaque();
1206 assert_eq!(profile.name, "opaque");
1207 profile
1208 .validate("anything")
1209 .unwrap_or_else(|e| panic!("opaque profile should accept any string: {e}"));
1210 profile
1211 .validate("12345")
1212 .unwrap_or_else(|e| panic!("opaque profile should accept digits: {e}"));
1213 profile
1214 .validate("special@chars!#$%")
1215 .unwrap_or_else(|e| panic!("opaque profile should accept special chars: {e}"));
1216 }
1217
1218 #[test]
1219 fn test_id_validation_profile_by_name() {
1220 assert!(IDValidationProfile::by_name("uuid").is_some(), "uuid profile should exist");
1222 assert!(
1223 IDValidationProfile::by_name("numeric").is_some(),
1224 "numeric profile should exist"
1225 );
1226 assert!(IDValidationProfile::by_name("ulid").is_some(), "ulid profile should exist");
1227 assert!(IDValidationProfile::by_name("opaque").is_some(), "opaque profile should exist");
1228
1229 assert!(
1231 IDValidationProfile::by_name("UUID").is_some(),
1232 "UUID (uppercase) should resolve"
1233 );
1234 assert!(
1235 IDValidationProfile::by_name("NUMERIC").is_some(),
1236 "NUMERIC (uppercase) should resolve"
1237 );
1238 assert!(
1239 IDValidationProfile::by_name("ULID").is_some(),
1240 "ULID (uppercase) should resolve"
1241 );
1242
1243 assert!(
1245 IDValidationProfile::by_name("integer").is_some(),
1246 "integer alias should resolve"
1247 );
1248 assert!(IDValidationProfile::by_name("string").is_some(), "string alias should resolve");
1249
1250 assert!(
1252 IDValidationProfile::by_name("invalid").is_none(),
1253 "unknown name should return None"
1254 );
1255 }
1256
1257 #[test]
1258 fn test_id_validation_profile_by_name_uuid_validation() {
1259 let profile = IDValidationProfile::by_name("uuid").unwrap();
1260 assert_eq!(profile.name, "uuid");
1261 profile
1262 .validate("550e8400-e29b-41d4-a716-446655440000")
1263 .unwrap_or_else(|e| panic!("UUID profile by name should accept valid UUID: {e}"));
1264 }
1265
1266 #[test]
1267 fn test_id_validation_profile_by_name_numeric_validation() {
1268 let profile = IDValidationProfile::by_name("numeric").unwrap();
1269 assert_eq!(profile.name, "numeric");
1270 profile
1271 .validate("12345")
1272 .unwrap_or_else(|e| panic!("numeric profile by name should accept number: {e}"));
1273 }
1274
1275 #[test]
1276 fn test_id_validation_profile_by_name_integer_alias() {
1277 let profile_numeric = IDValidationProfile::by_name("numeric").unwrap();
1278 let profile_integer = IDValidationProfile::by_name("integer").unwrap();
1279
1280 profile_numeric
1282 .validate("12345")
1283 .unwrap_or_else(|e| panic!("numeric profile should accept number: {e}"));
1284 profile_integer
1285 .validate("12345")
1286 .unwrap_or_else(|e| panic!("integer alias should accept number: {e}"));
1287 assert!(
1288 matches!(profile_numeric.validate("not-a-number"), Err(IDValidationError { .. })),
1289 "numeric profile should reject non-number"
1290 );
1291 assert!(
1292 matches!(profile_integer.validate("not-a-number"), Err(IDValidationError { .. })),
1293 "integer alias should reject non-number"
1294 );
1295 }
1296
1297 #[test]
1298 fn test_id_validation_profile_by_name_string_alias() {
1299 let profile_opaque = IDValidationProfile::by_name("opaque").unwrap();
1300 let profile_string = IDValidationProfile::by_name("string").unwrap();
1301
1302 profile_opaque
1304 .validate("anything")
1305 .unwrap_or_else(|e| panic!("opaque profile should accept any string: {e}"));
1306 profile_string
1307 .validate("anything")
1308 .unwrap_or_else(|e| panic!("string alias should accept any string: {e}"));
1309 }
1310
1311 #[test]
1312 fn test_validation_profile_type_as_validator() {
1313 let uuid_type = ValidationProfileType::Uuid(UuidIdValidator);
1314 uuid_type
1315 .as_validator()
1316 .validate("550e8400-e29b-41d4-a716-446655440000")
1317 .unwrap_or_else(|e| panic!("UUID profile type should accept valid UUID: {e}"));
1318
1319 let numeric_type = ValidationProfileType::Numeric(NumericIdValidator);
1320 numeric_type
1321 .as_validator()
1322 .validate("12345")
1323 .unwrap_or_else(|e| panic!("numeric profile type should accept number: {e}"));
1324
1325 let ulid_type = ValidationProfileType::Ulid(UlidIdValidator);
1326 ulid_type
1327 .as_validator()
1328 .validate("01ARZ3NDEKTSV4RRFFQ69G5FAV")
1329 .unwrap_or_else(|e| panic!("ULID profile type should accept valid ULID: {e}"));
1330
1331 let opaque_type = ValidationProfileType::Opaque(OpaqueIdValidator);
1332 opaque_type
1333 .as_validator()
1334 .validate("any_value")
1335 .unwrap_or_else(|e| panic!("opaque profile type should accept any string: {e}"));
1336 }
1337
1338 #[test]
1339 fn test_id_validation_profile_clone() {
1340 let profile1 = IDValidationProfile::uuid();
1341 let profile2 = profile1.clone();
1342
1343 assert_eq!(profile1.name, profile2.name);
1344 profile1
1345 .validate("550e8400-e29b-41d4-a716-446655440000")
1346 .unwrap_or_else(|e| panic!("original profile should accept valid UUID: {e}"));
1347 profile2
1348 .validate("550e8400-e29b-41d4-a716-446655440000")
1349 .unwrap_or_else(|e| panic!("cloned profile should accept valid UUID: {e}"));
1350 }
1351
1352 #[test]
1353 fn test_all_profiles_available() {
1354 let profiles = [
1355 IDValidationProfile::uuid(),
1356 IDValidationProfile::numeric(),
1357 IDValidationProfile::ulid(),
1358 IDValidationProfile::opaque(),
1359 ];
1360
1361 assert_eq!(profiles.len(), 4);
1362 assert_eq!(profiles[0].name, "uuid");
1363 assert_eq!(profiles[1].name, "numeric");
1364 assert_eq!(profiles[2].name, "ulid");
1365 assert_eq!(profiles[3].name, "opaque");
1366 }
1367}