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#[allow(clippy::struct_excessive_bools)]
478#[derive(Debug, Clone, PartialEq, Eq)]
479pub struct DomainInfo {
480 /// Domain name
481 pub name: &'static str,
482 /// Maximum allowed length
483 pub max_length: usize,
484 /// Minimum allowed length
485 pub min_length: usize,
486 /// Expected average length
487 pub expected_length: usize,
488 /// Whether typically short
489 pub typically_short: bool,
490 /// Whether frequently compared
491 pub frequently_compared: bool,
492 /// Whether frequently split
493 pub frequently_split: bool,
494 /// Whether case insensitive
495 pub case_insensitive: bool,
496 /// Whether has custom validation
497 pub has_custom_validation: bool,
498 /// Whether has custom normalization
499 pub has_custom_normalization: bool,
500 /// Default separator character
501 pub default_separator: char,
502 /// Validation help text
503 pub validation_help: Option<&'static str>,
504 /// Example valid keys
505 pub examples: &'static [&'static str],
506}
507
508impl fmt::Display for DomainInfo {
509 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
510 writeln!(f, "Domain: {}", self.name)?;
511 writeln!(
512 f,
513 "Length: {}-{} (expected: {})",
514 self.min_length, self.max_length, self.expected_length
515 )?;
516 writeln!(f, "Optimization hints:")?;
517 writeln!(f, " • Typically short: {}", self.typically_short)?;
518 writeln!(f, " • Frequently compared: {}", self.frequently_compared)?;
519 writeln!(f, " • Frequently split: {}", self.frequently_split)?;
520 writeln!(f, " • Case insensitive: {}", self.case_insensitive)?;
521 writeln!(f, "Custom features:")?;
522 writeln!(f, " • Custom validation: {}", self.has_custom_validation)?;
523 writeln!(
524 f,
525 " • Custom normalization: {}",
526 self.has_custom_normalization
527 )?;
528 writeln!(f, "Default separator: '{}'", self.default_separator)?;
529
530 if let Some(help) = self.validation_help {
531 writeln!(f, "Validation help: {help}")?;
532 }
533
534 if !self.examples.is_empty() {
535 writeln!(f, "Examples: {:?}", self.examples)?;
536 }
537
538 Ok(())
539 }
540}
541
542/// Get comprehensive information about a domain
543///
544/// This function returns detailed information about a domain's configuration,
545/// useful for debugging, documentation, and introspection.
546///
547/// # Examples
548///
549/// ```rust
550/// use domain_key::{Domain, KeyDomain, domain_info};
551///
552/// #[derive(Debug)]
553/// struct TestDomain;
554///
555/// impl Domain for TestDomain {
556/// const DOMAIN_NAME: &'static str = "test";
557/// }
558/// impl KeyDomain for TestDomain {
559/// const MAX_LENGTH: usize = 32;
560/// }
561///
562/// let info = domain_info::<TestDomain>();
563/// println!("{}", info);
564/// ```
565#[must_use]
566pub fn domain_info<T: KeyDomain>() -> DomainInfo {
567 DomainInfo {
568 name: T::DOMAIN_NAME,
569 max_length: T::MAX_LENGTH,
570 min_length: T::min_length(),
571 expected_length: T::EXPECTED_LENGTH,
572 typically_short: T::TYPICALLY_SHORT,
573 frequently_compared: T::FREQUENTLY_COMPARED,
574 frequently_split: T::FREQUENTLY_SPLIT,
575 case_insensitive: T::CASE_INSENSITIVE,
576 has_custom_validation: T::HAS_CUSTOM_VALIDATION,
577 has_custom_normalization: T::HAS_CUSTOM_NORMALIZATION,
578 default_separator: T::default_separator(),
579 validation_help: T::validation_help(),
580 examples: T::examples(),
581 }
582}
583
584/// Check if two domains have similar configuration
585///
586/// Returns `true` if both domains share the same max length, case sensitivity,
587/// and default separator. This is a **surface-level** check — it does not
588/// compare character sets, custom validation, or normalization rules.
589///
590/// Use this as a heuristic hint, not as a guarantee of interoperability.
591#[must_use]
592pub fn domains_compatible<T1: KeyDomain, T2: KeyDomain>() -> bool {
593 T1::MAX_LENGTH == T2::MAX_LENGTH
594 && T1::CASE_INSENSITIVE == T2::CASE_INSENSITIVE
595 && T1::default_separator() == T2::default_separator()
596}
597
598// ============================================================================
599// BUILT-IN DOMAIN IMPLEMENTATIONS
600// ============================================================================
601
602/// A simple default domain for general-purpose keys
603///
604/// This domain provides sensible defaults for most use cases:
605/// - Alphanumeric characters plus underscore, hyphen, and dot
606/// - Case-insensitive (normalized to lowercase)
607/// - Maximum length of 64 characters
608/// - No custom validation or normalization
609///
610/// # Examples
611///
612/// ```rust
613/// use domain_key::{Key, DefaultDomain};
614///
615/// type DefaultKey = Key<DefaultDomain>;
616///
617/// let key = DefaultKey::new("example_key")?;
618/// assert_eq!(key.as_str(), "example_key");
619/// # Ok::<(), domain_key::KeyParseError>(())
620/// ```
621#[derive(Debug)]
622pub struct DefaultDomain;
623
624impl Domain for DefaultDomain {
625 const DOMAIN_NAME: &'static str = "default";
626}
627
628impl KeyDomain for DefaultDomain {
629 const MAX_LENGTH: usize = 64;
630 const EXPECTED_LENGTH: usize = 24;
631 const TYPICALLY_SHORT: bool = true;
632 const CASE_INSENSITIVE: bool = true;
633
634 fn validation_help() -> Option<&'static str> {
635 Some("Use alphanumeric characters, underscores, hyphens, and dots. Case insensitive.")
636 }
637
638 fn examples() -> &'static [&'static str] {
639 &["user_123", "session-abc", "cache.key", "simple"]
640 }
641}
642
643/// A strict domain for identifiers that must follow strict naming rules
644///
645/// This domain is suitable for cases where keys must be valid identifiers
646/// in programming languages or databases:
647/// - Must start with a letter or underscore
648/// - Can contain letters, numbers, and underscores only
649/// - Case-sensitive
650/// - No consecutive underscores
651///
652/// # Examples
653///
654/// ```rust
655/// use domain_key::{Key, IdentifierDomain};
656///
657/// type IdKey = Key<IdentifierDomain>;
658///
659/// let key = IdKey::new("valid_identifier")?;
660/// assert_eq!(key.as_str(), "valid_identifier");
661/// # Ok::<(), domain_key::KeyParseError>(())
662/// ```
663#[derive(Debug)]
664pub struct IdentifierDomain;
665
666impl Domain for IdentifierDomain {
667 const DOMAIN_NAME: &'static str = "identifier";
668}
669
670impl KeyDomain for IdentifierDomain {
671 const MAX_LENGTH: usize = 64;
672 const EXPECTED_LENGTH: usize = 20;
673 const TYPICALLY_SHORT: bool = true;
674 const CASE_INSENSITIVE: bool = false;
675 const HAS_CUSTOM_VALIDATION: bool = true;
676
677 fn allowed_characters(c: char) -> bool {
678 c.is_ascii_alphanumeric() || c == '_'
679 }
680
681 fn allowed_start_character(c: char) -> bool {
682 c.is_ascii_alphabetic() || c == '_'
683 }
684
685 fn validate_domain_rules(key: &str) -> Result<(), KeyParseError> {
686 if let Some(first) = key.chars().next() {
687 if !Self::allowed_start_character(first) {
688 return Err(KeyParseError::domain_error(
689 Self::DOMAIN_NAME,
690 "Identifier must start with a letter or underscore",
691 ));
692 }
693 }
694 Ok(())
695 }
696
697 fn validation_help() -> Option<&'static str> {
698 Some("Must start with letter or underscore, contain only letters, numbers, and underscores. Case sensitive.")
699 }
700
701 fn examples() -> &'static [&'static str] {
702 &["user_id", "session_key", "_private", "publicVar"]
703 }
704}
705
706/// A domain for file path-like keys
707///
708/// This domain allows forward slashes and is suitable for hierarchical keys
709/// that resemble file paths:
710/// - Allows alphanumeric, underscore, hyphen, dot, and forward slash
711/// - Case-insensitive
712/// - No consecutive slashes
713/// - Cannot start or end with slash
714///
715/// # Examples
716///
717/// ```rust
718/// use domain_key::{Key, PathDomain};
719///
720/// type PathKey = Key<PathDomain>;
721///
722/// let key = PathKey::new("users/profile/settings")?;
723/// assert_eq!(key.as_str(), "users/profile/settings");
724/// # Ok::<(), domain_key::KeyParseError>(())
725/// ```
726#[derive(Debug)]
727pub struct PathDomain;
728
729impl Domain for PathDomain {
730 const DOMAIN_NAME: &'static str = "path";
731}
732
733impl KeyDomain for PathDomain {
734 const MAX_LENGTH: usize = 256;
735 const EXPECTED_LENGTH: usize = 48;
736 const TYPICALLY_SHORT: bool = false;
737 const CASE_INSENSITIVE: bool = true;
738 const FREQUENTLY_SPLIT: bool = true;
739 const HAS_CUSTOM_VALIDATION: bool = true;
740
741 fn allowed_characters(c: char) -> bool {
742 c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' || c == '/'
743 }
744
745 fn allowed_start_character(c: char) -> bool {
746 Self::allowed_characters(c) && c != '/'
747 }
748
749 fn allowed_end_character(c: char) -> bool {
750 Self::allowed_characters(c) && c != '/'
751 }
752
753 fn allowed_consecutive_characters(prev: char, curr: char) -> bool {
754 // Prevent consecutive slashes
755 !(prev == '/' && curr == '/')
756 }
757
758 fn default_separator() -> char {
759 '/'
760 }
761
762 fn validate_domain_rules(key: &str) -> Result<(), KeyParseError> {
763 if key.starts_with('/') || key.ends_with('/') {
764 return Err(KeyParseError::domain_error(
765 Self::DOMAIN_NAME,
766 "Path cannot start or end with '/'",
767 ));
768 }
769
770 if key.contains("//") {
771 return Err(KeyParseError::domain_error(
772 Self::DOMAIN_NAME,
773 "Path cannot contain consecutive '/'",
774 ));
775 }
776
777 Ok(())
778 }
779
780 fn validation_help() -> Option<&'static str> {
781 Some("Use path-like format with '/' separators. Cannot start/end with '/' or have consecutive '//'.")
782 }
783
784 fn examples() -> &'static [&'static str] {
785 &["users/profile", "cache/session/data", "config/app.settings"]
786 }
787}
788
789// ============================================================================
790// TESTS
791// ============================================================================
792
793#[cfg(test)]
794mod tests {
795 use super::*;
796
797 #[cfg(not(feature = "std"))]
798 use alloc::borrow::Cow;
799 #[cfg(not(feature = "std"))]
800 use alloc::format;
801 #[cfg(not(feature = "std"))]
802 use alloc::string::ToString;
803 #[cfg(feature = "std")]
804 use std::borrow::Cow;
805
806 #[test]
807 fn default_domain_is_case_insensitive_with_max_64() {
808 let info = domain_info::<DefaultDomain>();
809 assert_eq!(info.name, "default");
810 assert_eq!(info.max_length, 64);
811 assert!(info.case_insensitive);
812 assert!(!info.has_custom_validation);
813 }
814
815 #[test]
816 fn identifier_domain_rejects_hyphens_and_leading_digits() {
817 let info = domain_info::<IdentifierDomain>();
818 assert_eq!(info.name, "identifier");
819 assert!(!info.case_insensitive);
820 assert!(info.has_custom_validation);
821
822 // Test character validation
823 assert!(IdentifierDomain::allowed_characters('a'));
824 assert!(IdentifierDomain::allowed_characters('_'));
825 assert!(!IdentifierDomain::allowed_characters('-'));
826
827 // Test start character validation
828 assert!(IdentifierDomain::allowed_start_character('a'));
829 assert!(IdentifierDomain::allowed_start_character('_'));
830 assert!(!IdentifierDomain::allowed_start_character('1'));
831 }
832
833 #[test]
834 fn path_domain_allows_slashes_but_not_consecutive() {
835 let info = domain_info::<PathDomain>();
836 assert_eq!(info.name, "path");
837 assert_eq!(info.default_separator, '/');
838 assert!(info.frequently_split);
839 assert!(info.has_custom_validation);
840
841 // Test character validation
842 assert!(PathDomain::allowed_characters('/'));
843 assert!(!PathDomain::allowed_start_character('/'));
844 assert!(!PathDomain::allowed_end_character('/'));
845 assert!(!PathDomain::allowed_consecutive_characters('/', '/'));
846 }
847
848 #[test]
849 fn domain_info_display_includes_name_and_length() {
850 let info = domain_info::<DefaultDomain>();
851 let display = format!("{info}");
852 assert!(display.contains("Domain: default"));
853 assert!(display.contains("Length: 1-64"));
854 assert!(display.contains("Case insensitive: true"));
855 }
856
857 #[test]
858 fn compatible_domains_share_config_incompatible_differ() {
859 assert!(domains_compatible::<DefaultDomain, DefaultDomain>());
860 assert!(!domains_compatible::<DefaultDomain, IdentifierDomain>());
861 assert!(!domains_compatible::<IdentifierDomain, PathDomain>());
862 }
863
864 #[test]
865 fn default_trait_methods_return_sensible_defaults() {
866 // Test default implementations
867 assert!(DefaultDomain::allowed_characters('a'));
868 assert!(!DefaultDomain::is_reserved_prefix("test"));
869 assert!(!DefaultDomain::is_reserved_suffix("test"));
870 assert!(!DefaultDomain::requires_ascii_only("test"));
871 assert_eq!(DefaultDomain::min_length(), 1);
872
873 // Test validation help
874 assert!(DefaultDomain::validation_help().is_some());
875 assert!(!DefaultDomain::examples().is_empty());
876 }
877
878 #[test]
879 fn normalize_domain_borrows_when_unchanged() {
880 // Test default normalization (no change)
881 let input = Cow::Borrowed("test");
882 let output = DefaultDomain::normalize_domain(input);
883 assert!(matches!(output, Cow::Borrowed("test")));
884
885 // Test with owned string
886 let input = Cow::Owned("test".to_string());
887 let output = DefaultDomain::normalize_domain(input);
888 assert!(matches!(output, Cow::Owned(_)));
889 }
890}