Skip to main content

domain_key/
validation.rs

1//! Validation utilities and helper traits for domain-key
2//!
3//! This module provides comprehensive validation functionality, including
4//! validation without key creation, batch validation, and helper traits
5//! for converting various types into keys.
6
7use 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// ============================================================================
23// VALIDATION FUNCTIONS
24// ============================================================================
25
26/// Check if a string would be a valid key for a domain without creating the key
27///
28/// This is useful for pre-validation or filtering operations where you don't
29/// need the actual key object.
30///
31/// # Examples
32///
33/// ```rust
34/// use domain_key::{Domain, KeyDomain, validation};
35///
36/// #[derive(Debug)]
37/// struct TestDomain;
38/// impl Domain for TestDomain {
39///     const DOMAIN_NAME: &'static str = "test";
40/// }
41/// impl KeyDomain for TestDomain {}
42///
43/// assert!(validation::is_valid_key::<TestDomain>("good_key"));
44/// assert!(!validation::is_valid_key::<TestDomain>(""));
45/// ```
46#[inline]
47#[must_use]
48pub fn is_valid_key<T: KeyDomain>(key: &str) -> bool {
49    validate_key::<T>(key).is_ok()
50}
51
52/// Validate a key string and return detailed error information
53///
54/// This performs the same validation as `Key::new` but without creating
55/// the key object, making it useful for validation-only scenarios.
56///
57/// # Errors
58///
59/// Returns `KeyParseError` if the key fails common or domain-specific validation
60///
61/// # Examples
62///
63/// ```rust
64/// use domain_key::{Domain, KeyDomain, validation, KeyParseError};
65///
66/// #[derive(Debug)]
67/// struct TestDomain;
68/// impl Domain for TestDomain {
69///     const DOMAIN_NAME: &'static str = "test";
70/// }
71/// impl KeyDomain for TestDomain {}
72///
73/// match validation::validate_key::<TestDomain>("") {
74///     Err(KeyParseError::Empty) => println!("Key is empty"),
75///     Err(e) => println!("Other error: {}", e),
76///     Ok(()) => println!("Key is valid"),
77/// }
78/// ```
79pub fn validate_key<T: KeyDomain>(key: &str) -> Result<(), KeyParseError> {
80    Key::<T>::new(key).map(|_| ())
81}
82
83/// Get validation help text for a domain
84///
85/// Returns the help text provided by the domain's `validation_help` method,
86/// if any. This can be useful for providing user-friendly error messages.
87///
88/// # Examples
89///
90/// ```rust
91/// use domain_key::{Domain, KeyDomain, validation};
92///
93/// #[derive(Debug)]
94/// struct TestDomain;
95/// impl Domain for TestDomain {
96///     const DOMAIN_NAME: &'static str = "test";
97/// }
98/// impl KeyDomain for TestDomain {
99///     fn validation_help() -> Option<&'static str> {
100///         Some("Keys must be alphanumeric with underscores")
101///     }
102/// }
103///
104/// if let Some(help) = validation::validation_help::<TestDomain>() {
105///     println!("Validation help: {}", help);
106/// }
107/// ```
108#[must_use]
109pub fn validation_help<T: KeyDomain>() -> Option<&'static str> {
110    T::validation_help()
111}
112
113/// Get detailed information about validation rules for a domain
114///
115/// Returns a formatted string containing comprehensive information about
116/// the domain's validation rules and characteristics.
117///
118/// # Examples
119///
120/// ```rust
121/// use domain_key::{Domain, KeyDomain, validation};
122///
123/// #[derive(Debug)]
124/// struct TestDomain;
125/// impl Domain for TestDomain {
126///     const DOMAIN_NAME: &'static str = "test";
127/// }
128/// impl KeyDomain for TestDomain {
129///     const MAX_LENGTH: usize = 32;
130/// }
131///
132/// let info = validation::validation_info::<TestDomain>();
133/// println!("{}", info);
134/// // Output:
135/// // Domain: test
136/// // Max length: 32
137/// ```
138#[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
176/// Validate multiple keys at once
177///
178/// This function validates a collection of keys and returns which ones
179/// are valid and which ones failed validation.
180///
181/// # Arguments
182///
183/// * `keys` - Iterator of string-like items to validate
184///
185/// # Returns
186///
187/// A tuple containing:
188/// - Vector of valid key strings
189/// - Vector of (invalid key string, error) pairs
190///
191/// # Examples
192///
193/// ```rust
194/// use domain_key::{Domain, KeyDomain, validation};
195///
196/// #[derive(Debug)]
197/// struct TestDomain;
198/// impl Domain for TestDomain {
199///     const DOMAIN_NAME: &'static str = "test";
200/// }
201/// impl KeyDomain for TestDomain {}
202///
203/// let keys = vec!["valid_key", "", "another_valid", "bad key"];
204/// let (valid, invalid) = validation::validate_batch::<TestDomain, _>(keys);
205///
206/// assert_eq!(valid.len(), 2);
207/// assert_eq!(invalid.len(), 2);
208/// ```
209pub 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
228/// Filter a collection of strings to only include valid keys
229///
230/// This function takes an iterator of strings and returns only those
231/// that would be valid keys for the specified domain.
232///
233/// # Examples
234///
235/// ```rust
236/// use domain_key::{Domain, KeyDomain, validation};
237///
238/// #[derive(Debug)]
239/// struct TestDomain;
240/// impl Domain for TestDomain {
241///     const DOMAIN_NAME: &'static str = "test";
242/// }
243/// impl KeyDomain for TestDomain {}
244///
245/// let candidates = vec!["valid_key", "", "another_valid", "bad key"];
246/// let valid_keys: Vec<_> = validation::filter_valid::<TestDomain, _>(candidates).collect();
247///
248/// assert_eq!(valid_keys.len(), 2);
249/// ```
250pub 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
259/// Count how many strings in a collection would be valid keys
260///
261/// This is more efficient than filtering when you only need the count.
262///
263/// # Examples
264///
265/// ```rust
266/// use domain_key::{Domain, KeyDomain, validation};
267///
268/// #[derive(Debug)]
269/// struct TestDomain;
270/// impl Domain for TestDomain {
271///     const DOMAIN_NAME: &'static str = "test";
272/// }
273/// impl KeyDomain for TestDomain {}
274///
275/// let candidates = vec!["valid_key", "", "another_valid", "bad key"];
276/// let count = validation::count_valid::<TestDomain, _>(candidates);
277///
278/// assert_eq!(count, 2);
279/// ```
280pub 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
290/// Check if all strings in a collection would be valid keys
291///
292/// Returns `true` only if every string in the collection would be a valid key.
293///
294/// # Examples
295///
296/// ```rust
297/// use domain_key::{Domain, KeyDomain, validation};
298///
299/// #[derive(Debug)]
300/// struct TestDomain;
301/// impl Domain for TestDomain {
302///     const DOMAIN_NAME: &'static str = "test";
303/// }
304/// impl KeyDomain for TestDomain {}
305///
306/// let all_valid = vec!["valid_key", "another_valid"];
307/// let mixed = vec!["valid_key", "", "another_valid"];
308///
309/// assert!(validation::all_valid::<TestDomain, _>(all_valid));
310/// assert!(!validation::all_valid::<TestDomain, _>(mixed));
311/// ```
312pub 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
320/// Check if any string in a collection would be a valid key
321///
322/// Returns `true` if at least one string in the collection would be a valid key.
323///
324/// # Examples
325///
326/// ```rust
327/// use domain_key::{Domain, KeyDomain, validation};
328///
329/// #[derive(Debug)]
330/// struct TestDomain;
331/// impl Domain for TestDomain {
332///     const DOMAIN_NAME: &'static str = "test";
333/// }
334/// impl KeyDomain for TestDomain {}
335///
336/// let mixed = vec!["", "valid_key", ""];
337/// let all_invalid = vec!["", ""];
338///
339/// assert!(validation::any_valid::<TestDomain, _>(mixed));
340/// assert!(!validation::any_valid::<TestDomain, _>(all_invalid));
341/// ```
342pub 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
350// ============================================================================
351// CONVENIENCE TRAITS
352// ============================================================================
353
354/// Helper trait for converting strings to keys
355///
356/// This trait provides convenient methods for converting various string types
357/// into keys with proper error handling.
358pub trait IntoKey<T: KeyDomain> {
359    /// Convert into a key, returning an error if validation fails
360    ///
361    /// # Errors
362    ///
363    /// Returns `KeyParseError` if the string fails validation for the domain
364    fn into_key(self) -> Result<Key<T>, KeyParseError>;
365
366    /// Convert into a key, returning None if validation fails
367    ///
368    /// This is useful when you want to filter out invalid keys rather than
369    /// handle errors explicitly.
370    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// ============================================================================
412// VALIDATION BUILDER
413// ============================================================================
414
415/// Builder for creating comprehensive validation configurations
416///
417/// This builder allows you to create complex validation scenarios with
418/// custom requirements and error handling.
419#[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    /// Create a new validation builder
436    #[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    /// Allow validation of empty collections
448    #[must_use]
449    pub fn allow_empty_collection(mut self, allow: bool) -> Self {
450        self.allow_empty_collection = allow;
451        self
452    }
453
454    /// Set maximum number of failures before stopping validation
455    #[must_use]
456    pub fn max_failures(mut self, max: usize) -> Self {
457        self.max_failures = Some(max);
458        self
459    }
460
461    /// Stop validation on the first error encountered
462    #[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    /// Add a custom validator function
469    #[must_use]
470    pub fn custom_validator(mut self, validator: ValidatorFunction) -> Self {
471        self.custom_validator = Some(validator);
472        self
473    }
474
475    /// Validate a collection of strings with the configured settings
476    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            // Check if we should stop due to error limits
497            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            // Validate with domain rules
508            match validate_key::<T>(key_str) {
509                Ok(()) => {
510                    // Apply custom validator if present - use normalized form
511                    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/// Result of a validation operation
534#[derive(Debug, Clone, PartialEq, Eq)]
535pub struct ValidationResult {
536    /// Number of items processed before stopping
537    pub total_processed: usize,
538    /// Valid key strings
539    pub valid: Vec<String>,
540    /// Invalid keys with their errors
541    pub errors: Vec<(String, KeyParseError)>,
542}
543
544impl ValidationResult {
545    /// Check if all processed items were valid
546    #[inline]
547    #[must_use]
548    pub fn is_success(&self) -> bool {
549        self.errors.is_empty()
550    }
551
552    /// Get the number of valid items
553    #[inline]
554    #[must_use]
555    pub fn valid_count(&self) -> usize {
556        self.valid.len()
557    }
558
559    /// Get the number of invalid items
560    #[inline]
561    #[must_use]
562    pub fn error_count(&self) -> usize {
563        self.errors.len()
564    }
565
566    /// Get the success rate as a percentage
567    #[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    /// Convert all valid strings to keys
579    ///
580    /// # Errors
581    ///
582    /// Returns `KeyParseError` if any valid string fails key creation
583    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    /// Try to convert all valid strings to keys, ignoring failures
591    #[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// ============================================================================
601// UTILITY FUNCTIONS
602// ============================================================================
603
604/// Create a validation builder with common settings for strict validation
605#[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/// Create a validation builder with common settings for lenient validation
613#[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
620/// Quickly validate and convert a collection of strings to keys
621///
622/// This is a convenience function that combines validation and conversion
623/// in a single step.
624///
625/// # Examples
626///
627/// ```rust
628/// use domain_key::{Domain, KeyDomain, validation};
629///
630/// #[derive(Debug)]
631/// struct TestDomain;
632/// impl Domain for TestDomain {
633///     const DOMAIN_NAME: &'static str = "test";
634/// }
635/// impl KeyDomain for TestDomain {}
636///
637/// let strings = vec!["key1", "key2", "key3"];
638/// let keys = validation::quick_convert::<TestDomain, _>(strings).unwrap();
639///
640/// assert_eq!(keys.len(), 3);
641/// ```
642///
643/// # Errors
644///
645/// Returns a vector of validation errors if any keys fail validation
646pub 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// ============================================================================
670// TESTS
671// ============================================================================
672
673#[cfg(test)]
674mod tests {
675    use super::*;
676
677    // Test domain
678    #[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        // Debug output to understand what's happening
788        #[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        // The builder has max_failures(2), so it should stop after 2 failures
798        // Input: ["valid1", "", "valid2", "", "valid3"]
799        // Processing:
800        // 1. "valid1" -> valid (valid_count = 1)
801        // 2. "" -> error (error_count = 1)
802        // 3. "valid2" -> valid (valid_count = 2)
803        // 4. "" -> error (error_count = 2, max_failures reached, stop processing)
804        // 5. "valid3" -> not processed
805
806        assert_eq!(result.valid_count(), 2); // "valid1", "valid2"
807        assert_eq!(result.error_count(), 2); // two empty strings
808        assert!(!result.is_success()); // has errors
809        assert_eq!(result.total_processed, 4); // processed 4 items before stopping
810        assert!(result.success_rate() > 40.0 && result.success_rate() <= 60.0); // 2/4 = 50%
811    }
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        // Should stop on first error (empty string)
823        assert_eq!(result.total_processed, 2); // "valid" + "" (error)
824        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        // Should process all items
838        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        // Should stop on first error (empty string)
872        assert_eq!(result.total_processed, 2); // "valid" + "" (error)
873        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        // Should process all items
884        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}