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#[must_use]
47pub fn is_valid_key<T: KeyDomain>(key: &str) -> bool {
48    validate_key::<T>(key).is_ok()
49}
50
51/// Validate a key string and return detailed error information
52///
53/// This performs the same validation as `Key::new` but without creating
54/// the key object, making it useful for validation-only scenarios.
55///
56/// # Errors
57///
58/// Returns `KeyParseError` if the key fails common or domain-specific validation
59///
60/// # Examples
61///
62/// ```rust
63/// use domain_key::{Domain, KeyDomain, validation, KeyParseError};
64///
65/// #[derive(Debug)]
66/// struct TestDomain;
67/// impl Domain for TestDomain {
68///     const DOMAIN_NAME: &'static str = "test";
69/// }
70/// impl KeyDomain for TestDomain {}
71///
72/// match validation::validate_key::<TestDomain>("") {
73///     Err(KeyParseError::Empty) => println!("Key is empty"),
74///     Err(e) => println!("Other error: {}", e),
75///     Ok(()) => println!("Key is valid"),
76/// }
77/// ```
78pub 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/// Get validation help text for a domain
88///
89/// Returns the help text provided by the domain's `validation_help` method,
90/// if any. This can be useful for providing user-friendly error messages.
91///
92/// # Examples
93///
94/// ```rust
95/// use domain_key::{Domain, KeyDomain, validation};
96///
97/// #[derive(Debug)]
98/// struct TestDomain;
99/// impl Domain for TestDomain {
100///     const DOMAIN_NAME: &'static str = "test";
101/// }
102/// impl KeyDomain for TestDomain {
103///     fn validation_help() -> Option<&'static str> {
104///         Some("Keys must be alphanumeric with underscores")
105///     }
106/// }
107///
108/// if let Some(help) = validation::validation_help::<TestDomain>() {
109///     println!("Validation help: {}", help);
110/// }
111/// ```
112#[must_use]
113pub fn validation_help<T: KeyDomain>() -> Option<&'static str> {
114    T::validation_help()
115}
116
117/// Get detailed information about validation rules for a domain
118///
119/// Returns a formatted string containing comprehensive information about
120/// the domain's validation rules and characteristics.
121///
122/// # Examples
123///
124/// ```rust
125/// use domain_key::{Domain, KeyDomain, validation};
126///
127/// #[derive(Debug)]
128/// struct TestDomain;
129/// impl Domain for TestDomain {
130///     const DOMAIN_NAME: &'static str = "test";
131/// }
132/// impl KeyDomain for TestDomain {
133///     const MAX_LENGTH: usize = 32;
134/// }
135///
136/// let info = validation::validation_info::<TestDomain>();
137/// println!("{}", info);
138/// // Output:
139/// // Domain: test
140/// // Max length: 32
141/// ```
142#[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
180/// Validate multiple keys at once
181///
182/// This function validates a collection of keys and returns which ones
183/// are valid and which ones failed validation.
184///
185/// # Arguments
186///
187/// * `keys` - Iterator of string-like items to validate
188///
189/// # Returns
190///
191/// A tuple containing:
192/// - Vector of valid key strings
193/// - Vector of (invalid key string, error) pairs
194///
195/// # Examples
196///
197/// ```rust
198/// use domain_key::{Domain, KeyDomain, validation};
199///
200/// #[derive(Debug)]
201/// struct TestDomain;
202/// impl Domain for TestDomain {
203///     const DOMAIN_NAME: &'static str = "test";
204/// }
205/// impl KeyDomain for TestDomain {}
206///
207/// let keys = vec!["valid_key", "", "another_valid", "bad key"];
208/// let (valid, invalid) = validation::validate_batch::<TestDomain, _>(keys);
209///
210/// assert_eq!(valid.len(), 2);
211/// assert_eq!(invalid.len(), 2);
212/// ```
213pub 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
232/// Filter a collection of strings to only include valid keys
233///
234/// This function takes an iterator of strings and returns only those
235/// that would be valid keys for the specified domain.
236///
237/// # Examples
238///
239/// ```rust
240/// use domain_key::{Domain, KeyDomain, validation};
241///
242/// #[derive(Debug)]
243/// struct TestDomain;
244/// impl Domain for TestDomain {
245///     const DOMAIN_NAME: &'static str = "test";
246/// }
247/// impl KeyDomain for TestDomain {}
248///
249/// let candidates = vec!["valid_key", "", "another_valid", "bad key"];
250/// let valid_keys: Vec<_> = validation::filter_valid::<TestDomain, _>(candidates).collect();
251///
252/// assert_eq!(valid_keys.len(), 2);
253/// ```
254pub 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
269/// Count how many strings in a collection would be valid keys
270///
271/// This is more efficient than filtering when you only need the count.
272///
273/// # Examples
274///
275/// ```rust
276/// use domain_key::{Domain, KeyDomain, validation};
277///
278/// #[derive(Debug)]
279/// struct TestDomain;
280/// impl Domain for TestDomain {
281///     const DOMAIN_NAME: &'static str = "test";
282/// }
283/// impl KeyDomain for TestDomain {}
284///
285/// let candidates = vec!["valid_key", "", "another_valid", "bad key"];
286/// let count = validation::count_valid::<TestDomain, _>(candidates);
287///
288/// assert_eq!(count, 2);
289/// ```
290pub 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
300/// Check if all strings in a collection would be valid keys
301///
302/// Returns `true` only if every string in the collection would be a valid key.
303///
304/// # Examples
305///
306/// ```rust
307/// use domain_key::{Domain, KeyDomain, validation};
308///
309/// #[derive(Debug)]
310/// struct TestDomain;
311/// impl Domain for TestDomain {
312///     const DOMAIN_NAME: &'static str = "test";
313/// }
314/// impl KeyDomain for TestDomain {}
315///
316/// let all_valid = vec!["valid_key", "another_valid"];
317/// let mixed = vec!["valid_key", "", "another_valid"];
318///
319/// assert!(validation::all_valid::<TestDomain, _>(all_valid));
320/// assert!(!validation::all_valid::<TestDomain, _>(mixed));
321/// ```
322pub 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
330/// Check if any string in a collection would be a valid key
331///
332/// Returns `true` if at least one string in the collection would be a valid key.
333///
334/// # Examples
335///
336/// ```rust
337/// use domain_key::{Domain, KeyDomain, validation};
338///
339/// #[derive(Debug)]
340/// struct TestDomain;
341/// impl Domain for TestDomain {
342///     const DOMAIN_NAME: &'static str = "test";
343/// }
344/// impl KeyDomain for TestDomain {}
345///
346/// let mixed = vec!["", "valid_key", ""];
347/// let all_invalid = vec!["", ""];
348///
349/// assert!(validation::any_valid::<TestDomain, _>(mixed));
350/// assert!(!validation::any_valid::<TestDomain, _>(all_invalid));
351/// ```
352pub 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
360// ============================================================================
361// CONVENIENCE TRAITS
362// ============================================================================
363
364/// Helper trait for converting strings to keys
365///
366/// This trait provides convenient methods for converting various string types
367/// into keys with proper error handling.
368pub trait IntoKey<T: KeyDomain> {
369    /// Convert into a key, returning an error if validation fails
370    ///
371    /// # Errors
372    ///
373    /// Returns `KeyParseError` if the string fails validation for the domain
374    fn into_key(self) -> Result<Key<T>, KeyParseError>;
375
376    /// Convert into a key, returning None if validation fails
377    ///
378    /// This is useful when you want to filter out invalid keys rather than
379    /// handle errors explicitly.
380    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// ============================================================================
422// VALIDATION BUILDER
423// ============================================================================
424
425/// Builder for creating comprehensive validation configurations
426///
427/// This builder allows you to create complex validation scenarios with
428/// custom requirements and error handling.
429#[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    /// Create a new validation builder
446    #[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    /// Allow validation of empty collections
458    #[must_use]
459    pub fn allow_empty_collection(mut self, allow: bool) -> Self {
460        self.allow_empty_collection = allow;
461        self
462    }
463
464    /// Set maximum number of failures before stopping validation
465    #[must_use]
466    pub fn max_failures(mut self, max: usize) -> Self {
467        self.max_failures = Some(max);
468        self
469    }
470
471    /// Stop validation on the first error encountered
472    #[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    /// Add a custom validator function
479    #[must_use]
480    pub fn custom_validator(mut self, validator: ValidatorFunction) -> Self {
481        self.custom_validator = Some(validator);
482        self
483    }
484
485    /// Validate a collection of strings with the configured settings
486    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            // Check if we should stop due to error limits
507            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            // Validate with domain rules
518            match validate_key::<T>(key_str) {
519                Ok(()) => {
520                    // Apply custom validator if present
521                    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/// Result of a validation operation
543#[derive(Debug, Clone, PartialEq, Eq)]
544pub struct ValidationResult {
545    /// Number of items processed before stopping
546    pub total_processed: usize,
547    /// Valid key strings
548    pub valid: Vec<String>,
549    /// Invalid keys with their errors
550    pub errors: Vec<(String, KeyParseError)>,
551}
552
553impl ValidationResult {
554    /// Check if all processed items were valid
555    #[must_use]
556    pub fn is_success(&self) -> bool {
557        self.errors.is_empty()
558    }
559
560    /// Get the number of valid items
561    #[must_use]
562    pub fn valid_count(&self) -> usize {
563        self.valid.len()
564    }
565
566    /// Get the number of invalid items
567    #[must_use]
568    pub fn error_count(&self) -> usize {
569        self.errors.len()
570    }
571
572    /// Get the success rate as a percentage
573    #[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    /// Convert all valid strings to keys
585    ///
586    /// # Errors
587    ///
588    /// Returns `KeyParseError` if any valid string fails key creation
589    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    /// Try to convert all valid strings to keys, ignoring failures
597    #[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// ============================================================================
607// UTILITY FUNCTIONS
608// ============================================================================
609
610/// Create a validation builder with common settings for strict validation
611#[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/// Create a validation builder with common settings for lenient validation
619#[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
626/// Quickly validate and convert a collection of strings to keys
627///
628/// This is a convenience function that combines validation and conversion
629/// in a single step.
630///
631/// # Examples
632///
633/// ```rust
634/// use domain_key::{Domain, KeyDomain, validation};
635///
636/// #[derive(Debug)]
637/// struct TestDomain;
638/// impl Domain for TestDomain {
639///     const DOMAIN_NAME: &'static str = "test";
640/// }
641/// impl KeyDomain for TestDomain {}
642///
643/// let strings = vec!["key1", "key2", "key3"];
644/// let keys = validation::quick_convert::<TestDomain, _>(strings).unwrap();
645///
646/// assert_eq!(keys.len(), 3);
647/// ```
648///
649/// # Errors
650///
651/// Returns a vector of validation errors if any keys fail validation
652pub 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// ============================================================================
671// TESTS
672// ============================================================================
673
674#[cfg(test)]
675mod tests {
676    use super::*;
677
678    // Test domain
679    #[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        // Debug output to understand what's happening
789        #[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        // The builder has max_failures(2), so it should stop after 2 failures
799        // Input: ["valid1", "", "valid2", "", "valid3"]
800        // Processing:
801        // 1. "valid1" -> valid (valid_count = 1)
802        // 2. "" -> error (error_count = 1)
803        // 3. "valid2" -> valid (valid_count = 2)
804        // 4. "" -> error (error_count = 2, max_failures reached, stop processing)
805        // 5. "valid3" -> not processed
806
807        assert_eq!(result.valid_count(), 2); // "valid1", "valid2"
808        assert_eq!(result.error_count(), 2); // two empty strings
809        assert!(!result.is_success()); // has errors
810        assert_eq!(result.total_processed, 4); // processed 4 items before stopping
811        assert!(result.success_rate() > 40.0 && result.success_rate() <= 60.0); // 2/4 = 50%
812    }
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        // Should stop on first error (empty string)
824        assert_eq!(result.total_processed, 2); // "valid" + "" (error)
825        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        // Should process all items
839        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        // Should stop on first error (empty string)
873        assert_eq!(result.total_processed, 2); // "valid" + "" (error)
874        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        // Should process all items
885        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}