1use crate::domain::KeyDomain;
8use crate::error::KeyParseError;
9use crate::key::Key;
10
11#[cfg(not(feature = "std"))]
12use alloc::format;
13#[cfg(not(feature = "std"))]
14use alloc::string::{String, ToString};
15#[cfg(not(feature = "std"))]
16use alloc::vec;
17#[cfg(not(feature = "std"))]
18use alloc::vec::Vec;
19
20use core::fmt::Write;
21
22const fn is_allowed_key_byte_default(b: u8) -> bool {
29 matches!(b, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'_' | b'-' | b'.')
30}
31
32const fn is_separator_byte_default(b: u8) -> bool {
35 matches!(b, b'_' | b'-' | b'.')
36}
37
38const fn is_allowed_end_byte_default(b: u8) -> bool {
44 matches!(b, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z')
45}
46
47#[must_use]
100pub const fn is_valid_key_default(s: &str, max_length: usize) -> bool {
101 let bytes = s.as_bytes();
102 let len = bytes.len();
103
104 if len == 0 {
106 return false;
107 }
108
109 if len > max_length {
111 return false;
112 }
113
114 let mut i = 0;
123 while i < len {
124 let b = bytes[i];
125
126 if !is_allowed_key_byte_default(b) {
127 return false;
128 }
129
130 if i > 0 && is_separator_byte_default(b) && bytes[i - 1] == b {
132 return false;
133 }
134
135 i += 1;
136 }
137
138 if !is_allowed_end_byte_default(bytes[len - 1]) {
141 return false;
142 }
143
144 true
145}
146
147#[inline]
172#[must_use]
173pub fn is_valid_key<T: KeyDomain>(key: &str) -> bool {
174 validate_key::<T>(key).is_ok()
175}
176
177pub fn validate_key<T: KeyDomain>(key: &str) -> Result<(), KeyParseError> {
205 Key::<T>::new(key).map(|_| ())
206}
207
208#[must_use]
234pub fn validation_help<T: KeyDomain>() -> Option<&'static str> {
235 T::validation_help()
236}
237
238#[must_use]
264pub fn validation_info<T: KeyDomain>() -> String {
265 let mut info = format!("Domain: {}\n", T::DOMAIN_NAME);
266 writeln!(info, "Max length: {}", T::MAX_LENGTH).unwrap();
267 writeln!(info, "Min length: {}", T::min_length()).unwrap();
268 writeln!(info, "Expected length: {}", T::EXPECTED_LENGTH).unwrap();
269 writeln!(info, "Case insensitive: {}", T::CASE_INSENSITIVE).unwrap();
270 writeln!(info, "Custom validation: {}", T::HAS_CUSTOM_VALIDATION).unwrap();
271 writeln!(
272 info,
273 "Custom normalization: {}",
274 T::HAS_CUSTOM_NORMALIZATION,
275 )
276 .unwrap();
277
278 writeln!(info, "Default separator: '{}'", T::default_separator()).unwrap();
279
280 if let Some(help) = T::validation_help() {
281 info.push_str("Help: ");
282 info.push_str(help);
283 info.push('\n');
284 }
285
286 let examples = T::examples();
287 if !examples.is_empty() {
288 info.push_str("Examples: ");
289 for (i, example) in examples.iter().enumerate() {
290 if i > 0 {
291 info.push_str(", ");
292 }
293 info.push_str(example);
294 }
295 info.push('\n');
296 }
297
298 info
299}
300
301pub fn validate_batch<T: KeyDomain, I>(keys: I) -> (Vec<String>, Vec<(String, KeyParseError)>)
335where
336 I: IntoIterator,
337 I::Item: AsRef<str>,
338{
339 let mut valid = Vec::new();
340 let mut invalid = Vec::new();
341
342 for key in keys {
343 let key_str = key.as_ref();
344 match validate_key::<T>(key_str) {
345 Ok(()) => valid.push(key_str.to_string()),
346 Err(e) => invalid.push((key_str.to_string(), e)),
347 }
348 }
349
350 (valid, invalid)
351}
352
353pub fn filter_valid<T: KeyDomain, I>(keys: I) -> impl Iterator<Item = I::Item>
376where
377 I: IntoIterator,
378 I::Item: AsRef<str>,
379{
380 keys.into_iter()
381 .filter(|key| is_valid_key::<T>(key.as_ref()))
382}
383
384pub fn count_valid<T: KeyDomain, I>(keys: I) -> usize
406where
407 I: IntoIterator,
408 I::Item: AsRef<str>,
409{
410 keys.into_iter()
411 .filter(|key| is_valid_key::<T>(key.as_ref()))
412 .count()
413}
414
415pub fn all_valid<T: KeyDomain, I>(keys: I) -> bool
438where
439 I: IntoIterator,
440 I::Item: AsRef<str>,
441{
442 keys.into_iter().all(|key| is_valid_key::<T>(key.as_ref()))
443}
444
445pub fn any_valid<T: KeyDomain, I>(keys: I) -> bool
468where
469 I: IntoIterator,
470 I::Item: AsRef<str>,
471{
472 keys.into_iter().any(|key| is_valid_key::<T>(key.as_ref()))
473}
474
475pub trait IntoKey<T: KeyDomain> {
484 fn into_key(self) -> Result<Key<T>, KeyParseError>;
490
491 fn try_into_key(self) -> Option<Key<T>>;
496}
497
498impl<T: KeyDomain> IntoKey<T> for &str {
499 #[inline]
500 fn into_key(self) -> Result<Key<T>, KeyParseError> {
501 Key::new(self)
502 }
503
504 #[inline]
505 fn try_into_key(self) -> Option<Key<T>> {
506 Key::try_new(self)
507 }
508}
509
510impl<T: KeyDomain> IntoKey<T> for String {
511 #[inline]
512 fn into_key(self) -> Result<Key<T>, KeyParseError> {
513 Key::from_string(self)
514 }
515
516 #[inline]
517 fn try_into_key(self) -> Option<Key<T>> {
518 Key::from_string(self).ok()
519 }
520}
521
522impl<T: KeyDomain> IntoKey<T> for &String {
523 #[inline]
524 fn into_key(self) -> Result<Key<T>, KeyParseError> {
525 Key::new(self)
526 }
527
528 #[inline]
529 fn try_into_key(self) -> Option<Key<T>> {
530 Key::try_new(self)
531 }
532}
533
534type ValidatorFunction = fn(&str) -> Result<(), KeyParseError>;
535
536#[derive(Debug)]
545pub struct ValidationBuilder<T: KeyDomain> {
546 allow_empty_collection: bool,
547 max_failures: Option<usize>,
548 stop_on_first_error: bool,
549 custom_validator: Option<ValidatorFunction>,
550 _phantom: core::marker::PhantomData<T>,
551}
552
553impl<T: KeyDomain> Default for ValidationBuilder<T> {
554 fn default() -> Self {
555 Self::new()
556 }
557}
558
559impl<T: KeyDomain> ValidationBuilder<T> {
560 #[must_use]
562 pub fn new() -> Self {
563 Self {
564 allow_empty_collection: false,
565 max_failures: None,
566 stop_on_first_error: false,
567 custom_validator: None,
568 _phantom: core::marker::PhantomData,
569 }
570 }
571
572 #[must_use]
574 pub fn allow_empty_collection(mut self, allow: bool) -> Self {
575 self.allow_empty_collection = allow;
576 self
577 }
578
579 #[must_use]
581 pub fn max_failures(mut self, max: usize) -> Self {
582 self.max_failures = Some(max);
583 self
584 }
585
586 #[must_use]
588 pub fn stop_on_first_error(mut self, stop: bool) -> Self {
589 self.stop_on_first_error = stop;
590 self
591 }
592
593 #[must_use]
595 pub fn custom_validator(mut self, validator: ValidatorFunction) -> Self {
596 self.custom_validator = Some(validator);
597 self
598 }
599
600 pub fn validate<I>(&self, keys: I) -> ValidationResult
602 where
603 I: IntoIterator,
604 I::Item: AsRef<str>,
605 {
606 let mut valid = Vec::new();
607 let mut errors = Vec::new();
608 let mut keys = keys.into_iter().peekable();
609
610 if keys.peek().is_none() && !self.allow_empty_collection {
611 return ValidationResult {
612 valid,
613 errors: vec![(String::new(), KeyParseError::Empty)],
614 total_processed: 0,
615 };
616 }
617
618 for key in keys {
619 let key_str = key.as_ref();
620
621 if let Some(max) = self.max_failures {
623 if errors.len() >= max {
624 break;
625 }
626 }
627
628 if self.stop_on_first_error && !errors.is_empty() {
629 break;
630 }
631
632 match validate_key::<T>(key_str) {
634 Ok(()) => {
635 let normalized = Key::<T>::normalize(key_str);
637 if let Some(custom) = self.custom_validator {
638 match custom(&normalized) {
639 Ok(()) => valid.push(normalized.into_owned()),
640 Err(e) => errors.push((normalized.into_owned(), e)),
641 }
642 } else {
643 valid.push(normalized.into_owned());
644 }
645 }
646 Err(e) => errors.push((key_str.to_string(), e)),
647 }
648 }
649
650 ValidationResult {
651 total_processed: valid.len() + errors.len(),
652 valid,
653 errors,
654 }
655 }
656}
657
658#[derive(Debug, Clone, PartialEq, Eq)]
660pub struct ValidationResult {
661 pub total_processed: usize,
663 pub valid: Vec<String>,
665 pub errors: Vec<(String, KeyParseError)>,
667}
668
669impl ValidationResult {
670 #[inline]
672 #[must_use]
673 pub fn is_success(&self) -> bool {
674 self.errors.is_empty()
675 }
676
677 #[inline]
679 #[must_use]
680 pub fn valid_count(&self) -> usize {
681 self.valid.len()
682 }
683
684 #[inline]
686 #[must_use]
687 pub fn error_count(&self) -> usize {
688 self.errors.len()
689 }
690
691 #[must_use]
693 pub fn success_rate(&self) -> f64 {
694 if self.total_processed == 0 {
695 0.0
696 } else {
697 #[expect(
698 clippy::cast_precision_loss,
699 reason = "total_processed fits comfortably in f64; precision loss only occurs above 2^53 items"
700 )]
701 let valid_ratio = self.valid.len() as f64 / self.total_processed as f64;
702 valid_ratio * 100.0
703 }
704 }
705
706 pub fn into_keys<T: KeyDomain>(self) -> Result<Vec<Key<T>>, KeyParseError> {
712 self.valid
713 .into_iter()
714 .map(|s| Key::from_string(s))
715 .collect()
716 }
717
718 #[must_use]
720 pub fn try_into_keys<T: KeyDomain>(self) -> Vec<Key<T>> {
721 self.valid
722 .into_iter()
723 .filter_map(|s| Key::from_string(s).ok())
724 .collect()
725 }
726}
727
728#[must_use]
734pub fn strict_validator<T: KeyDomain>() -> ValidationBuilder<T> {
735 ValidationBuilder::new()
736 .stop_on_first_error(true)
737 .allow_empty_collection(false)
738}
739
740#[must_use]
742pub fn lenient_validator<T: KeyDomain>() -> ValidationBuilder<T> {
743 ValidationBuilder::new()
744 .stop_on_first_error(false)
745 .allow_empty_collection(true)
746}
747
748pub fn quick_convert<T: KeyDomain, I>(keys: I) -> Result<Vec<Key<T>>, Vec<(String, KeyParseError)>>
775where
776 I: IntoIterator,
777 I::Item: AsRef<str>,
778{
779 let mut valid = Vec::new();
780 let mut errors = Vec::new();
781
782 for key in keys {
783 let key_str = key.as_ref().to_string();
784 match Key::<T>::from_string(key_str.clone()) {
785 Ok(k) => valid.push(k),
786 Err(e) => errors.push((key_str, e)),
787 }
788 }
789
790 if errors.is_empty() {
791 Ok(valid)
792 } else {
793 Err(errors)
794 }
795}
796
797#[cfg(test)]
802mod tests {
803 use super::*;
804
805 #[derive(Debug)]
807 struct TestDomain;
808
809 impl crate::Domain for TestDomain {
810 const DOMAIN_NAME: &'static str = "test";
811 }
812
813 impl KeyDomain for TestDomain {
814 const MAX_LENGTH: usize = 32;
815
816 fn validation_help() -> Option<&'static str> {
817 Some("Test domain help")
818 }
819
820 fn examples() -> &'static [&'static str] {
821 &["example1", "example2"]
822 }
823 }
824
825 #[test]
826 fn is_valid_key_accepts_good_rejects_bad() {
827 assert!(is_valid_key::<TestDomain>("valid_key"));
828 assert!(!is_valid_key::<TestDomain>(""));
829 assert!(!is_valid_key::<TestDomain>("a".repeat(50).as_str()));
830 }
831
832 #[test]
833 fn validate_key_returns_error_for_empty() {
834 assert!(validate_key::<TestDomain>("valid_key").is_ok());
835 assert!(validate_key::<TestDomain>("").is_err());
836 }
837
838 #[test]
839 fn validation_info_contains_domain_details() {
840 let info = validation_info::<TestDomain>();
841 assert!(info.contains("Domain: test"));
842 assert!(info.contains("Max length: 32"));
843 assert!(info.contains("Help: Test domain help"));
844 assert!(info.contains("Examples: example1, example2"));
845 }
846
847 #[test]
848 fn validate_batch_separates_valid_and_invalid() {
849 let keys = vec!["valid1", "", "valid2", "bad key"];
850 let (valid, invalid) = validate_batch::<TestDomain, _>(&keys);
851
852 assert_eq!(valid.len(), 2);
853 assert_eq!(invalid.len(), 2);
854 assert!(valid.contains(&"valid1".to_string()));
855 assert!(valid.contains(&"valid2".to_string()));
856 }
857
858 #[test]
859 fn filter_valid_removes_bad_keys() {
860 let keys = vec!["valid1", "", "valid2", "bad key"];
861 let valid: Vec<_> = filter_valid::<TestDomain, _>(&keys).collect();
862
863 assert_eq!(valid.len(), 2);
864 assert!(valid.contains(&&"valid1"));
865 assert!(valid.contains(&&"valid2"));
866 }
867
868 #[test]
869 fn count_valid_matches_filter_length() {
870 let keys = vec!["valid1", "", "valid2", "bad key"];
871 let count = count_valid::<TestDomain, _>(&keys);
872 assert_eq!(count, 2);
873 }
874
875 #[test]
876 fn all_valid_true_only_when_all_pass() {
877 let all_valid_keys = vec!["valid1", "valid2"];
878 let mixed = vec!["valid1", "", "valid2"];
879
880 assert!(all_valid::<TestDomain, _>(&all_valid_keys));
881 assert!(!all_valid::<TestDomain, _>(&mixed));
882 }
883
884 #[test]
885 fn any_valid_true_when_at_least_one_passes() {
886 let mixed = vec!["", "valid1", ""];
887 let all_invalid = vec!["", ""];
888
889 assert!(any_valid::<TestDomain, _>(&mixed));
890 assert!(!any_valid::<TestDomain, _>(&all_invalid));
891 }
892
893 #[test]
894 fn into_key_converts_str_and_string() {
895 let key1: Key<TestDomain> = "test_key".into_key().unwrap();
896 let key2: Key<TestDomain> = "another_key".to_string().into_key().unwrap();
897
898 assert_eq!(key1.as_str(), "test_key");
899 assert_eq!(key2.as_str(), "another_key");
900
901 let invalid: Option<Key<TestDomain>> = "".try_into_key();
902 assert!(invalid.is_none());
903 }
904
905 #[test]
906 fn builder_respects_max_failures_limit() {
907 let builder = ValidationBuilder::<TestDomain>::new()
908 .allow_empty_collection(true)
909 .max_failures(2)
910 .stop_on_first_error(false);
911
912 let keys = vec!["valid1", "", "valid2", "", "valid3"];
913 let result = builder.validate(&keys);
914
915 #[cfg(feature = "std")]
917 {
918 println!("Total processed: {}", result.total_processed);
919 println!("Valid count: {}", result.valid_count());
920 println!("Error count: {}", result.error_count());
921 println!("Valid keys: {:?}", result.valid);
922 println!("Errors: {:?}", result.errors);
923 }
924
925 assert_eq!(result.valid_count(), 2); assert_eq!(result.error_count(), 2); assert!(!result.is_success()); assert_eq!(result.total_processed, 4); assert!(result.success_rate() > 40.0 && result.success_rate() <= 60.0); }
940
941 #[test]
942 fn builder_stops_on_first_error_when_configured() {
943 let builder = ValidationBuilder::<TestDomain>::new()
944 .stop_on_first_error(true)
945 .allow_empty_collection(false);
946
947 let keys = vec!["valid", "", "another"];
948 let result = builder.validate(&keys);
949
950 assert_eq!(result.total_processed, 2); assert_eq!(result.valid_count(), 1);
953 assert_eq!(result.error_count(), 1);
954 }
955
956 #[test]
957 fn builder_processes_all_when_not_stopping_on_error() {
958 let builder = ValidationBuilder::<TestDomain>::new()
959 .stop_on_first_error(false)
960 .allow_empty_collection(true);
961
962 let keys = vec!["valid", "", "another"];
963 let result = builder.validate(&keys);
964
965 assert_eq!(result.total_processed, 3);
967 assert_eq!(result.valid_count(), 2);
968 assert_eq!(result.error_count(), 1);
969 }
970
971 #[test]
972 fn validation_result_computes_success_rate() {
973 const EPSILON: f64 = 1e-10;
974 let keys = vec!["valid1", "valid2"];
975 let (valid, errors) = validate_batch::<TestDomain, _>(keys);
976
977 let result = ValidationResult {
978 total_processed: valid.len() + errors.len(),
979 valid,
980 errors,
981 };
982
983 assert!(result.is_success());
984 assert_eq!(result.valid_count(), 2);
985 assert_eq!(result.error_count(), 0);
986
987 assert!((result.success_rate() - 100.0).abs() < EPSILON);
988
989 let keys = result.try_into_keys::<TestDomain>();
990 assert_eq!(keys.len(), 2);
991 }
992
993 #[test]
994 fn strict_validator_stops_on_first_error() {
995 let validator = strict_validator::<TestDomain>();
996 let keys = vec!["valid", "", "another"];
997 let result = validator.validate(&keys);
998
999 assert_eq!(result.total_processed, 2); assert_eq!(result.valid_count(), 1);
1002 assert_eq!(result.error_count(), 1);
1003 }
1004
1005 #[test]
1006 fn lenient_validator_processes_all_items() {
1007 let validator = lenient_validator::<TestDomain>();
1008 let keys = vec!["valid", "", "another"];
1009 let result = validator.validate(&keys);
1010
1011 assert_eq!(result.total_processed, 3);
1013 assert_eq!(result.valid_count(), 2);
1014 assert_eq!(result.error_count(), 1);
1015 }
1016
1017 #[test]
1018 fn quick_convert_succeeds_or_returns_errors() {
1019 let strings = vec!["key1", "key2", "key3"];
1020 let keys = quick_convert::<TestDomain, _>(&strings).unwrap();
1021 assert_eq!(keys.len(), 3);
1022
1023 let mixed = vec!["key1", "", "key2"];
1024 let result = quick_convert::<TestDomain, _>(&mixed);
1025 assert!(result.is_err());
1026 }
1027
1028 #[test]
1029 fn custom_validator_applies_extra_check() {
1030 fn custom_check(key: &str) -> Result<(), KeyParseError> {
1031 if key.starts_with("custom_") {
1032 Ok(())
1033 } else {
1034 Err(KeyParseError::custom(9999, "Must start with custom_"))
1035 }
1036 }
1037
1038 let validator = ValidationBuilder::<TestDomain>::new().custom_validator(custom_check);
1039
1040 let keys = vec!["custom_key", "invalid_key"];
1041 let result = validator.validate(&keys);
1042
1043 assert_eq!(result.valid_count(), 1);
1044 assert_eq!(result.error_count(), 1);
1045 }
1046
1047 const _GOOD: () = assert!(is_valid_key_default("user_123", 64));
1053 const _EMPTY: () = assert!(!is_valid_key_default("", 64));
1054 const _TOO_LONG: () = assert!(!is_valid_key_default("abcdefgh", 4));
1055 const _TRAILING_SEP: () = assert!(!is_valid_key_default("foo_", 64));
1056 const _CONSECUTIVE: () = assert!(!is_valid_key_default("a__b", 64));
1057 const _WITH_SPACE: () = assert!(!is_valid_key_default("hello world", 64));
1058 const _LEADING_SEP: () = assert!(is_valid_key_default("_foo", 64));
1059 const _HYPHEN_MID: () = assert!(is_valid_key_default("foo-bar", 64));
1060 const _DOT_MID: () = assert!(is_valid_key_default("foo.bar", 64));
1061 const _CONSEC_HYPHEN: () = assert!(!is_valid_key_default("foo--bar", 64));
1062 const _CONSEC_DOT: () = assert!(!is_valid_key_default("foo..bar", 64));
1063 const _NON_ASCII: () = assert!(!is_valid_key_default("héllo", 64));
1064 const _UPPERCASE: () = assert!(is_valid_key_default("FooBar", 64));
1065 const _DIGITS_ONLY: () = assert!(is_valid_key_default("12345", 64));
1066 const _EXACT_MAX: () = assert!(is_valid_key_default("ab", 2));
1067 const _OVER_MAX: () = assert!(!is_valid_key_default("abc", 2));
1068
1069 #[test]
1070 fn is_valid_key_default_matches_runtime_for_valid_keys() {
1071 assert!(is_valid_key_default("hello", 64));
1073 assert!(is_valid_key_default("user_name", 64));
1074 assert!(is_valid_key_default("foo-bar.baz", 64));
1075 assert!(is_valid_key_default("ABC123", 64));
1076 }
1077
1078 #[test]
1079 fn is_valid_key_default_rejects_all_bad_patterns() {
1080 assert!(!is_valid_key_default("", 64));
1081 assert!(!is_valid_key_default("trailing_", 64));
1082 assert!(!is_valid_key_default("trailing-", 64));
1083 assert!(!is_valid_key_default("trailing.", 64));
1084 assert!(!is_valid_key_default("a__b", 64));
1085 assert!(!is_valid_key_default("a--b", 64));
1086 assert!(!is_valid_key_default("a..b", 64));
1087 assert!(!is_valid_key_default("has space", 64));
1088 assert!(!is_valid_key_default("has\ttab", 64));
1089 assert!(!is_valid_key_default("a@b", 64));
1090 assert!(!is_valid_key_default("a!b", 64));
1091 }
1092
1093 #[test]
1094 fn is_valid_key_default_respects_max_length() {
1095 let exactly_max = "a".repeat(32);
1096 let over_max = "a".repeat(33);
1097 assert!(is_valid_key_default(&exactly_max, 32));
1098 assert!(!is_valid_key_default(&over_max, 32));
1099 }
1100}