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