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}