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            #[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    /// Convert all valid strings to keys
582    ///
583    /// # Errors
584    ///
585    /// Returns `KeyParseError` if any valid string fails key creation
586    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    /// Try to convert all valid strings to keys, ignoring failures
594    #[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// ============================================================================
604// UTILITY FUNCTIONS
605// ============================================================================
606
607/// Create a validation builder with common settings for strict validation
608#[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/// Create a validation builder with common settings for lenient validation
616#[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
623/// Quickly validate and convert a collection of strings to keys
624///
625/// This is a convenience function that combines validation and conversion
626/// in a single step.
627///
628/// # Examples
629///
630/// ```rust
631/// use domain_key::{Domain, KeyDomain, validation};
632///
633/// #[derive(Debug)]
634/// struct TestDomain;
635/// impl Domain for TestDomain {
636///     const DOMAIN_NAME: &'static str = "test";
637/// }
638/// impl KeyDomain for TestDomain {}
639///
640/// let strings = vec!["key1", "key2", "key3"];
641/// let keys = validation::quick_convert::<TestDomain, _>(strings).unwrap();
642///
643/// assert_eq!(keys.len(), 3);
644/// ```
645///
646/// # Errors
647///
648/// Returns a vector of validation errors if any keys fail validation
649pub 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// ============================================================================
673// TESTS
674// ============================================================================
675
676#[cfg(test)]
677mod tests {
678    use super::*;
679
680    // Test domain
681    #[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        // Debug output to understand what's happening
791        #[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        // The builder has max_failures(2), so it should stop after 2 failures
801        // Input: ["valid1", "", "valid2", "", "valid3"]
802        // Processing:
803        // 1. "valid1" -> valid (valid_count = 1)
804        // 2. "" -> error (error_count = 1)
805        // 3. "valid2" -> valid (valid_count = 2)
806        // 4. "" -> error (error_count = 2, max_failures reached, stop processing)
807        // 5. "valid3" -> not processed
808
809        assert_eq!(result.valid_count(), 2); // "valid1", "valid2"
810        assert_eq!(result.error_count(), 2); // two empty strings
811        assert!(!result.is_success()); // has errors
812        assert_eq!(result.total_processed, 4); // processed 4 items before stopping
813        assert!(result.success_rate() > 40.0 && result.success_rate() <= 60.0); // 2/4 = 50%
814    }
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        // Should stop on first error (empty string)
826        assert_eq!(result.total_processed, 2); // "valid" + "" (error)
827        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        // Should process all items
841        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        // Should stop on first error (empty string)
875        assert_eq!(result.total_processed, 2); // "valid" + "" (error)
876        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        // Should process all items
887        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}