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// COMPILE-TIME VALIDATION
24// ============================================================================
25
26/// Returns `true` if `b` is an ASCII byte that is allowed anywhere inside a
27/// default-domain key: alphanumeric, `_`, `-`, `.`.
28const fn is_allowed_key_byte_default(b: u8) -> bool {
29    matches!(b, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'_' | b'-' | b'.')
30}
31
32/// Returns `true` if `b` is one of the separator bytes that must not appear
33/// consecutively: `_`, `-`, `.`.
34const fn is_separator_byte_default(b: u8) -> bool {
35    matches!(b, b'_' | b'-' | b'.')
36}
37
38/// Returns `true` if `b` is allowed as the **last** byte of a default-domain
39/// key.
40///
41/// The default `allowed_end_character` excludes `_`, `-`, `.`, so only ASCII
42/// alphanumeric bytes are valid at the end position.
43const fn is_allowed_end_byte_default(b: u8) -> bool {
44    matches!(b, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z')
45}
46
47/// Validate a key string against the **default** [`KeyDomain`] rules at
48/// compile time.
49///
50/// This `const fn` mirrors the runtime validation that [`Key::new`] performs
51/// for domains that rely entirely on the default implementations of every
52/// [`KeyDomain`] method — i.e. domains that do **not** override
53/// `validate_domain_rules`, `allowed_characters`, `allowed_start_character`,
54/// `allowed_end_character`, `allowed_consecutive_characters`, or `min_length`.
55///
56/// Because it is a `const fn` it can be evaluated at compile time, making it
57/// suitable for `const` assertions and inside [`static_key!`].
58///
59/// # What is checked
60///
61/// | Rule | Detail |
62/// |------|--------|
63/// | Non-empty | `s.len() > 0` |
64/// | Max length | `s.len() <= max_length` |
65/// | Allowed bytes | ASCII alphanumeric, `_`, `-`, `.` |
66/// | End byte | ASCII alphanumeric only (not `_`, `-`, `.`) |
67/// | Consecutive separators | `__`, `--`, `..` are rejected |
68///
69/// # What is **not** checked
70///
71/// - Custom domain-specific rules (`validate_domain_rules`)
72/// - Whitespace trimming — the input is taken exactly as-is; leading or
73///   trailing whitespace will cause validation to fail, which is intentional
74///   for compile-time literals
75/// - Unicode — always rejected because the default `allowed_characters` only
76///   permits ASCII bytes
77///
78/// # Examples
79///
80/// ```rust
81/// use domain_key::{is_valid_key_default, DEFAULT_MAX_KEY_LENGTH};
82///
83/// // Evaluated entirely at compile time — zero runtime cost
84/// const GOOD: bool = is_valid_key_default("user_123", DEFAULT_MAX_KEY_LENGTH);
85/// assert!(GOOD);
86///
87/// const EMPTY: bool = is_valid_key_default("", DEFAULT_MAX_KEY_LENGTH);
88/// assert!(!EMPTY);
89///
90/// const TRAILING_SEP: bool = is_valid_key_default("foo_", DEFAULT_MAX_KEY_LENGTH);
91/// assert!(!TRAILING_SEP);
92///
93/// const CONSECUTIVE: bool = is_valid_key_default("a__b", DEFAULT_MAX_KEY_LENGTH);
94/// assert!(!CONSECUTIVE);
95///
96/// const WITH_SPACE: bool = is_valid_key_default("hello world", DEFAULT_MAX_KEY_LENGTH);
97/// assert!(!WITH_SPACE);
98/// ```
99#[must_use]
100pub const fn is_valid_key_default(s: &str, max_length: usize) -> bool {
101    let bytes = s.as_bytes();
102    let len = bytes.len();
103
104    // Rule 1: non-empty
105    if len == 0 {
106        return false;
107    }
108
109    // Rule 2: max length
110    if len > max_length {
111        return false;
112    }
113
114    // Rule 3: every byte must be an allowed key byte; consecutive separators
115    // are rejected.
116    //
117    // NOTE: The runtime `validate_fast` checks the first character via
118    // `is_key_char_fast(first) || T::allowed_start_character(first)`.
119    // Because `is_key_char_fast` already allows `_`, `-`, `.`, the first
120    // character of a default-domain key CAN be a separator.  We therefore
121    // validate all positions uniformly here.
122    let mut i = 0;
123    while i < len {
124        let b = bytes[i];
125
126        if !is_allowed_key_byte_default(b) {
127            return false;
128        }
129
130        // Reject consecutive identical separators: __, --, ..
131        if i > 0 && is_separator_byte_default(b) && bytes[i - 1] == b {
132            return false;
133        }
134
135        i += 1;
136    }
137
138    // Rule 4: last byte must be alphanumeric (mirrors default
139    // `allowed_end_character`, which excludes `_`, `-`, `.`)
140    if !is_allowed_end_byte_default(bytes[len - 1]) {
141        return false;
142    }
143
144    true
145}
146
147// ============================================================================
148// VALIDATION FUNCTIONS
149// ============================================================================
150
151/// Check if a string would be a valid key for a domain without creating the key
152///
153/// This is useful for pre-validation or filtering operations where you don't
154/// need the actual key object.
155///
156/// # Examples
157///
158/// ```rust
159/// use domain_key::{Domain, KeyDomain, validation};
160///
161/// #[derive(Debug)]
162/// struct TestDomain;
163/// impl Domain for TestDomain {
164///     const DOMAIN_NAME: &'static str = "test";
165/// }
166/// impl KeyDomain for TestDomain {}
167///
168/// assert!(validation::is_valid_key::<TestDomain>("good_key"));
169/// assert!(!validation::is_valid_key::<TestDomain>(""));
170/// ```
171#[inline]
172#[must_use]
173pub fn is_valid_key<T: KeyDomain>(key: &str) -> bool {
174    validate_key::<T>(key).is_ok()
175}
176
177/// Validate a key string and return detailed error information
178///
179/// This performs the same validation as `Key::new` but without creating
180/// the key object, making it useful for validation-only scenarios.
181///
182/// # Errors
183///
184/// Returns `KeyParseError` if the key fails common or domain-specific validation
185///
186/// # Examples
187///
188/// ```rust
189/// use domain_key::{Domain, KeyDomain, validation, KeyParseError};
190///
191/// #[derive(Debug)]
192/// struct TestDomain;
193/// impl Domain for TestDomain {
194///     const DOMAIN_NAME: &'static str = "test";
195/// }
196/// impl KeyDomain for TestDomain {}
197///
198/// match validation::validate_key::<TestDomain>("") {
199///     Err(KeyParseError::Empty) => println!("Key is empty"),
200///     Err(e) => println!("Other error: {}", e),
201///     Ok(()) => println!("Key is valid"),
202/// }
203/// ```
204pub fn validate_key<T: KeyDomain>(key: &str) -> Result<(), KeyParseError> {
205    Key::<T>::new(key).map(|_| ())
206}
207
208/// Get validation help text for a domain
209///
210/// Returns the help text provided by the domain's `validation_help` method,
211/// if any. This can be useful for providing user-friendly error messages.
212///
213/// # Examples
214///
215/// ```rust
216/// use domain_key::{Domain, KeyDomain, validation};
217///
218/// #[derive(Debug)]
219/// struct TestDomain;
220/// impl Domain for TestDomain {
221///     const DOMAIN_NAME: &'static str = "test";
222/// }
223/// impl KeyDomain for TestDomain {
224///     fn validation_help() -> Option<&'static str> {
225///         Some("Keys must be alphanumeric with underscores")
226///     }
227/// }
228///
229/// if let Some(help) = validation::validation_help::<TestDomain>() {
230///     println!("Validation help: {}", help);
231/// }
232/// ```
233#[must_use]
234pub fn validation_help<T: KeyDomain>() -> Option<&'static str> {
235    T::validation_help()
236}
237
238/// Get detailed information about validation rules for a domain
239///
240/// Returns a formatted string containing comprehensive information about
241/// the domain's validation rules and characteristics.
242///
243/// # Examples
244///
245/// ```rust
246/// use domain_key::{Domain, KeyDomain, validation};
247///
248/// #[derive(Debug)]
249/// struct TestDomain;
250/// impl Domain for TestDomain {
251///     const DOMAIN_NAME: &'static str = "test";
252/// }
253/// impl KeyDomain for TestDomain {
254///     const MAX_LENGTH: usize = 32;
255/// }
256///
257/// let info = validation::validation_info::<TestDomain>();
258/// println!("{}", info);
259/// // Output:
260/// // Domain: test
261/// // Max length: 32
262/// ```
263#[must_use]
264pub fn validation_info<T: KeyDomain>() -> String {
265    let mut info = format!("Domain: {}\n", T::DOMAIN_NAME);
266    writeln!(info, "Max length: {}", T::MAX_LENGTH).unwrap();
267    writeln!(info, "Min length: {}", T::min_length()).unwrap();
268    writeln!(info, "Expected length: {}", T::EXPECTED_LENGTH).unwrap();
269    writeln!(info, "Case insensitive: {}", T::CASE_INSENSITIVE).unwrap();
270    writeln!(info, "Custom validation: {}", T::HAS_CUSTOM_VALIDATION).unwrap();
271    writeln!(
272        info,
273        "Custom normalization: {}",
274        T::HAS_CUSTOM_NORMALIZATION,
275    )
276    .unwrap();
277
278    writeln!(info, "Default separator: '{}'", T::default_separator()).unwrap();
279
280    if let Some(help) = T::validation_help() {
281        info.push_str("Help: ");
282        info.push_str(help);
283        info.push('\n');
284    }
285
286    let examples = T::examples();
287    if !examples.is_empty() {
288        info.push_str("Examples: ");
289        for (i, example) in examples.iter().enumerate() {
290            if i > 0 {
291                info.push_str(", ");
292            }
293            info.push_str(example);
294        }
295        info.push('\n');
296    }
297
298    info
299}
300
301/// Validate multiple keys at once
302///
303/// This function validates a collection of keys and returns which ones
304/// are valid and which ones failed validation.
305///
306/// # Arguments
307///
308/// * `keys` - Iterator of string-like items to validate
309///
310/// # Returns
311///
312/// A tuple containing:
313/// - Vector of valid key strings
314/// - Vector of (invalid key string, error) pairs
315///
316/// # Examples
317///
318/// ```rust
319/// use domain_key::{Domain, KeyDomain, validation};
320///
321/// #[derive(Debug)]
322/// struct TestDomain;
323/// impl Domain for TestDomain {
324///     const DOMAIN_NAME: &'static str = "test";
325/// }
326/// impl KeyDomain for TestDomain {}
327///
328/// let keys = vec!["valid_key", "", "another_valid", "bad key"];
329/// let (valid, invalid) = validation::validate_batch::<TestDomain, _>(keys);
330///
331/// assert_eq!(valid.len(), 2);
332/// assert_eq!(invalid.len(), 2);
333/// ```
334pub fn validate_batch<T: KeyDomain, I>(keys: I) -> (Vec<String>, Vec<(String, KeyParseError)>)
335where
336    I: IntoIterator,
337    I::Item: AsRef<str>,
338{
339    let mut valid = Vec::new();
340    let mut invalid = Vec::new();
341
342    for key in keys {
343        let key_str = key.as_ref();
344        match validate_key::<T>(key_str) {
345            Ok(()) => valid.push(key_str.to_string()),
346            Err(e) => invalid.push((key_str.to_string(), e)),
347        }
348    }
349
350    (valid, invalid)
351}
352
353/// Filter a collection of strings to only include valid keys
354///
355/// This function takes an iterator of strings and returns only those
356/// that would be valid keys for the specified domain.
357///
358/// # Examples
359///
360/// ```rust
361/// use domain_key::{Domain, KeyDomain, validation};
362///
363/// #[derive(Debug)]
364/// struct TestDomain;
365/// impl Domain for TestDomain {
366///     const DOMAIN_NAME: &'static str = "test";
367/// }
368/// impl KeyDomain for TestDomain {}
369///
370/// let candidates = vec!["valid_key", "", "another_valid", "bad key"];
371/// let valid_keys: Vec<_> = validation::filter_valid::<TestDomain, _>(candidates).collect();
372///
373/// assert_eq!(valid_keys.len(), 2);
374/// ```
375pub fn filter_valid<T: KeyDomain, I>(keys: I) -> impl Iterator<Item = I::Item>
376where
377    I: IntoIterator,
378    I::Item: AsRef<str>,
379{
380    keys.into_iter()
381        .filter(|key| is_valid_key::<T>(key.as_ref()))
382}
383
384/// Count how many strings in a collection would be valid keys
385///
386/// This is more efficient than filtering when you only need the count.
387///
388/// # Examples
389///
390/// ```rust
391/// use domain_key::{Domain, KeyDomain, validation};
392///
393/// #[derive(Debug)]
394/// struct TestDomain;
395/// impl Domain for TestDomain {
396///     const DOMAIN_NAME: &'static str = "test";
397/// }
398/// impl KeyDomain for TestDomain {}
399///
400/// let candidates = vec!["valid_key", "", "another_valid", "bad key"];
401/// let count = validation::count_valid::<TestDomain, _>(candidates);
402///
403/// assert_eq!(count, 2);
404/// ```
405pub fn count_valid<T: KeyDomain, I>(keys: I) -> usize
406where
407    I: IntoIterator,
408    I::Item: AsRef<str>,
409{
410    keys.into_iter()
411        .filter(|key| is_valid_key::<T>(key.as_ref()))
412        .count()
413}
414
415/// Check if all strings in a collection would be valid keys
416///
417/// Returns `true` only if every string in the collection would be a valid key.
418///
419/// # Examples
420///
421/// ```rust
422/// use domain_key::{Domain, KeyDomain, validation};
423///
424/// #[derive(Debug)]
425/// struct TestDomain;
426/// impl Domain for TestDomain {
427///     const DOMAIN_NAME: &'static str = "test";
428/// }
429/// impl KeyDomain for TestDomain {}
430///
431/// let all_valid = vec!["valid_key", "another_valid"];
432/// let mixed = vec!["valid_key", "", "another_valid"];
433///
434/// assert!(validation::all_valid::<TestDomain, _>(all_valid));
435/// assert!(!validation::all_valid::<TestDomain, _>(mixed));
436/// ```
437pub fn all_valid<T: KeyDomain, I>(keys: I) -> bool
438where
439    I: IntoIterator,
440    I::Item: AsRef<str>,
441{
442    keys.into_iter().all(|key| is_valid_key::<T>(key.as_ref()))
443}
444
445/// Check if any string in a collection would be a valid key
446///
447/// Returns `true` if at least one string in the collection would be a valid key.
448///
449/// # Examples
450///
451/// ```rust
452/// use domain_key::{Domain, KeyDomain, validation};
453///
454/// #[derive(Debug)]
455/// struct TestDomain;
456/// impl Domain for TestDomain {
457///     const DOMAIN_NAME: &'static str = "test";
458/// }
459/// impl KeyDomain for TestDomain {}
460///
461/// let mixed = vec!["", "valid_key", ""];
462/// let all_invalid = vec!["", ""];
463///
464/// assert!(validation::any_valid::<TestDomain, _>(mixed));
465/// assert!(!validation::any_valid::<TestDomain, _>(all_invalid));
466/// ```
467pub fn any_valid<T: KeyDomain, I>(keys: I) -> bool
468where
469    I: IntoIterator,
470    I::Item: AsRef<str>,
471{
472    keys.into_iter().any(|key| is_valid_key::<T>(key.as_ref()))
473}
474
475// ============================================================================
476// CONVENIENCE TRAITS
477// ============================================================================
478
479/// Helper trait for converting strings to keys
480///
481/// This trait provides convenient methods for converting various string types
482/// into keys with proper error handling.
483pub trait IntoKey<T: KeyDomain> {
484    /// Convert into a key, returning an error if validation fails
485    ///
486    /// # Errors
487    ///
488    /// Returns `KeyParseError` if the string fails validation for the domain
489    fn into_key(self) -> Result<Key<T>, KeyParseError>;
490
491    /// Convert into a key, returning None if validation fails
492    ///
493    /// This is useful when you want to filter out invalid keys rather than
494    /// handle errors explicitly.
495    fn try_into_key(self) -> Option<Key<T>>;
496}
497
498impl<T: KeyDomain> IntoKey<T> for &str {
499    #[inline]
500    fn into_key(self) -> Result<Key<T>, KeyParseError> {
501        Key::new(self)
502    }
503
504    #[inline]
505    fn try_into_key(self) -> Option<Key<T>> {
506        Key::try_new(self)
507    }
508}
509
510impl<T: KeyDomain> IntoKey<T> for String {
511    #[inline]
512    fn into_key(self) -> Result<Key<T>, KeyParseError> {
513        Key::from_string(self)
514    }
515
516    #[inline]
517    fn try_into_key(self) -> Option<Key<T>> {
518        Key::from_string(self).ok()
519    }
520}
521
522impl<T: KeyDomain> IntoKey<T> for &String {
523    #[inline]
524    fn into_key(self) -> Result<Key<T>, KeyParseError> {
525        Key::new(self)
526    }
527
528    #[inline]
529    fn try_into_key(self) -> Option<Key<T>> {
530        Key::try_new(self)
531    }
532}
533
534type ValidatorFunction = fn(&str) -> Result<(), KeyParseError>;
535
536// ============================================================================
537// VALIDATION BUILDER
538// ============================================================================
539
540/// Builder for creating comprehensive validation configurations
541///
542/// This builder allows you to create complex validation scenarios with
543/// custom requirements and error handling.
544#[derive(Debug)]
545pub struct ValidationBuilder<T: KeyDomain> {
546    allow_empty_collection: bool,
547    max_failures: Option<usize>,
548    stop_on_first_error: bool,
549    custom_validator: Option<ValidatorFunction>,
550    _phantom: core::marker::PhantomData<T>,
551}
552
553impl<T: KeyDomain> Default for ValidationBuilder<T> {
554    fn default() -> Self {
555        Self::new()
556    }
557}
558
559impl<T: KeyDomain> ValidationBuilder<T> {
560    /// Create a new validation builder
561    #[must_use]
562    pub fn new() -> Self {
563        Self {
564            allow_empty_collection: false,
565            max_failures: None,
566            stop_on_first_error: false,
567            custom_validator: None,
568            _phantom: core::marker::PhantomData,
569        }
570    }
571
572    /// Allow validation of empty collections
573    #[must_use]
574    pub fn allow_empty_collection(mut self, allow: bool) -> Self {
575        self.allow_empty_collection = allow;
576        self
577    }
578
579    /// Set maximum number of failures before stopping validation
580    #[must_use]
581    pub fn max_failures(mut self, max: usize) -> Self {
582        self.max_failures = Some(max);
583        self
584    }
585
586    /// Stop validation on the first error encountered
587    #[must_use]
588    pub fn stop_on_first_error(mut self, stop: bool) -> Self {
589        self.stop_on_first_error = stop;
590        self
591    }
592
593    /// Add a custom validator function
594    #[must_use]
595    pub fn custom_validator(mut self, validator: ValidatorFunction) -> Self {
596        self.custom_validator = Some(validator);
597        self
598    }
599
600    /// Validate a collection of strings with the configured settings
601    pub fn validate<I>(&self, keys: I) -> ValidationResult
602    where
603        I: IntoIterator,
604        I::Item: AsRef<str>,
605    {
606        let mut valid = Vec::new();
607        let mut errors = Vec::new();
608        let mut keys = keys.into_iter().peekable();
609
610        if keys.peek().is_none() && !self.allow_empty_collection {
611            return ValidationResult {
612                valid,
613                errors: vec![(String::new(), KeyParseError::Empty)],
614                total_processed: 0,
615            };
616        }
617
618        for key in keys {
619            let key_str = key.as_ref();
620
621            // Check if we should stop due to error limits
622            if let Some(max) = self.max_failures {
623                if errors.len() >= max {
624                    break;
625                }
626            }
627
628            if self.stop_on_first_error && !errors.is_empty() {
629                break;
630            }
631
632            // Validate with domain rules
633            match validate_key::<T>(key_str) {
634                Ok(()) => {
635                    // Apply custom validator if present - use normalized form
636                    let normalized = Key::<T>::normalize(key_str);
637                    if let Some(custom) = self.custom_validator {
638                        match custom(&normalized) {
639                            Ok(()) => valid.push(normalized.into_owned()),
640                            Err(e) => errors.push((normalized.into_owned(), e)),
641                        }
642                    } else {
643                        valid.push(normalized.into_owned());
644                    }
645                }
646                Err(e) => errors.push((key_str.to_string(), e)),
647            }
648        }
649
650        ValidationResult {
651            total_processed: valid.len() + errors.len(),
652            valid,
653            errors,
654        }
655    }
656}
657
658/// Result of a validation operation
659#[derive(Debug, Clone, PartialEq, Eq)]
660pub struct ValidationResult {
661    /// Number of items processed before stopping
662    pub total_processed: usize,
663    /// Valid key strings
664    pub valid: Vec<String>,
665    /// Invalid keys with their errors
666    pub errors: Vec<(String, KeyParseError)>,
667}
668
669impl ValidationResult {
670    /// Check if all processed items were valid
671    #[inline]
672    #[must_use]
673    pub fn is_success(&self) -> bool {
674        self.errors.is_empty()
675    }
676
677    /// Get the number of valid items
678    #[inline]
679    #[must_use]
680    pub fn valid_count(&self) -> usize {
681        self.valid.len()
682    }
683
684    /// Get the number of invalid items
685    #[inline]
686    #[must_use]
687    pub fn error_count(&self) -> usize {
688        self.errors.len()
689    }
690
691    /// Get the success rate as a percentage
692    #[must_use]
693    pub fn success_rate(&self) -> f64 {
694        if self.total_processed == 0 {
695            0.0
696        } else {
697            #[expect(
698                clippy::cast_precision_loss,
699                reason = "total_processed fits comfortably in f64; precision loss only occurs above 2^53 items"
700            )]
701            let valid_ratio = self.valid.len() as f64 / self.total_processed as f64;
702            valid_ratio * 100.0
703        }
704    }
705
706    /// Convert all valid strings to keys
707    ///
708    /// # Errors
709    ///
710    /// Returns `KeyParseError` if any valid string fails key creation
711    pub fn into_keys<T: KeyDomain>(self) -> Result<Vec<Key<T>>, KeyParseError> {
712        self.valid
713            .into_iter()
714            .map(|s| Key::from_string(s))
715            .collect()
716    }
717
718    /// Try to convert all valid strings to keys, ignoring failures
719    #[must_use]
720    pub fn try_into_keys<T: KeyDomain>(self) -> Vec<Key<T>> {
721        self.valid
722            .into_iter()
723            .filter_map(|s| Key::from_string(s).ok())
724            .collect()
725    }
726}
727
728// ============================================================================
729// UTILITY FUNCTIONS
730// ============================================================================
731
732/// Create a validation builder with common settings for strict validation
733#[must_use]
734pub fn strict_validator<T: KeyDomain>() -> ValidationBuilder<T> {
735    ValidationBuilder::new()
736        .stop_on_first_error(true)
737        .allow_empty_collection(false)
738}
739
740/// Create a validation builder with common settings for lenient validation
741#[must_use]
742pub fn lenient_validator<T: KeyDomain>() -> ValidationBuilder<T> {
743    ValidationBuilder::new()
744        .stop_on_first_error(false)
745        .allow_empty_collection(true)
746}
747
748/// Quickly validate and convert a collection of strings to keys
749///
750/// This is a convenience function that combines validation and conversion
751/// in a single step.
752///
753/// # Examples
754///
755/// ```rust
756/// use domain_key::{Domain, KeyDomain, validation};
757///
758/// #[derive(Debug)]
759/// struct TestDomain;
760/// impl Domain for TestDomain {
761///     const DOMAIN_NAME: &'static str = "test";
762/// }
763/// impl KeyDomain for TestDomain {}
764///
765/// let strings = vec!["key1", "key2", "key3"];
766/// let keys = validation::quick_convert::<TestDomain, _>(strings).unwrap();
767///
768/// assert_eq!(keys.len(), 3);
769/// ```
770///
771/// # Errors
772///
773/// Returns a vector of validation errors if any keys fail validation
774pub fn quick_convert<T: KeyDomain, I>(keys: I) -> Result<Vec<Key<T>>, Vec<(String, KeyParseError)>>
775where
776    I: IntoIterator,
777    I::Item: AsRef<str>,
778{
779    let mut valid = Vec::new();
780    let mut errors = Vec::new();
781
782    for key in keys {
783        let key_str = key.as_ref().to_string();
784        match Key::<T>::from_string(key_str.clone()) {
785            Ok(k) => valid.push(k),
786            Err(e) => errors.push((key_str, e)),
787        }
788    }
789
790    if errors.is_empty() {
791        Ok(valid)
792    } else {
793        Err(errors)
794    }
795}
796
797// ============================================================================
798// TESTS
799// ============================================================================
800
801#[cfg(test)]
802mod tests {
803    use super::*;
804
805    // Test domain
806    #[derive(Debug)]
807    struct TestDomain;
808
809    impl crate::Domain for TestDomain {
810        const DOMAIN_NAME: &'static str = "test";
811    }
812
813    impl KeyDomain for TestDomain {
814        const MAX_LENGTH: usize = 32;
815
816        fn validation_help() -> Option<&'static str> {
817            Some("Test domain help")
818        }
819
820        fn examples() -> &'static [&'static str] {
821            &["example1", "example2"]
822        }
823    }
824
825    #[test]
826    fn is_valid_key_accepts_good_rejects_bad() {
827        assert!(is_valid_key::<TestDomain>("valid_key"));
828        assert!(!is_valid_key::<TestDomain>(""));
829        assert!(!is_valid_key::<TestDomain>("a".repeat(50).as_str()));
830    }
831
832    #[test]
833    fn validate_key_returns_error_for_empty() {
834        assert!(validate_key::<TestDomain>("valid_key").is_ok());
835        assert!(validate_key::<TestDomain>("").is_err());
836    }
837
838    #[test]
839    fn validation_info_contains_domain_details() {
840        let info = validation_info::<TestDomain>();
841        assert!(info.contains("Domain: test"));
842        assert!(info.contains("Max length: 32"));
843        assert!(info.contains("Help: Test domain help"));
844        assert!(info.contains("Examples: example1, example2"));
845    }
846
847    #[test]
848    fn validate_batch_separates_valid_and_invalid() {
849        let keys = vec!["valid1", "", "valid2", "bad key"];
850        let (valid, invalid) = validate_batch::<TestDomain, _>(&keys);
851
852        assert_eq!(valid.len(), 2);
853        assert_eq!(invalid.len(), 2);
854        assert!(valid.contains(&"valid1".to_string()));
855        assert!(valid.contains(&"valid2".to_string()));
856    }
857
858    #[test]
859    fn filter_valid_removes_bad_keys() {
860        let keys = vec!["valid1", "", "valid2", "bad key"];
861        let valid: Vec<_> = filter_valid::<TestDomain, _>(&keys).collect();
862
863        assert_eq!(valid.len(), 2);
864        assert!(valid.contains(&&"valid1"));
865        assert!(valid.contains(&&"valid2"));
866    }
867
868    #[test]
869    fn count_valid_matches_filter_length() {
870        let keys = vec!["valid1", "", "valid2", "bad key"];
871        let count = count_valid::<TestDomain, _>(&keys);
872        assert_eq!(count, 2);
873    }
874
875    #[test]
876    fn all_valid_true_only_when_all_pass() {
877        let all_valid_keys = vec!["valid1", "valid2"];
878        let mixed = vec!["valid1", "", "valid2"];
879
880        assert!(all_valid::<TestDomain, _>(&all_valid_keys));
881        assert!(!all_valid::<TestDomain, _>(&mixed));
882    }
883
884    #[test]
885    fn any_valid_true_when_at_least_one_passes() {
886        let mixed = vec!["", "valid1", ""];
887        let all_invalid = vec!["", ""];
888
889        assert!(any_valid::<TestDomain, _>(&mixed));
890        assert!(!any_valid::<TestDomain, _>(&all_invalid));
891    }
892
893    #[test]
894    fn into_key_converts_str_and_string() {
895        let key1: Key<TestDomain> = "test_key".into_key().unwrap();
896        let key2: Key<TestDomain> = "another_key".to_string().into_key().unwrap();
897
898        assert_eq!(key1.as_str(), "test_key");
899        assert_eq!(key2.as_str(), "another_key");
900
901        let invalid: Option<Key<TestDomain>> = "".try_into_key();
902        assert!(invalid.is_none());
903    }
904
905    #[test]
906    fn builder_respects_max_failures_limit() {
907        let builder = ValidationBuilder::<TestDomain>::new()
908            .allow_empty_collection(true)
909            .max_failures(2)
910            .stop_on_first_error(false);
911
912        let keys = vec!["valid1", "", "valid2", "", "valid3"];
913        let result = builder.validate(&keys);
914
915        // Debug output to understand what's happening
916        #[cfg(feature = "std")]
917        {
918            println!("Total processed: {}", result.total_processed);
919            println!("Valid count: {}", result.valid_count());
920            println!("Error count: {}", result.error_count());
921            println!("Valid keys: {:?}", result.valid);
922            println!("Errors: {:?}", result.errors);
923        }
924
925        // The builder has max_failures(2), so it should stop after 2 failures
926        // Input: ["valid1", "", "valid2", "", "valid3"]
927        // Processing:
928        // 1. "valid1" -> valid (valid_count = 1)
929        // 2. "" -> error (error_count = 1)
930        // 3. "valid2" -> valid (valid_count = 2)
931        // 4. "" -> error (error_count = 2, max_failures reached, stop processing)
932        // 5. "valid3" -> not processed
933
934        assert_eq!(result.valid_count(), 2); // "valid1", "valid2"
935        assert_eq!(result.error_count(), 2); // two empty strings
936        assert!(!result.is_success()); // has errors
937        assert_eq!(result.total_processed, 4); // processed 4 items before stopping
938        assert!(result.success_rate() > 40.0 && result.success_rate() <= 60.0); // 2/4 = 50%
939    }
940
941    #[test]
942    fn builder_stops_on_first_error_when_configured() {
943        let builder = ValidationBuilder::<TestDomain>::new()
944            .stop_on_first_error(true)
945            .allow_empty_collection(false);
946
947        let keys = vec!["valid", "", "another"];
948        let result = builder.validate(&keys);
949
950        // Should stop on first error (empty string)
951        assert_eq!(result.total_processed, 2); // "valid" + "" (error)
952        assert_eq!(result.valid_count(), 1);
953        assert_eq!(result.error_count(), 1);
954    }
955
956    #[test]
957    fn builder_processes_all_when_not_stopping_on_error() {
958        let builder = ValidationBuilder::<TestDomain>::new()
959            .stop_on_first_error(false)
960            .allow_empty_collection(true);
961
962        let keys = vec!["valid", "", "another"];
963        let result = builder.validate(&keys);
964
965        // Should process all items
966        assert_eq!(result.total_processed, 3);
967        assert_eq!(result.valid_count(), 2);
968        assert_eq!(result.error_count(), 1);
969    }
970
971    #[test]
972    fn validation_result_computes_success_rate() {
973        const EPSILON: f64 = 1e-10;
974        let keys = vec!["valid1", "valid2"];
975        let (valid, errors) = validate_batch::<TestDomain, _>(keys);
976
977        let result = ValidationResult {
978            total_processed: valid.len() + errors.len(),
979            valid,
980            errors,
981        };
982
983        assert!(result.is_success());
984        assert_eq!(result.valid_count(), 2);
985        assert_eq!(result.error_count(), 0);
986
987        assert!((result.success_rate() - 100.0).abs() < EPSILON);
988
989        let keys = result.try_into_keys::<TestDomain>();
990        assert_eq!(keys.len(), 2);
991    }
992
993    #[test]
994    fn strict_validator_stops_on_first_error() {
995        let validator = strict_validator::<TestDomain>();
996        let keys = vec!["valid", "", "another"];
997        let result = validator.validate(&keys);
998
999        // Should stop on first error (empty string)
1000        assert_eq!(result.total_processed, 2); // "valid" + "" (error)
1001        assert_eq!(result.valid_count(), 1);
1002        assert_eq!(result.error_count(), 1);
1003    }
1004
1005    #[test]
1006    fn lenient_validator_processes_all_items() {
1007        let validator = lenient_validator::<TestDomain>();
1008        let keys = vec!["valid", "", "another"];
1009        let result = validator.validate(&keys);
1010
1011        // Should process all items
1012        assert_eq!(result.total_processed, 3);
1013        assert_eq!(result.valid_count(), 2);
1014        assert_eq!(result.error_count(), 1);
1015    }
1016
1017    #[test]
1018    fn quick_convert_succeeds_or_returns_errors() {
1019        let strings = vec!["key1", "key2", "key3"];
1020        let keys = quick_convert::<TestDomain, _>(&strings).unwrap();
1021        assert_eq!(keys.len(), 3);
1022
1023        let mixed = vec!["key1", "", "key2"];
1024        let result = quick_convert::<TestDomain, _>(&mixed);
1025        assert!(result.is_err());
1026    }
1027
1028    #[test]
1029    fn custom_validator_applies_extra_check() {
1030        fn custom_check(key: &str) -> Result<(), KeyParseError> {
1031            if key.starts_with("custom_") {
1032                Ok(())
1033            } else {
1034                Err(KeyParseError::custom(9999, "Must start with custom_"))
1035            }
1036        }
1037
1038        let validator = ValidationBuilder::<TestDomain>::new().custom_validator(custom_check);
1039
1040        let keys = vec!["custom_key", "invalid_key"];
1041        let result = validator.validate(&keys);
1042
1043        assert_eq!(result.valid_count(), 1);
1044        assert_eq!(result.error_count(), 1);
1045    }
1046
1047    // ------------------------------------------------------------------
1048    // Compile-time validation tests
1049    // ------------------------------------------------------------------
1050
1051    /// Smoke-test that the const fn is correctly evaluable at compile time.
1052    const _GOOD: () = assert!(is_valid_key_default("user_123", 64));
1053    const _EMPTY: () = assert!(!is_valid_key_default("", 64));
1054    const _TOO_LONG: () = assert!(!is_valid_key_default("abcdefgh", 4));
1055    const _TRAILING_SEP: () = assert!(!is_valid_key_default("foo_", 64));
1056    const _CONSECUTIVE: () = assert!(!is_valid_key_default("a__b", 64));
1057    const _WITH_SPACE: () = assert!(!is_valid_key_default("hello world", 64));
1058    const _LEADING_SEP: () = assert!(is_valid_key_default("_foo", 64));
1059    const _HYPHEN_MID: () = assert!(is_valid_key_default("foo-bar", 64));
1060    const _DOT_MID: () = assert!(is_valid_key_default("foo.bar", 64));
1061    const _CONSEC_HYPHEN: () = assert!(!is_valid_key_default("foo--bar", 64));
1062    const _CONSEC_DOT: () = assert!(!is_valid_key_default("foo..bar", 64));
1063    const _NON_ASCII: () = assert!(!is_valid_key_default("héllo", 64));
1064    const _UPPERCASE: () = assert!(is_valid_key_default("FooBar", 64));
1065    const _DIGITS_ONLY: () = assert!(is_valid_key_default("12345", 64));
1066    const _EXACT_MAX: () = assert!(is_valid_key_default("ab", 2));
1067    const _OVER_MAX: () = assert!(!is_valid_key_default("abc", 2));
1068
1069    #[test]
1070    fn is_valid_key_default_matches_runtime_for_valid_keys() {
1071        // These should also pass the runtime domain validation
1072        assert!(is_valid_key_default("hello", 64));
1073        assert!(is_valid_key_default("user_name", 64));
1074        assert!(is_valid_key_default("foo-bar.baz", 64));
1075        assert!(is_valid_key_default("ABC123", 64));
1076    }
1077
1078    #[test]
1079    fn is_valid_key_default_rejects_all_bad_patterns() {
1080        assert!(!is_valid_key_default("", 64));
1081        assert!(!is_valid_key_default("trailing_", 64));
1082        assert!(!is_valid_key_default("trailing-", 64));
1083        assert!(!is_valid_key_default("trailing.", 64));
1084        assert!(!is_valid_key_default("a__b", 64));
1085        assert!(!is_valid_key_default("a--b", 64));
1086        assert!(!is_valid_key_default("a..b", 64));
1087        assert!(!is_valid_key_default("has space", 64));
1088        assert!(!is_valid_key_default("has\ttab", 64));
1089        assert!(!is_valid_key_default("a@b", 64));
1090        assert!(!is_valid_key_default("a!b", 64));
1091    }
1092
1093    #[test]
1094    fn is_valid_key_default_respects_max_length() {
1095        let exactly_max = "a".repeat(32);
1096        let over_max = "a".repeat(33);
1097        assert!(is_valid_key_default(&exactly_max, 32));
1098        assert!(!is_valid_key_default(&over_max, 32));
1099    }
1100}