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#[inline]
47#[must_use]
48pub fn is_valid_key<T: KeyDomain>(key: &str) -> bool {
49 validate_key::<T>(key).is_ok()
50}
51
52pub fn validate_key<T: KeyDomain>(key: &str) -> Result<(), KeyParseError> {
80 Key::<T>::new(key).map(|_| ())
81}
82
83#[must_use]
109pub fn validation_help<T: KeyDomain>() -> Option<&'static str> {
110 T::validation_help()
111}
112
113#[must_use]
139pub fn validation_info<T: KeyDomain>() -> String {
140 let mut info = format!("Domain: {}\n", T::DOMAIN_NAME);
141 writeln!(info, "Max length: {}", T::MAX_LENGTH).unwrap();
142 writeln!(info, "Min length: {}", T::min_length()).unwrap();
143 writeln!(info, "Expected length: {}", T::EXPECTED_LENGTH).unwrap();
144 writeln!(info, "Case insensitive: {}", T::CASE_INSENSITIVE).unwrap();
145 writeln!(info, "Custom validation: {}", T::HAS_CUSTOM_VALIDATION).unwrap();
146 writeln!(
147 info,
148 "Custom normalization: {}",
149 T::HAS_CUSTOM_NORMALIZATION,
150 )
151 .unwrap();
152
153 writeln!(info, "Default separator: '{}'", T::default_separator()).unwrap();
154
155 if let Some(help) = T::validation_help() {
156 info.push_str("Help: ");
157 info.push_str(help);
158 info.push('\n');
159 }
160
161 let examples = T::examples();
162 if !examples.is_empty() {
163 info.push_str("Examples: ");
164 for (i, example) in examples.iter().enumerate() {
165 if i > 0 {
166 info.push_str(", ");
167 }
168 info.push_str(example);
169 }
170 info.push('\n');
171 }
172
173 info
174}
175
176pub fn validate_batch<T: KeyDomain, I>(keys: I) -> (Vec<String>, Vec<(String, KeyParseError)>)
210where
211 I: IntoIterator,
212 I::Item: AsRef<str>,
213{
214 let mut valid = Vec::new();
215 let mut invalid = Vec::new();
216
217 for key in keys {
218 let key_str = key.as_ref();
219 match validate_key::<T>(key_str) {
220 Ok(()) => valid.push(key_str.to_string()),
221 Err(e) => invalid.push((key_str.to_string(), e)),
222 }
223 }
224
225 (valid, invalid)
226}
227
228pub fn filter_valid<T: KeyDomain, I>(keys: I) -> impl Iterator<Item = I::Item>
251where
252 I: IntoIterator,
253 I::Item: AsRef<str>,
254{
255 keys.into_iter()
256 .filter(|key| is_valid_key::<T>(key.as_ref()))
257}
258
259pub fn count_valid<T: KeyDomain, I>(keys: I) -> usize
281where
282 I: IntoIterator,
283 I::Item: AsRef<str>,
284{
285 keys.into_iter()
286 .filter(|key| is_valid_key::<T>(key.as_ref()))
287 .count()
288}
289
290pub fn all_valid<T: KeyDomain, I>(keys: I) -> bool
313where
314 I: IntoIterator,
315 I::Item: AsRef<str>,
316{
317 keys.into_iter().all(|key| is_valid_key::<T>(key.as_ref()))
318}
319
320pub fn any_valid<T: KeyDomain, I>(keys: I) -> bool
343where
344 I: IntoIterator,
345 I::Item: AsRef<str>,
346{
347 keys.into_iter().any(|key| is_valid_key::<T>(key.as_ref()))
348}
349
350pub trait IntoKey<T: KeyDomain> {
359 fn into_key(self) -> Result<Key<T>, KeyParseError>;
365
366 fn try_into_key(self) -> Option<Key<T>>;
371}
372
373impl<T: KeyDomain> IntoKey<T> for &str {
374 #[inline]
375 fn into_key(self) -> Result<Key<T>, KeyParseError> {
376 Key::new(self)
377 }
378
379 #[inline]
380 fn try_into_key(self) -> Option<Key<T>> {
381 Key::try_new(self)
382 }
383}
384
385impl<T: KeyDomain> IntoKey<T> for String {
386 #[inline]
387 fn into_key(self) -> Result<Key<T>, KeyParseError> {
388 Key::from_string(self)
389 }
390
391 #[inline]
392 fn try_into_key(self) -> Option<Key<T>> {
393 Key::from_string(self).ok()
394 }
395}
396
397impl<T: KeyDomain> IntoKey<T> for &String {
398 #[inline]
399 fn into_key(self) -> Result<Key<T>, KeyParseError> {
400 Key::new(self)
401 }
402
403 #[inline]
404 fn try_into_key(self) -> Option<Key<T>> {
405 Key::try_new(self)
406 }
407}
408
409type ValidatorFunction = fn(&str) -> Result<(), KeyParseError>;
410
411#[derive(Debug)]
420pub struct ValidationBuilder<T: KeyDomain> {
421 allow_empty_collection: bool,
422 max_failures: Option<usize>,
423 stop_on_first_error: bool,
424 custom_validator: Option<ValidatorFunction>,
425 _phantom: core::marker::PhantomData<T>,
426}
427
428impl<T: KeyDomain> Default for ValidationBuilder<T> {
429 fn default() -> Self {
430 Self::new()
431 }
432}
433
434impl<T: KeyDomain> ValidationBuilder<T> {
435 #[must_use]
437 pub fn new() -> Self {
438 Self {
439 allow_empty_collection: false,
440 max_failures: None,
441 stop_on_first_error: false,
442 custom_validator: None,
443 _phantom: core::marker::PhantomData,
444 }
445 }
446
447 #[must_use]
449 pub fn allow_empty_collection(mut self, allow: bool) -> Self {
450 self.allow_empty_collection = allow;
451 self
452 }
453
454 #[must_use]
456 pub fn max_failures(mut self, max: usize) -> Self {
457 self.max_failures = Some(max);
458 self
459 }
460
461 #[must_use]
463 pub fn stop_on_first_error(mut self, stop: bool) -> Self {
464 self.stop_on_first_error = stop;
465 self
466 }
467
468 #[must_use]
470 pub fn custom_validator(mut self, validator: ValidatorFunction) -> Self {
471 self.custom_validator = Some(validator);
472 self
473 }
474
475 pub fn validate<I>(&self, keys: I) -> ValidationResult
477 where
478 I: IntoIterator,
479 I::Item: AsRef<str>,
480 {
481 let mut valid = Vec::new();
482 let mut errors = Vec::new();
483 let mut keys = keys.into_iter().peekable();
484
485 if keys.peek().is_none() && !self.allow_empty_collection {
486 return ValidationResult {
487 valid,
488 errors: vec![(String::new(), KeyParseError::Empty)],
489 total_processed: 0,
490 };
491 }
492
493 for key in keys {
494 let key_str = key.as_ref();
495
496 if let Some(max) = self.max_failures {
498 if errors.len() >= max {
499 break;
500 }
501 }
502
503 if self.stop_on_first_error && !errors.is_empty() {
504 break;
505 }
506
507 match validate_key::<T>(key_str) {
509 Ok(()) => {
510 let normalized = Key::<T>::normalize(key_str);
512 if let Some(custom) = self.custom_validator {
513 match custom(&normalized) {
514 Ok(()) => valid.push(normalized.into_owned()),
515 Err(e) => errors.push((normalized.into_owned(), e)),
516 }
517 } else {
518 valid.push(normalized.into_owned());
519 }
520 }
521 Err(e) => errors.push((key_str.to_string(), e)),
522 }
523 }
524
525 ValidationResult {
526 total_processed: valid.len() + errors.len(),
527 valid,
528 errors,
529 }
530 }
531}
532
533#[derive(Debug, Clone, PartialEq, Eq)]
535pub struct ValidationResult {
536 pub total_processed: usize,
538 pub valid: Vec<String>,
540 pub errors: Vec<(String, KeyParseError)>,
542}
543
544impl ValidationResult {
545 #[inline]
547 #[must_use]
548 pub fn is_success(&self) -> bool {
549 self.errors.is_empty()
550 }
551
552 #[inline]
554 #[must_use]
555 pub fn valid_count(&self) -> usize {
556 self.valid.len()
557 }
558
559 #[inline]
561 #[must_use]
562 pub fn error_count(&self) -> usize {
563 self.errors.len()
564 }
565
566 #[must_use]
568 pub fn success_rate(&self) -> f64 {
569 if self.total_processed == 0 {
570 0.0
571 } else {
572 #[allow(clippy::cast_precision_loss)]
573 let valid_ratio = self.valid.len() as f64 / self.total_processed as f64;
574 valid_ratio * 100.0
575 }
576 }
577
578 pub fn into_keys<T: KeyDomain>(self) -> Result<Vec<Key<T>>, KeyParseError> {
584 self.valid
585 .into_iter()
586 .map(|s| Key::from_string(s))
587 .collect()
588 }
589
590 #[must_use]
592 pub fn try_into_keys<T: KeyDomain>(self) -> Vec<Key<T>> {
593 self.valid
594 .into_iter()
595 .filter_map(|s| Key::from_string(s).ok())
596 .collect()
597 }
598}
599
600#[must_use]
606pub fn strict_validator<T: KeyDomain>() -> ValidationBuilder<T> {
607 ValidationBuilder::new()
608 .stop_on_first_error(true)
609 .allow_empty_collection(false)
610}
611
612#[must_use]
614pub fn lenient_validator<T: KeyDomain>() -> ValidationBuilder<T> {
615 ValidationBuilder::new()
616 .stop_on_first_error(false)
617 .allow_empty_collection(true)
618}
619
620pub fn quick_convert<T: KeyDomain, I>(keys: I) -> Result<Vec<Key<T>>, Vec<(String, KeyParseError)>>
647where
648 I: IntoIterator,
649 I::Item: AsRef<str>,
650{
651 let mut valid = Vec::new();
652 let mut errors = Vec::new();
653
654 for key in keys {
655 let key_str = key.as_ref().to_string();
656 match Key::<T>::from_string(key_str.clone()) {
657 Ok(k) => valid.push(k),
658 Err(e) => errors.push((key_str, e)),
659 }
660 }
661
662 if errors.is_empty() {
663 Ok(valid)
664 } else {
665 Err(errors)
666 }
667}
668
669#[cfg(test)]
674mod tests {
675 use super::*;
676
677 #[derive(Debug)]
679 struct TestDomain;
680
681 impl crate::Domain for TestDomain {
682 const DOMAIN_NAME: &'static str = "test";
683 }
684
685 impl KeyDomain for TestDomain {
686 const MAX_LENGTH: usize = 32;
687
688 fn validation_help() -> Option<&'static str> {
689 Some("Test domain help")
690 }
691
692 fn examples() -> &'static [&'static str] {
693 &["example1", "example2"]
694 }
695 }
696
697 #[test]
698 fn is_valid_key_accepts_good_rejects_bad() {
699 assert!(is_valid_key::<TestDomain>("valid_key"));
700 assert!(!is_valid_key::<TestDomain>(""));
701 assert!(!is_valid_key::<TestDomain>("a".repeat(50).as_str()));
702 }
703
704 #[test]
705 fn validate_key_returns_error_for_empty() {
706 assert!(validate_key::<TestDomain>("valid_key").is_ok());
707 assert!(validate_key::<TestDomain>("").is_err());
708 }
709
710 #[test]
711 fn validation_info_contains_domain_details() {
712 let info = validation_info::<TestDomain>();
713 assert!(info.contains("Domain: test"));
714 assert!(info.contains("Max length: 32"));
715 assert!(info.contains("Help: Test domain help"));
716 assert!(info.contains("Examples: example1, example2"));
717 }
718
719 #[test]
720 fn validate_batch_separates_valid_and_invalid() {
721 let keys = vec!["valid1", "", "valid2", "bad key"];
722 let (valid, invalid) = validate_batch::<TestDomain, _>(&keys);
723
724 assert_eq!(valid.len(), 2);
725 assert_eq!(invalid.len(), 2);
726 assert!(valid.contains(&"valid1".to_string()));
727 assert!(valid.contains(&"valid2".to_string()));
728 }
729
730 #[test]
731 fn filter_valid_removes_bad_keys() {
732 let keys = vec!["valid1", "", "valid2", "bad key"];
733 let valid: Vec<_> = filter_valid::<TestDomain, _>(&keys).collect();
734
735 assert_eq!(valid.len(), 2);
736 assert!(valid.contains(&&"valid1"));
737 assert!(valid.contains(&&"valid2"));
738 }
739
740 #[test]
741 fn count_valid_matches_filter_length() {
742 let keys = vec!["valid1", "", "valid2", "bad key"];
743 let count = count_valid::<TestDomain, _>(&keys);
744 assert_eq!(count, 2);
745 }
746
747 #[test]
748 fn all_valid_true_only_when_all_pass() {
749 let all_valid_keys = vec!["valid1", "valid2"];
750 let mixed = vec!["valid1", "", "valid2"];
751
752 assert!(all_valid::<TestDomain, _>(&all_valid_keys));
753 assert!(!all_valid::<TestDomain, _>(&mixed));
754 }
755
756 #[test]
757 fn any_valid_true_when_at_least_one_passes() {
758 let mixed = vec!["", "valid1", ""];
759 let all_invalid = vec!["", ""];
760
761 assert!(any_valid::<TestDomain, _>(&mixed));
762 assert!(!any_valid::<TestDomain, _>(&all_invalid));
763 }
764
765 #[test]
766 fn into_key_converts_str_and_string() {
767 let key1: Key<TestDomain> = "test_key".into_key().unwrap();
768 let key2: Key<TestDomain> = "another_key".to_string().into_key().unwrap();
769
770 assert_eq!(key1.as_str(), "test_key");
771 assert_eq!(key2.as_str(), "another_key");
772
773 let invalid: Option<Key<TestDomain>> = "".try_into_key();
774 assert!(invalid.is_none());
775 }
776
777 #[test]
778 fn builder_respects_max_failures_limit() {
779 let builder = ValidationBuilder::<TestDomain>::new()
780 .allow_empty_collection(true)
781 .max_failures(2)
782 .stop_on_first_error(false);
783
784 let keys = vec!["valid1", "", "valid2", "", "valid3"];
785 let result = builder.validate(&keys);
786
787 #[cfg(feature = "std")]
789 {
790 println!("Total processed: {}", result.total_processed);
791 println!("Valid count: {}", result.valid_count());
792 println!("Error count: {}", result.error_count());
793 println!("Valid keys: {:?}", result.valid);
794 println!("Errors: {:?}", result.errors);
795 }
796
797 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); }
812
813 #[test]
814 fn builder_stops_on_first_error_when_configured() {
815 let builder = ValidationBuilder::<TestDomain>::new()
816 .stop_on_first_error(true)
817 .allow_empty_collection(false);
818
819 let keys = vec!["valid", "", "another"];
820 let result = builder.validate(&keys);
821
822 assert_eq!(result.total_processed, 2); assert_eq!(result.valid_count(), 1);
825 assert_eq!(result.error_count(), 1);
826 }
827
828 #[test]
829 fn builder_processes_all_when_not_stopping_on_error() {
830 let builder = ValidationBuilder::<TestDomain>::new()
831 .stop_on_first_error(false)
832 .allow_empty_collection(true);
833
834 let keys = vec!["valid", "", "another"];
835 let result = builder.validate(&keys);
836
837 assert_eq!(result.total_processed, 3);
839 assert_eq!(result.valid_count(), 2);
840 assert_eq!(result.error_count(), 1);
841 }
842
843 #[test]
844 fn validation_result_computes_success_rate() {
845 const EPSILON: f64 = 1e-10;
846 let keys = vec!["valid1", "valid2"];
847 let (valid, errors) = validate_batch::<TestDomain, _>(keys);
848
849 let result = ValidationResult {
850 total_processed: valid.len() + errors.len(),
851 valid,
852 errors,
853 };
854
855 assert!(result.is_success());
856 assert_eq!(result.valid_count(), 2);
857 assert_eq!(result.error_count(), 0);
858
859 assert!((result.success_rate() - 100.0).abs() < EPSILON);
860
861 let keys = result.try_into_keys::<TestDomain>();
862 assert_eq!(keys.len(), 2);
863 }
864
865 #[test]
866 fn strict_validator_stops_on_first_error() {
867 let validator = strict_validator::<TestDomain>();
868 let keys = vec!["valid", "", "another"];
869 let result = validator.validate(&keys);
870
871 assert_eq!(result.total_processed, 2); assert_eq!(result.valid_count(), 1);
874 assert_eq!(result.error_count(), 1);
875 }
876
877 #[test]
878 fn lenient_validator_processes_all_items() {
879 let validator = lenient_validator::<TestDomain>();
880 let keys = vec!["valid", "", "another"];
881 let result = validator.validate(&keys);
882
883 assert_eq!(result.total_processed, 3);
885 assert_eq!(result.valid_count(), 2);
886 assert_eq!(result.error_count(), 1);
887 }
888
889 #[test]
890 fn quick_convert_succeeds_or_returns_errors() {
891 let strings = vec!["key1", "key2", "key3"];
892 let keys = quick_convert::<TestDomain, _>(&strings).unwrap();
893 assert_eq!(keys.len(), 3);
894
895 let mixed = vec!["key1", "", "key2"];
896 let result = quick_convert::<TestDomain, _>(&mixed);
897 assert!(result.is_err());
898 }
899
900 #[test]
901 fn custom_validator_applies_extra_check() {
902 fn custom_check(key: &str) -> Result<(), KeyParseError> {
903 if key.starts_with("custom_") {
904 Ok(())
905 } else {
906 Err(KeyParseError::custom(9999, "Must start with custom_"))
907 }
908 }
909
910 let validator = ValidationBuilder::<TestDomain>::new().custom_validator(custom_check);
911
912 let keys = vec!["custom_key", "invalid_key"];
913 let result = validator.validate(&keys);
914
915 assert_eq!(result.valid_count(), 1);
916 assert_eq!(result.error_count(), 1);
917 }
918}