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