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 #[expect(
579 clippy::cast_precision_loss,
580 reason = "count-to-f64 for percentage calculation"
581 )]
582 let valid_ratio = self.valid.len() as f64 / self.total_processed as f64;
583 valid_ratio * 100.0
584 }
585 }
586
587 pub fn into_keys<T: KeyDomain>(self) -> Result<Vec<Key<T>>, KeyParseError> {
593 self.valid
594 .into_iter()
595 .map(|s| Key::from_string(s))
596 .collect()
597 }
598
599 #[must_use]
601 pub fn try_into_keys<T: KeyDomain>(self) -> Vec<Key<T>> {
602 self.valid
603 .into_iter()
604 .filter_map(|s| Key::from_string(s).ok())
605 .collect()
606 }
607}
608
609#[must_use]
615pub fn strict_validator<T: KeyDomain>() -> ValidationBuilder<T> {
616 ValidationBuilder::new()
617 .stop_on_first_error(true)
618 .allow_empty_collection(false)
619}
620
621#[must_use]
623pub fn lenient_validator<T: KeyDomain>() -> ValidationBuilder<T> {
624 ValidationBuilder::new()
625 .stop_on_first_error(false)
626 .allow_empty_collection(true)
627}
628
629pub fn quick_convert<T: KeyDomain, I>(keys: I) -> Result<Vec<Key<T>>, Vec<(String, KeyParseError)>>
656where
657 I: IntoIterator,
658 I::Item: AsRef<str>,
659{
660 let (valid, invalid) = validate_batch::<T, I>(keys);
661
662 if invalid.is_empty() {
663 let keys: Result<Vec<_>, _> = valid.into_iter().map(|s| Key::from_string(s)).collect();
664 match keys {
665 Ok(k) => Ok(k),
666 Err(e) => Err(vec![(String::new(), e)]),
667 }
668 } else {
669 Err(invalid)
670 }
671}
672
673#[cfg(test)]
678mod tests {
679 use super::*;
680
681 #[derive(Debug)]
683 struct TestDomain;
684
685 impl crate::Domain for TestDomain {
686 const DOMAIN_NAME: &'static str = "test";
687 }
688
689 impl KeyDomain for TestDomain {
690 const MAX_LENGTH: usize = 32;
691
692 fn validation_help() -> Option<&'static str> {
693 Some("Test domain help")
694 }
695
696 fn examples() -> &'static [&'static str] {
697 &["example1", "example2"]
698 }
699 }
700
701 #[test]
702 fn is_valid_key_accepts_good_rejects_bad() {
703 assert!(is_valid_key::<TestDomain>("valid_key"));
704 assert!(!is_valid_key::<TestDomain>(""));
705 assert!(!is_valid_key::<TestDomain>("a".repeat(50).as_str()));
706 }
707
708 #[test]
709 fn validate_key_returns_error_for_empty() {
710 assert!(validate_key::<TestDomain>("valid_key").is_ok());
711 assert!(validate_key::<TestDomain>("").is_err());
712 }
713
714 #[test]
715 fn validation_info_contains_domain_details() {
716 let info = validation_info::<TestDomain>();
717 assert!(info.contains("Domain: test"));
718 assert!(info.contains("Max length: 32"));
719 assert!(info.contains("Help: Test domain help"));
720 assert!(info.contains("Examples: example1, example2"));
721 }
722
723 #[test]
724 fn validate_batch_separates_valid_and_invalid() {
725 let keys = vec!["valid1", "", "valid2", "bad key"];
726 let (valid, invalid) = validate_batch::<TestDomain, _>(&keys);
727
728 assert_eq!(valid.len(), 2);
729 assert_eq!(invalid.len(), 2);
730 assert!(valid.contains(&"valid1".to_string()));
731 assert!(valid.contains(&"valid2".to_string()));
732 }
733
734 #[test]
735 fn filter_valid_removes_bad_keys() {
736 let keys = vec!["valid1", "", "valid2", "bad key"];
737 let valid: Vec<_> = filter_valid::<TestDomain, _>(&keys).collect();
738
739 assert_eq!(valid.len(), 2);
740 assert!(valid.contains(&"valid1".to_string()));
741 assert!(valid.contains(&"valid2".to_string()));
742 }
743
744 #[test]
745 fn count_valid_matches_filter_length() {
746 let keys = vec!["valid1", "", "valid2", "bad key"];
747 let count = count_valid::<TestDomain, _>(&keys);
748 assert_eq!(count, 2);
749 }
750
751 #[test]
752 fn all_valid_true_only_when_all_pass() {
753 let all_valid_keys = vec!["valid1", "valid2"];
754 let mixed = vec!["valid1", "", "valid2"];
755
756 assert!(all_valid::<TestDomain, _>(&all_valid_keys));
757 assert!(!all_valid::<TestDomain, _>(&mixed));
758 }
759
760 #[test]
761 fn any_valid_true_when_at_least_one_passes() {
762 let mixed = vec!["", "valid1", ""];
763 let all_invalid = vec!["", ""];
764
765 assert!(any_valid::<TestDomain, _>(&mixed));
766 assert!(!any_valid::<TestDomain, _>(&all_invalid));
767 }
768
769 #[test]
770 fn into_key_converts_str_and_string() {
771 let key1: Key<TestDomain> = "test_key".into_key().unwrap();
772 let key2: Key<TestDomain> = "another_key".to_string().into_key().unwrap();
773
774 assert_eq!(key1.as_str(), "test_key");
775 assert_eq!(key2.as_str(), "another_key");
776
777 let invalid: Option<Key<TestDomain>> = "".try_into_key();
778 assert!(invalid.is_none());
779 }
780
781 #[test]
782 fn builder_respects_max_failures_limit() {
783 let builder = ValidationBuilder::<TestDomain>::new()
784 .allow_empty_collection(true)
785 .max_failures(2)
786 .stop_on_first_error(false);
787
788 let keys = vec!["valid1", "", "valid2", "", "valid3"];
789 let result = builder.validate(&keys);
790
791 #[cfg(feature = "std")]
793 {
794 println!("Total processed: {}", result.total_processed);
795 println!("Valid count: {}", result.valid_count());
796 println!("Error count: {}", result.error_count());
797 println!("Valid keys: {:?}", result.valid);
798 println!("Errors: {:?}", result.errors);
799 }
800
801 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); }
816
817 #[test]
818 fn builder_stops_on_first_error_when_configured() {
819 let builder = ValidationBuilder::<TestDomain>::new()
820 .stop_on_first_error(true)
821 .allow_empty_collection(false);
822
823 let keys = vec!["valid", "", "another"];
824 let result = builder.validate(&keys);
825
826 assert_eq!(result.total_processed, 2); assert_eq!(result.valid_count(), 1);
829 assert_eq!(result.error_count(), 1);
830 }
831
832 #[test]
833 fn builder_processes_all_when_not_stopping_on_error() {
834 let builder = ValidationBuilder::<TestDomain>::new()
835 .stop_on_first_error(false)
836 .allow_empty_collection(true);
837
838 let keys = vec!["valid", "", "another"];
839 let result = builder.validate(&keys);
840
841 assert_eq!(result.total_processed, 3);
843 assert_eq!(result.valid_count(), 2);
844 assert_eq!(result.error_count(), 1);
845 }
846
847 #[test]
848 fn validation_result_computes_success_rate() {
849 const EPSILON: f64 = 1e-10;
850 let keys = vec!["valid1", "valid2"];
851 let (valid, errors) = validate_batch::<TestDomain, _>(keys);
852
853 let result = ValidationResult {
854 total_processed: valid.len() + errors.len(),
855 valid,
856 errors,
857 };
858
859 assert!(result.is_success());
860 assert_eq!(result.valid_count(), 2);
861 assert_eq!(result.error_count(), 0);
862
863 assert!((result.success_rate() - 100.0).abs() < EPSILON);
864
865 let keys = result.try_into_keys::<TestDomain>();
866 assert_eq!(keys.len(), 2);
867 }
868
869 #[test]
870 fn strict_validator_stops_on_first_error() {
871 let validator = strict_validator::<TestDomain>();
872 let keys = vec!["valid", "", "another"];
873 let result = validator.validate(&keys);
874
875 assert_eq!(result.total_processed, 2); assert_eq!(result.valid_count(), 1);
878 assert_eq!(result.error_count(), 1);
879 }
880
881 #[test]
882 fn lenient_validator_processes_all_items() {
883 let validator = lenient_validator::<TestDomain>();
884 let keys = vec!["valid", "", "another"];
885 let result = validator.validate(&keys);
886
887 assert_eq!(result.total_processed, 3);
889 assert_eq!(result.valid_count(), 2);
890 assert_eq!(result.error_count(), 1);
891 }
892
893 #[test]
894 fn quick_convert_succeeds_or_returns_errors() {
895 let strings = vec!["key1", "key2", "key3"];
896 let keys = quick_convert::<TestDomain, _>(&strings).unwrap();
897 assert_eq!(keys.len(), 3);
898
899 let mixed = vec!["key1", "", "key2"];
900 let result = quick_convert::<TestDomain, _>(&mixed);
901 assert!(result.is_err());
902 }
903
904 #[test]
905 fn custom_validator_applies_extra_check() {
906 fn custom_check(key: &str) -> Result<(), KeyParseError> {
907 if key.starts_with("custom_") {
908 Ok(())
909 } else {
910 Err(KeyParseError::custom(9999, "Must start with custom_"))
911 }
912 }
913
914 let validator = ValidationBuilder::<TestDomain>::new().custom_validator(custom_check);
915
916 let keys = vec!["custom_key", "invalid_key"];
917 let result = validator.validate(&keys);
918
919 assert_eq!(result.valid_count(), 1);
920 assert_eq!(result.error_count(), 1);
921 }
922}