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