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