Skip to main content

domain_key/
domain.rs

1//! Domain trait and related functionality for domain-key
2//!
3//! This module defines the trait hierarchy for domain markers:
4//!
5//! - [`Domain`] — common supertrait with `DOMAIN_NAME` and basic bounds
6//! - [`KeyDomain`] — extends `Domain` with validation, normalization, and optimization hints
7//! - [`IdDomain`] — lightweight marker for numeric `Id<D>` identifiers
8//! - [`UuidDomain`] — lightweight marker for `Uuid<D>` identifiers (behind `uuid` feature)
9
10use core::fmt;
11
12#[cfg(not(feature = "std"))]
13use alloc::borrow::Cow;
14#[cfg(feature = "std")]
15use std::borrow::Cow;
16
17use crate::error::KeyParseError;
18use crate::key::DEFAULT_MAX_KEY_LENGTH;
19
20// ============================================================================
21// DOMAIN SUPERTRAIT
22// ============================================================================
23
24/// Common supertrait for all domain markers
25///
26/// Every domain — whether it's used for string keys, numeric IDs, or UUIDs —
27/// must implement this trait. It provides the minimal set of bounds and the
28/// human-readable domain name.
29///
30/// Specific domain traits ([`KeyDomain`], [`IdDomain`], [`UuidDomain`]) extend
31/// this trait with additional capabilities.
32///
33/// # Examples
34///
35/// ```rust
36/// use domain_key::Domain;
37///
38/// #[derive(Debug)]
39/// struct MyDomain;
40///
41/// impl Domain for MyDomain {
42///     const DOMAIN_NAME: &'static str = "my_domain";
43/// }
44/// ```
45pub trait Domain: 'static + Send + Sync + fmt::Debug {
46    /// Human-readable name for this domain
47    ///
48    /// This name is used in error messages and debugging output.
49    /// It should be a valid identifier that clearly describes the domain.
50    const DOMAIN_NAME: &'static str;
51}
52
53// ============================================================================
54// ID DOMAIN TRAIT
55// ============================================================================
56
57/// Marker trait for numeric `Id<D>` identifiers
58///
59/// This is a lightweight marker trait that extends [`Domain`]. Types
60/// implementing `IdDomain` can be used as the domain parameter for [`Id<D>`](crate::Id).
61///
62/// No additional methods or constants are required — just a domain name
63/// via [`Domain::DOMAIN_NAME`].
64///
65/// # Examples
66///
67/// ```rust
68/// use domain_key::{Domain, IdDomain, Id};
69///
70/// #[derive(Debug)]
71/// struct UserDomain;
72///
73/// impl Domain for UserDomain {
74///     const DOMAIN_NAME: &'static str = "user";
75/// }
76/// impl IdDomain for UserDomain {}
77///
78/// type UserId = Id<UserDomain>;
79/// ```
80pub trait IdDomain: Domain {}
81
82// ============================================================================
83// UUID DOMAIN TRAIT
84// ============================================================================
85
86/// Marker trait for `Uuid<D>` identifiers
87///
88/// This is a lightweight marker trait that extends [`Domain`]. Types
89/// implementing `UuidDomain` can be used as the domain parameter for [`Uuid<D>`](crate::Uuid).
90///
91/// No additional methods or constants are required — just a domain name
92/// via [`Domain::DOMAIN_NAME`].
93///
94/// # Examples
95///
96/// ```rust
97/// # #[cfg(feature = "uuid")]
98/// # {
99/// use domain_key::{Domain, UuidDomain, Uuid};
100///
101/// #[derive(Debug)]
102/// struct OrderDomain;
103///
104/// impl Domain for OrderDomain {
105///     const DOMAIN_NAME: &'static str = "order";
106/// }
107/// impl UuidDomain for OrderDomain {}
108///
109/// type OrderUuid = Uuid<OrderDomain>;
110/// # }
111/// ```
112#[cfg(feature = "uuid")]
113pub trait UuidDomain: Domain {}
114
115// ============================================================================
116// KEY DOMAIN TRAIT
117// ============================================================================
118
119/// Trait for key domain markers with validation, normalization, and optimization hints
120///
121/// This trait extends [`Domain`] with string-key-specific behavior: validation
122/// rules, normalization, character restrictions, and performance optimization hints.
123///
124/// # Implementation Requirements
125///
126/// Types implementing this trait must also implement:
127/// - [`Domain`] — for the domain name and basic bounds
128/// - `PartialEq + Eq + Hash + Ord + PartialOrd` — for standard key operations
129///
130/// # Design Philosophy
131///
132/// The trait is designed to be both powerful and performant:
133/// - **Const generics** for compile-time optimization hints
134/// - **Associated constants** for zero-cost configuration
135/// - **Default implementations** for common cases
136/// - **Hooks** for custom behavior where needed
137///
138/// # Examples
139///
140/// ## Basic domain with optimization hints
141/// ```rust
142/// use domain_key::{Domain, KeyDomain, KeyParseError};
143///
144/// #[derive(Debug)]
145/// struct UserDomain;
146///
147/// impl Domain for UserDomain {
148///     const DOMAIN_NAME: &'static str = "user";
149/// }
150///
151/// impl KeyDomain for UserDomain {
152///     const MAX_LENGTH: usize = 32;
153///     const EXPECTED_LENGTH: usize = 16;    // Optimization hint
154///     const TYPICALLY_SHORT: bool = true;   // Enable stack allocation
155/// }
156/// ```
157///
158/// ## Domain with custom validation
159/// ```rust
160/// use domain_key::{Domain, KeyDomain, KeyParseError};
161/// use std::borrow::Cow;
162///
163/// #[derive(Debug)]
164/// struct EmailDomain;
165///
166/// impl Domain for EmailDomain {
167///     const DOMAIN_NAME: &'static str = "email";
168/// }
169///
170/// impl KeyDomain for EmailDomain {
171///     const HAS_CUSTOM_VALIDATION: bool = true;
172///
173///     fn validate_domain_rules(key: &str) -> Result<(), KeyParseError> {
174///         if !key.contains('@') {
175///             return Err(KeyParseError::domain_error(Self::DOMAIN_NAME, "Email must contain @"));
176///         }
177///         Ok(())
178///     }
179///
180///     fn allowed_characters(c: char) -> bool {
181///         c.is_ascii_alphanumeric() || c == '@' || c == '.' || c == '_' || c == '-'
182///     }
183/// }
184/// ```
185pub trait KeyDomain: Domain {
186    /// Maximum length for keys in this domain
187    ///
188    /// Keys longer than this will be rejected during validation.
189    /// Setting this to a reasonable value enables performance optimizations.
190    const MAX_LENGTH: usize = DEFAULT_MAX_KEY_LENGTH;
191
192    /// Whether this domain has custom validation rules
193    ///
194    /// Set to `true` if you override `validate_domain_rules` with custom logic.
195    /// This is used for introspection and debugging.
196    const HAS_CUSTOM_VALIDATION: bool = false;
197
198    /// Whether this domain has custom normalization rules
199    ///
200    /// Set to `true` if you override `normalize_domain` with custom logic.
201    /// This is used for introspection and debugging.
202    const HAS_CUSTOM_NORMALIZATION: bool = false;
203
204    /// Optimization hint: expected average key length for this domain
205    ///
206    /// This hint helps the library pre-allocate the right amount of memory
207    /// for string operations, reducing reallocations.
208    const EXPECTED_LENGTH: usize = 32;
209
210    /// Optimization hint: whether keys in this domain are typically short (≤32 chars)
211    ///
212    /// When `true`, enables stack allocation optimizations for the majority
213    /// of keys in this domain. Set to `false` for domains with typically
214    /// long keys to avoid stack overflow risks.
215    const TYPICALLY_SHORT: bool = true;
216
217    /// Optimization hint: whether keys in this domain are frequently compared
218    ///
219    /// When `true`, enables additional hash caching and comparison optimizations.
220    /// Use for domains where keys are often used in hash maps or comparison operations.
221    const FREQUENTLY_COMPARED: bool = false;
222
223    /// Optimization hint: whether keys in this domain are frequently split
224    ///
225    /// When `true`, enables position caching for split operations.
226    /// Use for domains where keys are regularly split into components.
227    const FREQUENTLY_SPLIT: bool = false;
228
229    /// Whether keys in this domain are case-insensitive
230    ///
231    /// When `true`, keys are normalized to lowercase during creation.
232    /// When `false` (the default), keys preserve their original casing.
233    const CASE_INSENSITIVE: bool = false;
234
235    /// Domain-specific validation rules
236    ///
237    /// This method is called after common validation passes.
238    /// Domains can enforce their own specific rules here.
239    ///
240    /// # Performance Considerations
241    ///
242    /// This method is called for every key creation, so it should be fast:
243    /// - Prefer simple string operations over complex regex
244    /// - Use early returns for quick rejection
245    /// - Avoid expensive computations or I/O operations
246    ///
247    /// # Arguments
248    ///
249    /// * `key` - The normalized key string to validate
250    ///
251    /// # Returns
252    ///
253    /// * `Ok(())` if the key is valid for this domain
254    /// * `Err(KeyParseError)` with the specific validation failure
255    ///
256    /// # Errors
257    ///
258    /// Returns `KeyParseError` if the key doesn't meet domain-specific
259    /// validation requirements. Use `KeyParseError::domain_error` for
260    /// consistent error formatting.
261    fn validate_domain_rules(_key: &str) -> Result<(), KeyParseError> {
262        Ok(()) // Default: no domain-specific validation
263    }
264
265    /// Check which characters are allowed for this domain
266    ///
267    /// Override this method to define domain-specific character restrictions.
268    /// The default implementation allows ASCII alphanumeric characters and
269    /// common separators.
270    ///
271    /// # Performance Considerations
272    ///
273    /// This method is called for every character in every key, so it must be
274    /// extremely fast. Consider using lookup tables for complex character sets.
275    ///
276    /// # Arguments
277    ///
278    /// * `c` - Character to check
279    ///
280    /// # Returns
281    ///
282    /// `true` if the character is allowed, `false` otherwise
283    #[must_use]
284    fn allowed_characters(c: char) -> bool {
285        c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.'
286    }
287
288    /// Domain-specific normalization
289    ///
290    /// This method is called after common normalization (trimming, lowercasing).
291    /// Domains can apply additional normalization rules here.
292    /// Uses `Cow` to avoid unnecessary allocations when no changes are needed.
293    ///
294    /// # Performance Considerations
295    ///
296    /// - Return `Cow::Borrowed` when no changes are needed
297    /// - Only create `Cow::Owned` when actual changes are required
298    /// - Keep normalization rules simple for best performance
299    ///
300    /// # Arguments
301    ///
302    /// * `key` - The key string after common normalization
303    ///
304    /// # Returns
305    ///
306    /// The normalized key string for this domain
307    #[must_use]
308    fn normalize_domain(key: Cow<'_, str>) -> Cow<'_, str> {
309        key // Default: no additional normalization
310    }
311
312    /// Check if a key has a reserved prefix for this domain
313    ///
314    /// Override this method to define domain-specific reserved prefixes.
315    /// This can be used to prevent creation of keys that might conflict
316    /// with system-generated keys or have special meaning.
317    ///
318    /// # Arguments
319    ///
320    /// * `key` - The key string to check
321    ///
322    /// # Returns
323    ///
324    /// `true` if the key uses a reserved prefix, `false` otherwise
325    #[must_use]
326    fn is_reserved_prefix(_key: &str) -> bool {
327        false // Default: no reserved prefixes
328    }
329
330    /// Check if a key has a reserved suffix for this domain
331    ///
332    /// Similar to `is_reserved_prefix` but for suffixes.
333    ///
334    /// # Arguments
335    ///
336    /// * `key` - The key string to check
337    ///
338    /// # Returns
339    ///
340    /// `true` if the key uses a reserved suffix, `false` otherwise
341    #[must_use]
342    fn is_reserved_suffix(_key: &str) -> bool {
343        false // Default: no reserved suffixes
344    }
345
346    /// Get domain-specific help text for validation errors
347    ///
348    /// This can provide users with helpful information about what
349    /// constitutes a valid key for this domain.
350    ///
351    /// # Returns
352    ///
353    /// Optional help text that will be included in error messages
354    #[must_use]
355    fn validation_help() -> Option<&'static str> {
356        None // Default: no help text
357    }
358
359    /// Get examples of valid keys for this domain
360    ///
361    /// This can be used in documentation, error messages, or testing
362    /// to show users what valid keys look like.
363    ///
364    /// # Returns
365    ///
366    /// Array of example valid keys
367    #[must_use]
368    fn examples() -> &'static [&'static str] {
369        &[] // Default: no examples
370    }
371
372    /// Get the default separator character for this domain
373    ///
374    /// This is used when composing keys from multiple parts.
375    /// Different domains might prefer different separators.
376    ///
377    /// # Returns
378    ///
379    /// The preferred separator character
380    #[must_use]
381    fn default_separator() -> char {
382        '_' // Default: underscore
383    }
384
385    /// Check if the key contains only ASCII characters
386    ///
387    /// Some domains might require ASCII-only keys for compatibility reasons.
388    /// Override this method if your domain has specific ASCII requirements.
389    ///
390    /// # Arguments
391    ///
392    /// * `key` - The key string to check
393    ///
394    /// # Returns
395    ///
396    /// `true` if ASCII-only is required, `false` otherwise
397    #[must_use]
398    fn requires_ascii_only(_key: &str) -> bool {
399        false // Default: allow Unicode
400    }
401
402    /// Get the minimum allowed length for keys in this domain
403    ///
404    /// While empty keys are always rejected, some domains might require
405    /// a minimum length greater than 1.
406    ///
407    /// # Returns
408    ///
409    /// The minimum allowed length (must be >= 1)
410    #[must_use]
411    fn min_length() -> usize {
412        1 // Default: at least 1 character
413    }
414
415    /// Check if a character is allowed at the start of a key
416    ///
417    /// Some domains have stricter rules for the first character.
418    /// The default implementation uses the same rules as `allowed_characters`.
419    ///
420    /// # Arguments
421    ///
422    /// * `c` - Character to check
423    ///
424    /// # Returns
425    ///
426    /// `true` if the character is allowed at the start, `false` otherwise
427    #[must_use]
428    fn allowed_start_character(c: char) -> bool {
429        Self::allowed_characters(c) && c != '_' && c != '-' && c != '.'
430    }
431
432    /// Check if a character is allowed at the end of a key
433    ///
434    /// Some domains have stricter rules for the last character.
435    /// The default implementation uses the same rules as `allowed_characters`.
436    ///
437    /// # Arguments
438    ///
439    /// * `c` - Character to check
440    ///
441    /// # Returns
442    ///
443    /// `true` if the character is allowed at the end, `false` otherwise
444    #[must_use]
445    fn allowed_end_character(c: char) -> bool {
446        Self::allowed_characters(c) && c != '_' && c != '-' && c != '.'
447    }
448
449    /// Check if two consecutive characters are allowed
450    ///
451    /// This can be used to prevent patterns like double underscores
452    /// or other consecutive special characters.
453    ///
454    /// # Arguments
455    ///
456    /// * `prev` - Previous character
457    /// * `curr` - Current character
458    ///
459    /// # Returns
460    ///
461    /// `true` if the consecutive characters are allowed, `false` otherwise
462    #[must_use]
463    fn allowed_consecutive_characters(prev: char, curr: char) -> bool {
464        // Default: prevent consecutive special characters
465        !(prev == curr && (prev == '_' || prev == '-' || prev == '.'))
466    }
467}
468
469// ============================================================================
470// DOMAIN UTILITIES
471// ============================================================================
472
473/// Information about a domain's characteristics
474///
475/// This structure provides detailed information about a domain's configuration
476/// and optimization hints, useful for debugging and introspection.
477#[expect(
478    clippy::struct_excessive_bools,
479    reason = "info struct with boolean flags for domain characteristics"
480)]
481#[derive(Debug, Clone, PartialEq, Eq)]
482pub struct DomainInfo {
483    /// Domain name
484    pub name: &'static str,
485    /// Maximum allowed length
486    pub max_length: usize,
487    /// Minimum allowed length
488    pub min_length: usize,
489    /// Expected average length
490    pub expected_length: usize,
491    /// Whether typically short
492    pub typically_short: bool,
493    /// Whether frequently compared
494    pub frequently_compared: bool,
495    /// Whether frequently split
496    pub frequently_split: bool,
497    /// Whether case insensitive
498    pub case_insensitive: bool,
499    /// Whether has custom validation
500    pub has_custom_validation: bool,
501    /// Whether has custom normalization
502    pub has_custom_normalization: bool,
503    /// Default separator character
504    pub default_separator: char,
505    /// Validation help text
506    pub validation_help: Option<&'static str>,
507    /// Example valid keys
508    pub examples: &'static [&'static str],
509}
510
511impl fmt::Display for DomainInfo {
512    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
513        writeln!(f, "Domain: {}", self.name)?;
514        writeln!(
515            f,
516            "Length: {}-{} (expected: {})",
517            self.min_length, self.max_length, self.expected_length
518        )?;
519        writeln!(f, "Optimization hints:")?;
520        writeln!(f, "  • Typically short: {}", self.typically_short)?;
521        writeln!(f, "  • Frequently compared: {}", self.frequently_compared)?;
522        writeln!(f, "  • Frequently split: {}", self.frequently_split)?;
523        writeln!(f, "  • Case insensitive: {}", self.case_insensitive)?;
524        writeln!(f, "Custom features:")?;
525        writeln!(f, "  • Custom validation: {}", self.has_custom_validation)?;
526        writeln!(
527            f,
528            "  • Custom normalization: {}",
529            self.has_custom_normalization
530        )?;
531        writeln!(f, "Default separator: '{}'", self.default_separator)?;
532
533        if let Some(help) = self.validation_help {
534            writeln!(f, "Validation help: {help}")?;
535        }
536
537        if !self.examples.is_empty() {
538            writeln!(f, "Examples: {:?}", self.examples)?;
539        }
540
541        Ok(())
542    }
543}
544
545/// Get comprehensive information about a domain
546///
547/// This function returns detailed information about a domain's configuration,
548/// useful for debugging, documentation, and introspection.
549///
550/// # Examples
551///
552/// ```rust
553/// use domain_key::{Domain, KeyDomain, domain_info};
554///
555/// #[derive(Debug)]
556/// struct TestDomain;
557///
558/// impl Domain for TestDomain {
559///     const DOMAIN_NAME: &'static str = "test";
560/// }
561/// impl KeyDomain for TestDomain {
562///     const MAX_LENGTH: usize = 32;
563/// }
564///
565/// let info = domain_info::<TestDomain>();
566/// println!("{}", info);
567/// ```
568#[must_use]
569pub fn domain_info<T: KeyDomain>() -> DomainInfo {
570    DomainInfo {
571        name: T::DOMAIN_NAME,
572        max_length: T::MAX_LENGTH,
573        min_length: T::min_length(),
574        expected_length: T::EXPECTED_LENGTH,
575        typically_short: T::TYPICALLY_SHORT,
576        frequently_compared: T::FREQUENTLY_COMPARED,
577        frequently_split: T::FREQUENTLY_SPLIT,
578        case_insensitive: T::CASE_INSENSITIVE,
579        has_custom_validation: T::HAS_CUSTOM_VALIDATION,
580        has_custom_normalization: T::HAS_CUSTOM_NORMALIZATION,
581        default_separator: T::default_separator(),
582        validation_help: T::validation_help(),
583        examples: T::examples(),
584    }
585}
586
587/// Check if two domains have similar configuration
588///
589/// Returns `true` if both domains share the same max length, case sensitivity,
590/// and default separator. This is a **surface-level** check — it does not
591/// compare character sets, custom validation, or normalization rules.
592///
593/// Use this as a heuristic hint, not as a guarantee of interoperability.
594#[must_use]
595pub fn domains_compatible<T1: KeyDomain, T2: KeyDomain>() -> bool {
596    T1::MAX_LENGTH == T2::MAX_LENGTH
597        && T1::CASE_INSENSITIVE == T2::CASE_INSENSITIVE
598        && T1::default_separator() == T2::default_separator()
599}
600
601// ============================================================================
602// BUILT-IN DOMAIN IMPLEMENTATIONS
603// ============================================================================
604
605/// A simple default domain for general-purpose keys
606///
607/// This domain provides sensible defaults for most use cases:
608/// - Alphanumeric characters plus underscore, hyphen, and dot
609/// - Case-insensitive (normalized to lowercase)
610/// - Maximum length of 64 characters
611/// - No custom validation or normalization
612///
613/// # Examples
614///
615/// ```rust
616/// use domain_key::{Key, DefaultDomain};
617///
618/// type DefaultKey = Key<DefaultDomain>;
619///
620/// let key = DefaultKey::new("example_key")?;
621/// assert_eq!(key.as_str(), "example_key");
622/// # Ok::<(), domain_key::KeyParseError>(())
623/// ```
624#[derive(Debug)]
625pub struct DefaultDomain;
626
627impl Domain for DefaultDomain {
628    const DOMAIN_NAME: &'static str = "default";
629}
630
631impl KeyDomain for DefaultDomain {
632    const MAX_LENGTH: usize = 64;
633    const EXPECTED_LENGTH: usize = 24;
634    const TYPICALLY_SHORT: bool = true;
635    const CASE_INSENSITIVE: bool = true;
636
637    fn validation_help() -> Option<&'static str> {
638        Some("Use alphanumeric characters, underscores, hyphens, and dots. Case insensitive.")
639    }
640
641    fn examples() -> &'static [&'static str] {
642        &["user_123", "session-abc", "cache.key", "simple"]
643    }
644}
645
646/// A strict domain for identifiers that must follow strict naming rules
647///
648/// This domain is suitable for cases where keys must be valid identifiers
649/// in programming languages or databases:
650/// - Must start with a letter or underscore
651/// - Can contain letters, numbers, and underscores only
652/// - Case-sensitive
653/// - No consecutive underscores
654///
655/// # Examples
656///
657/// ```rust
658/// use domain_key::{Key, IdentifierDomain};
659///
660/// type IdKey = Key<IdentifierDomain>;
661///
662/// let key = IdKey::new("valid_identifier")?;
663/// assert_eq!(key.as_str(), "valid_identifier");
664/// # Ok::<(), domain_key::KeyParseError>(())
665/// ```
666#[derive(Debug)]
667pub struct IdentifierDomain;
668
669impl Domain for IdentifierDomain {
670    const DOMAIN_NAME: &'static str = "identifier";
671}
672
673impl KeyDomain for IdentifierDomain {
674    const MAX_LENGTH: usize = 64;
675    const EXPECTED_LENGTH: usize = 20;
676    const TYPICALLY_SHORT: bool = true;
677    const CASE_INSENSITIVE: bool = false;
678    const HAS_CUSTOM_VALIDATION: bool = true;
679
680    fn allowed_characters(c: char) -> bool {
681        c.is_ascii_alphanumeric() || c == '_'
682    }
683
684    fn allowed_start_character(c: char) -> bool {
685        c.is_ascii_alphabetic() || c == '_'
686    }
687
688    fn validate_domain_rules(key: &str) -> Result<(), KeyParseError> {
689        if let Some(first) = key.chars().next() {
690            if !Self::allowed_start_character(first) {
691                return Err(KeyParseError::domain_error(
692                    Self::DOMAIN_NAME,
693                    "Identifier must start with a letter or underscore",
694                ));
695            }
696        }
697        Ok(())
698    }
699
700    fn validation_help() -> Option<&'static str> {
701        Some("Must start with letter or underscore, contain only letters, numbers, and underscores. Case sensitive.")
702    }
703
704    fn examples() -> &'static [&'static str] {
705        &["user_id", "session_key", "_private", "publicVar"]
706    }
707}
708
709/// A domain for file path-like keys
710///
711/// This domain allows forward slashes and is suitable for hierarchical keys
712/// that resemble file paths:
713/// - Allows alphanumeric, underscore, hyphen, dot, and forward slash
714/// - Case-insensitive
715/// - No consecutive slashes
716/// - Cannot start or end with slash
717///
718/// # Examples
719///
720/// ```rust
721/// use domain_key::{Key, PathDomain};
722///
723/// type PathKey = Key<PathDomain>;
724///
725/// let key = PathKey::new("users/profile/settings")?;
726/// assert_eq!(key.as_str(), "users/profile/settings");
727/// # Ok::<(), domain_key::KeyParseError>(())
728/// ```
729#[derive(Debug)]
730pub struct PathDomain;
731
732impl Domain for PathDomain {
733    const DOMAIN_NAME: &'static str = "path";
734}
735
736impl KeyDomain for PathDomain {
737    const MAX_LENGTH: usize = 256;
738    const EXPECTED_LENGTH: usize = 48;
739    const TYPICALLY_SHORT: bool = false;
740    const CASE_INSENSITIVE: bool = true;
741    const FREQUENTLY_SPLIT: bool = true;
742    const HAS_CUSTOM_VALIDATION: bool = true;
743
744    fn allowed_characters(c: char) -> bool {
745        c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' || c == '/'
746    }
747
748    fn allowed_start_character(c: char) -> bool {
749        Self::allowed_characters(c) && c != '/'
750    }
751
752    fn allowed_end_character(c: char) -> bool {
753        Self::allowed_characters(c) && c != '/'
754    }
755
756    fn allowed_consecutive_characters(prev: char, curr: char) -> bool {
757        // Prevent consecutive slashes
758        !(prev == '/' && curr == '/')
759    }
760
761    fn default_separator() -> char {
762        '/'
763    }
764
765    fn validate_domain_rules(key: &str) -> Result<(), KeyParseError> {
766        if key.starts_with('/') || key.ends_with('/') {
767            return Err(KeyParseError::domain_error(
768                Self::DOMAIN_NAME,
769                "Path cannot start or end with '/'",
770            ));
771        }
772
773        if key.contains("//") {
774            return Err(KeyParseError::domain_error(
775                Self::DOMAIN_NAME,
776                "Path cannot contain consecutive '/'",
777            ));
778        }
779
780        Ok(())
781    }
782
783    fn validation_help() -> Option<&'static str> {
784        Some("Use path-like format with '/' separators. Cannot start/end with '/' or have consecutive '//'.")
785    }
786
787    fn examples() -> &'static [&'static str] {
788        &["users/profile", "cache/session/data", "config/app.settings"]
789    }
790}
791
792// ============================================================================
793// TESTS
794// ============================================================================
795
796#[cfg(test)]
797mod tests {
798    use super::*;
799
800    #[cfg(not(feature = "std"))]
801    use alloc::borrow::Cow;
802    #[cfg(not(feature = "std"))]
803    use alloc::format;
804    #[cfg(not(feature = "std"))]
805    use alloc::string::ToString;
806    #[cfg(feature = "std")]
807    use std::borrow::Cow;
808
809    #[test]
810    fn default_domain_is_case_insensitive_with_max_64() {
811        let info = domain_info::<DefaultDomain>();
812        assert_eq!(info.name, "default");
813        assert_eq!(info.max_length, 64);
814        assert!(info.case_insensitive);
815        assert!(!info.has_custom_validation);
816    }
817
818    #[test]
819    fn identifier_domain_rejects_hyphens_and_leading_digits() {
820        let info = domain_info::<IdentifierDomain>();
821        assert_eq!(info.name, "identifier");
822        assert!(!info.case_insensitive);
823        assert!(info.has_custom_validation);
824
825        // Test character validation
826        assert!(IdentifierDomain::allowed_characters('a'));
827        assert!(IdentifierDomain::allowed_characters('_'));
828        assert!(!IdentifierDomain::allowed_characters('-'));
829
830        // Test start character validation
831        assert!(IdentifierDomain::allowed_start_character('a'));
832        assert!(IdentifierDomain::allowed_start_character('_'));
833        assert!(!IdentifierDomain::allowed_start_character('1'));
834    }
835
836    #[test]
837    fn path_domain_allows_slashes_but_not_consecutive() {
838        let info = domain_info::<PathDomain>();
839        assert_eq!(info.name, "path");
840        assert_eq!(info.default_separator, '/');
841        assert!(info.frequently_split);
842        assert!(info.has_custom_validation);
843
844        // Test character validation
845        assert!(PathDomain::allowed_characters('/'));
846        assert!(!PathDomain::allowed_start_character('/'));
847        assert!(!PathDomain::allowed_end_character('/'));
848        assert!(!PathDomain::allowed_consecutive_characters('/', '/'));
849    }
850
851    #[test]
852    fn domain_info_display_includes_name_and_length() {
853        let info = domain_info::<DefaultDomain>();
854        let display = format!("{info}");
855        assert!(display.contains("Domain: default"));
856        assert!(display.contains("Length: 1-64"));
857        assert!(display.contains("Case insensitive: true"));
858    }
859
860    #[test]
861    fn compatible_domains_share_config_incompatible_differ() {
862        assert!(domains_compatible::<DefaultDomain, DefaultDomain>());
863        assert!(!domains_compatible::<DefaultDomain, IdentifierDomain>());
864        assert!(!domains_compatible::<IdentifierDomain, PathDomain>());
865    }
866
867    #[test]
868    fn default_trait_methods_return_sensible_defaults() {
869        // Test default implementations
870        assert!(DefaultDomain::allowed_characters('a'));
871        assert!(!DefaultDomain::is_reserved_prefix("test"));
872        assert!(!DefaultDomain::is_reserved_suffix("test"));
873        assert!(!DefaultDomain::requires_ascii_only("test"));
874        assert_eq!(DefaultDomain::min_length(), 1);
875
876        // Test validation help
877        assert!(DefaultDomain::validation_help().is_some());
878        assert!(!DefaultDomain::examples().is_empty());
879    }
880
881    #[test]
882    fn normalize_domain_borrows_when_unchanged() {
883        // Test default normalization (no change)
884        let input = Cow::Borrowed("test");
885        let output = DefaultDomain::normalize_domain(input);
886        assert!(matches!(output, Cow::Borrowed("test")));
887
888        // Test with owned string
889        let input = Cow::Owned("test".to_string());
890        let output = DefaultDomain::normalize_domain(input);
891        assert!(matches!(output, Cow::Owned(_)));
892    }
893}