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
22#[must_use]
47pub fn is_valid_key<T: KeyDomain>(key: &str) -> bool {
48 validate_key::<T>(key).is_ok()
49}
50
51pub fn validate_key<T: KeyDomain>(key: &str) -> Result<(), KeyParseError> {
79 if key.trim().is_empty() {
80 return Err(KeyParseError::Empty);
81 }
82 let normalized = Key::<T>::normalize(key);
83 Key::<T>::validate_common(&normalized)?;
84 T::validate_domain_rules(&normalized)
85}
86
87#[must_use]
113pub fn validation_help<T: KeyDomain>() -> Option<&'static str> {
114 T::validation_help()
115}
116
117#[must_use]
143pub fn validation_info<T: KeyDomain>() -> String {
144 let mut info = format!("Domain: {}\n", T::DOMAIN_NAME);
145 writeln!(info, "Max length: {}", T::MAX_LENGTH).unwrap();
146 writeln!(info, "Min length: {}", T::min_length()).unwrap();
147 writeln!(info, "Expected length: {}", T::EXPECTED_LENGTH).unwrap();
148 writeln!(info, "Case insensitive: {}", T::CASE_INSENSITIVE).unwrap();
149 writeln!(info, "Custom validation: {}", T::HAS_CUSTOM_VALIDATION).unwrap();
150 writeln!(
151 info,
152 "Custom normalization: {}",
153 T::HAS_CUSTOM_NORMALIZATION,
154 )
155 .unwrap();
156
157 writeln!(info, "Default separator: '{}'", T::default_separator()).unwrap();
158
159 if let Some(help) = T::validation_help() {
160 info.push_str("Help: ");
161 info.push_str(help);
162 info.push('\n');
163 }
164
165 let examples = T::examples();
166 if !examples.is_empty() {
167 info.push_str("Examples: ");
168 for (i, example) in examples.iter().enumerate() {
169 if i > 0 {
170 info.push_str(", ");
171 }
172 info.push_str(example);
173 }
174 info.push('\n');
175 }
176
177 info
178}
179
180pub fn validate_batch<T: KeyDomain, I>(keys: I) -> (Vec<String>, Vec<(String, KeyParseError)>)
214where
215 I: IntoIterator,
216 I::Item: AsRef<str>,
217{
218 let mut valid = Vec::new();
219 let mut invalid = Vec::new();
220
221 for key in keys {
222 let key_str = key.as_ref();
223 match validate_key::<T>(key_str) {
224 Ok(()) => valid.push(key_str.to_string()),
225 Err(e) => invalid.push((key_str.to_string(), e)),
226 }
227 }
228
229 (valid, invalid)
230}
231
232pub fn filter_valid<T: KeyDomain, I>(keys: I) -> impl Iterator<Item = String>
255where
256 I: IntoIterator,
257 I::Item: AsRef<str>,
258{
259 keys.into_iter().filter_map(|key| {
260 let key_str = key.as_ref();
261 if is_valid_key::<T>(key_str) {
262 Some(key_str.to_string())
263 } else {
264 None
265 }
266 })
267}
268
269pub fn count_valid<T: KeyDomain, I>(keys: I) -> usize
291where
292 I: IntoIterator,
293 I::Item: AsRef<str>,
294{
295 keys.into_iter()
296 .filter(|key| is_valid_key::<T>(key.as_ref()))
297 .count()
298}
299
300pub fn all_valid<T: KeyDomain, I>(keys: I) -> bool
323where
324 I: IntoIterator,
325 I::Item: AsRef<str>,
326{
327 keys.into_iter().all(|key| is_valid_key::<T>(key.as_ref()))
328}
329
330pub fn any_valid<T: KeyDomain, I>(keys: I) -> bool
353where
354 I: IntoIterator,
355 I::Item: AsRef<str>,
356{
357 keys.into_iter().any(|key| is_valid_key::<T>(key.as_ref()))
358}
359
360pub trait IntoKey<T: KeyDomain> {
369 fn into_key(self) -> Result<Key<T>, KeyParseError>;
375
376 fn try_into_key(self) -> Option<Key<T>>;
381}
382
383impl<T: KeyDomain> IntoKey<T> for &str {
384 #[inline]
385 fn into_key(self) -> Result<Key<T>, KeyParseError> {
386 Key::new(self)
387 }
388
389 #[inline]
390 fn try_into_key(self) -> Option<Key<T>> {
391 Key::try_new(self)
392 }
393}
394
395impl<T: KeyDomain> IntoKey<T> for String {
396 #[inline]
397 fn into_key(self) -> Result<Key<T>, KeyParseError> {
398 Key::from_string(self)
399 }
400
401 #[inline]
402 fn try_into_key(self) -> Option<Key<T>> {
403 Key::from_string(self).ok()
404 }
405}
406
407impl<T: KeyDomain> IntoKey<T> for &String {
408 #[inline]
409 fn into_key(self) -> Result<Key<T>, KeyParseError> {
410 Key::new(self)
411 }
412
413 #[inline]
414 fn try_into_key(self) -> Option<Key<T>> {
415 Key::try_new(self)
416 }
417}
418
419type ValidatorFunction = fn(&str) -> Result<(), KeyParseError>;
420
421#[derive(Debug)]
430pub struct ValidationBuilder<T: KeyDomain> {
431 allow_empty_collection: bool,
432 max_failures: Option<usize>,
433 stop_on_first_error: bool,
434 custom_validator: Option<ValidatorFunction>,
435 _phantom: core::marker::PhantomData<T>,
436}
437
438impl<T: KeyDomain> Default for ValidationBuilder<T> {
439 fn default() -> Self {
440 Self::new()
441 }
442}
443
444impl<T: KeyDomain> ValidationBuilder<T> {
445 #[must_use]
447 pub fn new() -> Self {
448 Self {
449 allow_empty_collection: false,
450 max_failures: None,
451 stop_on_first_error: false,
452 custom_validator: None,
453 _phantom: core::marker::PhantomData,
454 }
455 }
456
457 #[must_use]
459 pub fn allow_empty_collection(mut self, allow: bool) -> Self {
460 self.allow_empty_collection = allow;
461 self
462 }
463
464 #[must_use]
466 pub fn max_failures(mut self, max: usize) -> Self {
467 self.max_failures = Some(max);
468 self
469 }
470
471 #[must_use]
473 pub fn stop_on_first_error(mut self, stop: bool) -> Self {
474 self.stop_on_first_error = stop;
475 self
476 }
477
478 #[must_use]
480 pub fn custom_validator(mut self, validator: ValidatorFunction) -> Self {
481 self.custom_validator = Some(validator);
482 self
483 }
484
485 pub fn validate<I>(&self, keys: I) -> ValidationResult
487 where
488 I: IntoIterator,
489 I::Item: AsRef<str>,
490 {
491 let mut valid = Vec::new();
492 let mut errors = Vec::new();
493 let mut keys = keys.into_iter().peekable();
494
495 if keys.peek().is_none() && !self.allow_empty_collection {
496 return ValidationResult {
497 valid,
498 errors: vec![(String::new(), KeyParseError::Empty)],
499 total_processed: 0,
500 };
501 }
502
503 for key in keys {
504 let key_str = key.as_ref();
505
506 if let Some(max) = self.max_failures {
508 if errors.len() >= max {
509 break;
510 }
511 }
512
513 if self.stop_on_first_error && !errors.is_empty() {
514 break;
515 }
516
517 match validate_key::<T>(key_str) {
519 Ok(()) => {
520 if let Some(custom) = self.custom_validator {
522 match custom(key_str) {
523 Ok(()) => valid.push(key_str.to_string()),
524 Err(e) => errors.push((key_str.to_string(), e)),
525 }
526 } else {
527 valid.push(key_str.to_string());
528 }
529 }
530 Err(e) => errors.push((key_str.to_string(), e)),
531 }
532 }
533
534 ValidationResult {
535 total_processed: valid.len() + errors.len(),
536 valid,
537 errors,
538 }
539 }
540}
541
542#[derive(Debug, Clone, PartialEq, Eq)]
544pub struct ValidationResult {
545 pub total_processed: usize,
547 pub valid: Vec<String>,
549 pub errors: Vec<(String, KeyParseError)>,
551}
552
553impl ValidationResult {
554 #[must_use]
556 pub fn is_success(&self) -> bool {
557 self.errors.is_empty()
558 }
559
560 #[must_use]
562 pub fn valid_count(&self) -> usize {
563 self.valid.len()
564 }
565
566 #[must_use]
568 pub fn error_count(&self) -> usize {
569 self.errors.len()
570 }
571
572 #[must_use]
574 pub fn success_rate(&self) -> f64 {
575 if self.total_processed == 0 {
576 0.0
577 } else {
578 #[allow(clippy::cast_precision_loss)]
579 let valid_ratio = self.valid.len() as f64 / self.total_processed as f64;
580 valid_ratio * 100.0
581 }
582 }
583
584 pub fn into_keys<T: KeyDomain>(self) -> Result<Vec<Key<T>>, KeyParseError> {
590 self.valid
591 .into_iter()
592 .map(|s| Key::from_string(s))
593 .collect()
594 }
595
596 #[must_use]
598 pub fn try_into_keys<T: KeyDomain>(self) -> Vec<Key<T>> {
599 self.valid
600 .into_iter()
601 .filter_map(|s| Key::from_string(s).ok())
602 .collect()
603 }
604}
605
606#[must_use]
612pub fn strict_validator<T: KeyDomain>() -> ValidationBuilder<T> {
613 ValidationBuilder::new()
614 .stop_on_first_error(true)
615 .allow_empty_collection(false)
616}
617
618#[must_use]
620pub fn lenient_validator<T: KeyDomain>() -> ValidationBuilder<T> {
621 ValidationBuilder::new()
622 .stop_on_first_error(false)
623 .allow_empty_collection(true)
624}
625
626pub fn quick_convert<T: KeyDomain, I>(keys: I) -> Result<Vec<Key<T>>, Vec<(String, KeyParseError)>>
653where
654 I: IntoIterator,
655 I::Item: AsRef<str>,
656{
657 let (valid, invalid) = validate_batch::<T, I>(keys);
658
659 if invalid.is_empty() {
660 let keys: Result<Vec<_>, _> = valid.into_iter().map(|s| Key::from_string(s)).collect();
661 match keys {
662 Ok(k) => Ok(k),
663 Err(e) => Err(vec![(String::new(), e)]),
664 }
665 } else {
666 Err(invalid)
667 }
668}
669
670#[cfg(test)]
675mod tests {
676 use super::*;
677
678 #[derive(Debug)]
680 struct TestDomain;
681
682 impl crate::Domain for TestDomain {
683 const DOMAIN_NAME: &'static str = "test";
684 }
685
686 impl KeyDomain for TestDomain {
687 const MAX_LENGTH: usize = 32;
688
689 fn validation_help() -> Option<&'static str> {
690 Some("Test domain help")
691 }
692
693 fn examples() -> &'static [&'static str] {
694 &["example1", "example2"]
695 }
696 }
697
698 #[test]
699 fn is_valid_key_accepts_good_rejects_bad() {
700 assert!(is_valid_key::<TestDomain>("valid_key"));
701 assert!(!is_valid_key::<TestDomain>(""));
702 assert!(!is_valid_key::<TestDomain>("a".repeat(50).as_str()));
703 }
704
705 #[test]
706 fn validate_key_returns_error_for_empty() {
707 assert!(validate_key::<TestDomain>("valid_key").is_ok());
708 assert!(validate_key::<TestDomain>("").is_err());
709 }
710
711 #[test]
712 fn validation_info_contains_domain_details() {
713 let info = validation_info::<TestDomain>();
714 assert!(info.contains("Domain: test"));
715 assert!(info.contains("Max length: 32"));
716 assert!(info.contains("Help: Test domain help"));
717 assert!(info.contains("Examples: example1, example2"));
718 }
719
720 #[test]
721 fn validate_batch_separates_valid_and_invalid() {
722 let keys = vec!["valid1", "", "valid2", "bad key"];
723 let (valid, invalid) = validate_batch::<TestDomain, _>(&keys);
724
725 assert_eq!(valid.len(), 2);
726 assert_eq!(invalid.len(), 2);
727 assert!(valid.contains(&"valid1".to_string()));
728 assert!(valid.contains(&"valid2".to_string()));
729 }
730
731 #[test]
732 fn filter_valid_removes_bad_keys() {
733 let keys = vec!["valid1", "", "valid2", "bad key"];
734 let valid: Vec<_> = filter_valid::<TestDomain, _>(&keys).collect();
735
736 assert_eq!(valid.len(), 2);
737 assert!(valid.contains(&"valid1".to_string()));
738 assert!(valid.contains(&"valid2".to_string()));
739 }
740
741 #[test]
742 fn count_valid_matches_filter_length() {
743 let keys = vec!["valid1", "", "valid2", "bad key"];
744 let count = count_valid::<TestDomain, _>(&keys);
745 assert_eq!(count, 2);
746 }
747
748 #[test]
749 fn all_valid_true_only_when_all_pass() {
750 let all_valid_keys = vec!["valid1", "valid2"];
751 let mixed = vec!["valid1", "", "valid2"];
752
753 assert!(all_valid::<TestDomain, _>(&all_valid_keys));
754 assert!(!all_valid::<TestDomain, _>(&mixed));
755 }
756
757 #[test]
758 fn any_valid_true_when_at_least_one_passes() {
759 let mixed = vec!["", "valid1", ""];
760 let all_invalid = vec!["", ""];
761
762 assert!(any_valid::<TestDomain, _>(&mixed));
763 assert!(!any_valid::<TestDomain, _>(&all_invalid));
764 }
765
766 #[test]
767 fn into_key_converts_str_and_string() {
768 let key1: Key<TestDomain> = "test_key".into_key().unwrap();
769 let key2: Key<TestDomain> = "another_key".to_string().into_key().unwrap();
770
771 assert_eq!(key1.as_str(), "test_key");
772 assert_eq!(key2.as_str(), "another_key");
773
774 let invalid: Option<Key<TestDomain>> = "".try_into_key();
775 assert!(invalid.is_none());
776 }
777
778 #[test]
779 fn builder_respects_max_failures_limit() {
780 let builder = ValidationBuilder::<TestDomain>::new()
781 .allow_empty_collection(true)
782 .max_failures(2)
783 .stop_on_first_error(false);
784
785 let keys = vec!["valid1", "", "valid2", "", "valid3"];
786 let result = builder.validate(&keys);
787
788 #[cfg(feature = "std")]
790 {
791 println!("Total processed: {}", result.total_processed);
792 println!("Valid count: {}", result.valid_count());
793 println!("Error count: {}", result.error_count());
794 println!("Valid keys: {:?}", result.valid);
795 println!("Errors: {:?}", result.errors);
796 }
797
798 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); }
813
814 #[test]
815 fn builder_stops_on_first_error_when_configured() {
816 let builder = ValidationBuilder::<TestDomain>::new()
817 .stop_on_first_error(true)
818 .allow_empty_collection(false);
819
820 let keys = vec!["valid", "", "another"];
821 let result = builder.validate(&keys);
822
823 assert_eq!(result.total_processed, 2); assert_eq!(result.valid_count(), 1);
826 assert_eq!(result.error_count(), 1);
827 }
828
829 #[test]
830 fn builder_processes_all_when_not_stopping_on_error() {
831 let builder = ValidationBuilder::<TestDomain>::new()
832 .stop_on_first_error(false)
833 .allow_empty_collection(true);
834
835 let keys = vec!["valid", "", "another"];
836 let result = builder.validate(&keys);
837
838 assert_eq!(result.total_processed, 3);
840 assert_eq!(result.valid_count(), 2);
841 assert_eq!(result.error_count(), 1);
842 }
843
844 #[test]
845 fn validation_result_computes_success_rate() {
846 const EPSILON: f64 = 1e-10;
847 let keys = vec!["valid1", "valid2"];
848 let (valid, errors) = validate_batch::<TestDomain, _>(keys);
849
850 let result = ValidationResult {
851 total_processed: valid.len() + errors.len(),
852 valid,
853 errors,
854 };
855
856 assert!(result.is_success());
857 assert_eq!(result.valid_count(), 2);
858 assert_eq!(result.error_count(), 0);
859
860 assert!((result.success_rate() - 100.0).abs() < EPSILON);
861
862 let keys = result.try_into_keys::<TestDomain>();
863 assert_eq!(keys.len(), 2);
864 }
865
866 #[test]
867 fn strict_validator_stops_on_first_error() {
868 let validator = strict_validator::<TestDomain>();
869 let keys = vec!["valid", "", "another"];
870 let result = validator.validate(&keys);
871
872 assert_eq!(result.total_processed, 2); assert_eq!(result.valid_count(), 1);
875 assert_eq!(result.error_count(), 1);
876 }
877
878 #[test]
879 fn lenient_validator_processes_all_items() {
880 let validator = lenient_validator::<TestDomain>();
881 let keys = vec!["valid", "", "another"];
882 let result = validator.validate(&keys);
883
884 assert_eq!(result.total_processed, 3);
886 assert_eq!(result.valid_count(), 2);
887 assert_eq!(result.error_count(), 1);
888 }
889
890 #[test]
891 fn quick_convert_succeeds_or_returns_errors() {
892 let strings = vec!["key1", "key2", "key3"];
893 let keys = quick_convert::<TestDomain, _>(&strings).unwrap();
894 assert_eq!(keys.len(), 3);
895
896 let mixed = vec!["key1", "", "key2"];
897 let result = quick_convert::<TestDomain, _>(&mixed);
898 assert!(result.is_err());
899 }
900
901 #[test]
902 fn custom_validator_applies_extra_check() {
903 fn custom_check(key: &str) -> Result<(), KeyParseError> {
904 if key.starts_with("custom_") {
905 Ok(())
906 } else {
907 Err(KeyParseError::custom(9999, "Must start with custom_"))
908 }
909 }
910
911 let validator = ValidationBuilder::<TestDomain>::new().custom_validator(custom_check);
912
913 let keys = vec!["custom_key", "invalid_key"];
914 let result = validator.validate(&keys);
915
916 assert_eq!(result.valid_count(), 1);
917 assert_eq!(result.error_count(), 1);
918 }
919}