domain_key/macros.rs
1//! Macros for convenient key creation and domain definition in domain-key
2//!
3//! This module provides helpful macros that simplify the creation and usage
4//! of domain-specific keys, reducing boilerplate and improving ergonomics.
5
6// ============================================================================
7// STATIC KEY MACRO
8// ============================================================================
9
10/// Create a validated static key from a string literal.
11///
12/// This macro combines **compile-time** and **runtime** validation:
13///
14/// 1. **Compile-time** — a `const` assertion checks the literal against the
15/// default [`KeyDomain`] rules (character set, length ≤ `MAX_LENGTH`,
16/// end-character, no consecutive separators). An invalid literal is a
17/// **compile error**, not a runtime panic.
18///
19/// 2. **Runtime** — [`Key::try_from_static`] is still called to enforce any
20/// *custom* domain rules added via [`KeyDomain::validate_domain_rules`].
21/// If those rules reject the key this will panic; treat it as a bug in the
22/// domain configuration, not a recoverable error.
23///
24/// # Arguments
25///
26/// * `$key_type` - The key type alias (e.g. `UserKey`)
27/// * `$key_str` - A string literal for the key value
28///
29/// # Examples
30///
31/// ```rust
32/// use domain_key::{Key, Domain, KeyDomain, static_key};
33///
34/// #[derive(Debug)]
35/// struct AdminDomain;
36/// impl Domain for AdminDomain {
37/// const DOMAIN_NAME: &'static str = "admin";
38/// }
39/// impl KeyDomain for AdminDomain {}
40/// type AdminKey = Key<AdminDomain>;
41///
42/// // Invalid literals are caught at *compile time*, not at runtime:
43/// let key = static_key!(AdminKey, "system_admin");
44/// assert_eq!(key.as_str(), "system_admin");
45/// ```
46#[macro_export]
47macro_rules! static_key {
48 ($key_type:ty, $key_str:literal) => {{
49 // ── Compile-time validation ──────────────────────────────────────
50 // `is_valid_key_const` is a `const fn` that runs the default
51 // KeyDomain rules against T::MAX_LENGTH. Evaluating it inside a
52 // `const` item forces the compiler to check it right here; an
53 // invalid literal becomes a *compile error*.
54 const _: () = assert!(
55 <$key_type>::is_valid_key_const($key_str),
56 concat!(
57 "static_key!: literal failed compile-time validation: ",
58 $key_str
59 ),
60 );
61
62 // ── Runtime validation ───────────────────────────────────────────
63 // Still needed to enforce any custom `validate_domain_rules`.
64 // A panic here means the domain's custom rules reject a key that
65 // passed the default rules — fix the key or the domain, not the
66 // macro call site.
67 match <$key_type>::try_from_static($key_str) {
68 Ok(key) => key,
69 Err(e) => panic!("static_key!: runtime validation failed: {}", e),
70 }
71 }};
72}
73
74// ============================================================================
75// DOMAIN DEFINITION MACRO
76// ============================================================================
77
78/// Define a key domain with minimal boilerplate.
79///
80/// This macro generates:
81///
82/// * A `#[derive(Debug)]` unit struct with the given visibility.
83/// * An impl of [`Domain`] that sets `DOMAIN_NAME`.
84/// * An impl of [`KeyDomain`] that sets `MAX_LENGTH`.
85/// * An inherent `impl` block containing:
86/// - `pub const fn is_valid_key(s: &str) -> bool` — evaluates the default
87/// domain rules at **compile time** using `MAX_LENGTH` for this domain.
88/// Useful in `const` assertions and with [`static_key!`].
89///
90/// # Arguments
91///
92/// * `$name` — The domain struct name.
93/// * `$domain_name` — The human-readable string name embedded in error messages.
94/// * `$max_length` — Optional maximum key length (defaults to
95/// [`DEFAULT_MAX_KEY_LENGTH`]).
96///
97/// # Examples
98///
99/// ```rust
100/// use domain_key::{define_domain, Key};
101///
102/// // Simple domain — MAX_LENGTH defaults to DEFAULT_MAX_KEY_LENGTH
103/// define_domain!(UserDomain, "user");
104/// type UserKey = Key<UserDomain>;
105///
106/// // Domain with a custom max length
107/// define_domain!(SessionDomain, "session", 128);
108/// type SessionKey = Key<SessionDomain>;
109///
110/// // Compile-time key validation via the generated const fn
111/// const _: () = assert!(UserDomain::is_valid_key("john_doe"));
112/// const _: () = assert!(!UserDomain::is_valid_key(""));
113///
114/// let user = UserKey::new("john_doe")?;
115/// let session = SessionKey::new("sess_abc123")?;
116/// # Ok::<(), domain_key::KeyParseError>(())
117/// ```
118#[macro_export]
119macro_rules! define_domain {
120 ($vis:vis $name:ident, $domain_name:literal) => {
121 $crate::define_domain!($vis $name, $domain_name, $crate::DEFAULT_MAX_KEY_LENGTH);
122 };
123
124 ($vis:vis $name:ident, $domain_name:literal, $max_length:expr) => {
125 #[derive(Debug)]
126 $vis struct $name;
127
128 impl $crate::Domain for $name {
129 const DOMAIN_NAME: &'static str = $domain_name;
130 }
131
132 impl $crate::KeyDomain for $name {
133 const MAX_LENGTH: usize = $max_length;
134 }
135
136 #[allow(dead_code)]
137 impl $name {
138 /// Check whether `s` satisfies the **default** [`KeyDomain`]
139 /// validation rules for this domain.
140 ///
141 /// This is a `const fn` — it is evaluated entirely at compile
142 /// time when called in a `const` context.
143 ///
144 /// # What is checked
145 ///
146 /// * Non-empty
147 /// * `s.len() <= MAX_LENGTH` for this domain
148 /// * Every byte is ASCII alphanumeric, `_`, `-`, or `.`
149 /// * The last byte is ASCII alphanumeric (not `_`, `-`, `.`)
150 /// * No consecutive identical separators (`__`, `--`, `..`)
151 ///
152 /// # What is **not** checked
153 ///
154 /// Custom rules added via [`KeyDomain::validate_domain_rules`]
155 /// are **not** verified — those are enforced at runtime by
156 /// [`Key::new`] / [`Key::try_from_static`].
157 ///
158 /// # Examples
159 ///
160 /// ```rust,ignore
161 /// // Evaluated entirely at compile time:
162 /// const _: () = assert!(MyDomain::is_valid_key("good_key"));
163 /// const _: () = assert!(!MyDomain::is_valid_key("bad key!"));
164 /// ```
165 #[must_use]
166 pub const fn is_valid_key(s: &str) -> bool {
167 $crate::is_valid_key_default(s, $max_length)
168 }
169 }
170 };
171}
172
173// ============================================================================
174// KEY TYPE ALIAS MACRO
175// ============================================================================
176
177/// Create a key type alias
178///
179/// This macro creates a type alias for a key.
180///
181/// # Arguments
182///
183/// * `$key_name` - The name for the key type alias
184/// * `$domain` - The domain type
185///
186/// # Examples
187///
188/// ```rust
189/// use domain_key::{define_domain, key_type};
190///
191/// define_domain!(UserDomain, "user");
192/// key_type!(UserKey, UserDomain);
193///
194/// let user = UserKey::new("john")?;
195/// # Ok::<(), domain_key::KeyParseError>(())
196/// ```
197#[macro_export]
198macro_rules! key_type {
199 ($vis:vis $key_name:ident, $domain:ty) => {
200 $vis type $key_name = $crate::Key<$domain>;
201 };
202}
203
204// ============================================================================
205// ID DOMAIN DEFINITION MACRO
206// ============================================================================
207
208/// Define an ID domain with minimal boilerplate
209///
210/// This macro simplifies the definition of ID domains by generating the
211/// required trait implementations automatically.
212///
213/// # Arguments
214///
215/// * `$name` - The domain struct name
216/// * `$domain_name` - The string name for the domain
217///
218/// # Examples
219///
220/// ```rust
221/// use domain_key::{define_id_domain, Id};
222///
223/// define_id_domain!(UserIdDomain, "user");
224/// type UserId = Id<UserIdDomain>;
225///
226/// let id = UserId::new(42).unwrap();
227/// assert_eq!(id.domain(), "user");
228/// ```
229#[macro_export]
230macro_rules! define_id_domain {
231 // Without explicit name — uses stringify
232 ($vis:vis $name:ident) => {
233 $crate::define_id_domain!(@inner $vis $name, stringify!($name));
234 };
235 // With explicit string literal
236 ($vis:vis $name:ident, $domain_name:literal) => {
237 $crate::define_id_domain!(@inner $vis $name, $domain_name);
238 };
239 (@inner $vis:vis $name:ident, $domain_name:expr) => {
240 #[derive(Debug)]
241 $vis struct $name;
242
243 impl $crate::Domain for $name {
244 const DOMAIN_NAME: &'static str = $domain_name;
245 }
246
247 impl $crate::IdDomain for $name {}
248 };
249}
250
251// ============================================================================
252// UUID DOMAIN DEFINITION MACRO
253// ============================================================================
254
255/// Define a UUID domain with minimal boilerplate
256///
257/// This macro simplifies the definition of UUID domains by generating the
258/// required trait implementations automatically.
259///
260/// Requires the `uuid` feature.
261///
262/// # Arguments
263///
264/// * `$name` - The domain struct name
265/// * `$domain_name` - The string name for the domain
266///
267/// # Examples
268///
269/// ```rust
270/// # #[cfg(feature = "uuid")]
271/// # {
272/// use domain_key::{define_uuid_domain, Uuid};
273///
274/// define_uuid_domain!(OrderUuidDomain, "order");
275/// type OrderUuid = Uuid<OrderUuidDomain>;
276///
277/// let id = OrderUuid::nil();
278/// assert_eq!(id.domain(), "order");
279/// # }
280/// ```
281#[cfg(feature = "uuid")]
282#[macro_export]
283macro_rules! define_uuid_domain {
284 // Without explicit name — uses stringify
285 ($vis:vis $name:ident) => {
286 $crate::define_uuid_domain!(@inner $vis $name, stringify!($name));
287 };
288 // With explicit string literal
289 ($vis:vis $name:ident, $domain_name:literal) => {
290 $crate::define_uuid_domain!(@inner $vis $name, $domain_name);
291 };
292 (@inner $vis:vis $name:ident, $domain_name:expr) => {
293 #[derive(Debug)]
294 $vis struct $name;
295
296 impl $crate::Domain for $name {
297 const DOMAIN_NAME: &'static str = $domain_name;
298 }
299
300 impl $crate::UuidDomain for $name {}
301 };
302}
303
304// ============================================================================
305// ID TYPE ALIAS MACRO
306// ============================================================================
307
308/// Create an Id type alias
309///
310/// This macro creates a type alias for a numeric Id.
311///
312/// # Arguments
313///
314/// * `$id_name` - The name for the Id type alias
315/// * `$domain` - The domain type (must implement `IdDomain`)
316///
317/// # Examples
318///
319/// ```rust
320/// use domain_key::{define_id_domain, id_type};
321///
322/// define_id_domain!(UserIdDomain, "user");
323/// id_type!(UserId, UserIdDomain);
324///
325/// let id = UserId::new(1).unwrap();
326/// assert_eq!(id.get(), 1);
327/// ```
328#[macro_export]
329macro_rules! id_type {
330 ($vis:vis $id_name:ident, $domain:ty) => {
331 $vis type $id_name = $crate::Id<$domain>;
332 };
333}
334
335// ============================================================================
336// UUID TYPE ALIAS MACRO
337// ============================================================================
338
339/// Create a Uuid type alias
340///
341/// This macro creates a type alias for a typed Uuid.
342///
343/// Requires the `uuid` feature.
344///
345/// # Arguments
346///
347/// * `$uuid_name` - The name for the Uuid type alias
348/// * `$domain` - The domain type (must implement `UuidDomain`)
349///
350/// # Examples
351///
352/// ```rust
353/// # #[cfg(feature = "uuid")]
354/// # {
355/// use domain_key::{define_uuid_domain, uuid_type};
356///
357/// define_uuid_domain!(OrderUuidDomain, "order");
358/// uuid_type!(OrderUuid, OrderUuidDomain);
359///
360/// let id = OrderUuid::nil();
361/// assert!(id.is_nil());
362/// # }
363/// ```
364#[cfg(feature = "uuid")]
365#[macro_export]
366macro_rules! uuid_type {
367 ($vis:vis $uuid_name:ident, $domain:ty) => {
368 $vis type $uuid_name = $crate::Uuid<$domain>;
369 };
370}
371
372// ============================================================================
373// COMBINED DOMAIN + TYPE ALIAS MACROS
374// ============================================================================
375
376/// Define an Id domain and type alias in one step
377///
378/// This is a convenience macro that combines [`define_id_domain!`] and [`id_type!`].
379///
380/// # Examples
381///
382/// ```rust
383/// use domain_key::{define_id, Id};
384///
385/// define_id!(UserIdDomain => UserId);
386///
387/// let id = UserId::new(42).unwrap();
388/// assert_eq!(id.domain(), "UserId");
389/// ```
390#[macro_export]
391macro_rules! define_id {
392 ($vis:vis $domain:ident => $alias:ident) => {
393 $crate::define_id_domain!(@inner $vis $domain, stringify!($alias));
394 $crate::id_type!($vis $alias, $domain);
395 };
396}
397
398/// Define a Uuid domain and type alias in one step
399///
400/// This is a convenience macro that combines [`define_uuid_domain!`] and [`uuid_type!`].
401///
402/// Requires the `uuid` feature.
403///
404/// # Examples
405///
406/// ```rust
407/// # #[cfg(feature = "uuid")]
408/// # {
409/// use domain_key::{define_uuid, Uuid};
410///
411/// define_uuid!(OrderUuidDomain => OrderUuid);
412///
413/// let id = OrderUuid::nil();
414/// assert_eq!(id.domain(), "OrderUuid");
415/// # }
416/// ```
417#[cfg(feature = "uuid")]
418#[macro_export]
419macro_rules! define_uuid {
420 ($vis:vis $domain:ident => $alias:ident) => {
421 $crate::define_uuid_domain!(@inner $vis $domain, stringify!($alias));
422 $crate::uuid_type!($vis $alias, $domain);
423 };
424}
425
426// ============================================================================
427// BATCH KEY CREATION MACRO
428// ============================================================================
429
430/// Create multiple keys at once with error handling
431///
432/// This macro simplifies the creation of multiple keys from string literals
433/// or expressions, with automatic error collection.
434///
435/// # Examples
436///
437/// ```rust
438/// use domain_key::{define_domain, key_type, batch_keys};
439///
440/// define_domain!(UserDomain, "user");
441/// key_type!(UserKey, UserDomain);
442///
443/// // Create multiple keys, collecting any errors
444/// let result = batch_keys!(UserKey => [
445/// "user_1",
446/// "user_2",
447/// "user_3",
448/// ]);
449///
450/// match result {
451/// Ok(keys) => println!("Created {} keys", keys.len()),
452/// Err(errors) => println!("Failed to create {} keys", errors.len()),
453/// }
454/// ```
455#[macro_export]
456macro_rules! batch_keys {
457 ($key_type:ty => [$($key_str:expr),* $(,)?]) => {{
458 use $crate::__private::{Vec, ToString};
459 let mut keys = Vec::new();
460 let mut errors = Vec::new();
461
462 $(
463 match <$key_type>::new($key_str) {
464 Ok(key) => keys.push(key),
465 Err(e) => errors.push(($key_str.to_string(), e)),
466 }
467 )*
468
469 if errors.is_empty() {
470 Ok(keys)
471 } else {
472 Err(errors)
473 }
474 }};
475}
476
477// ============================================================================
478// TESTING HELPERS
479// ============================================================================
480
481/// Generate test cases for key domains
482///
483/// This macro creates comprehensive test cases for a domain,
484/// testing both valid and invalid keys. The macro generates a `domain_tests`
485/// submodule with test functions.
486///
487/// **Important**: This macro must be used at module level, not inside functions.
488///
489/// # Arguments
490///
491/// * `$domain` - The domain type to test
492/// * `valid` - Array of string literals that should be valid keys
493/// * `invalid` - Array of string literals that should be invalid keys
494///
495/// # Examples
496///
497/// ```rust
498/// use domain_key::{define_domain, test_domain};
499///
500/// define_domain!(MyTestDomain, "test");
501///
502/// // This creates a `domain_tests` module with test functions
503/// test_domain!(MyTestDomain {
504/// valid: [
505/// "valid_key",
506/// "another_valid",
507/// "key123",
508/// ],
509/// invalid: [
510/// "",
511/// "key with spaces",
512/// ]
513/// });
514/// ```
515///
516/// The generated tests will:
517/// - Test that all valid keys can be created successfully
518/// - Test that all invalid keys fail to create with appropriate errors
519/// - Test basic domain properties (name, max length, etc.)
520///
521/// Note: This macro should be used at module level, not inside functions.
522#[macro_export]
523macro_rules! test_domain {
524 // With explicit module name: test_domain!(MyDomain as my_domain_tests { ... })
525 //
526 // Use this form when invoking the macro more than once in the same module to
527 // avoid the `mod domain_tests` name collision.
528 ($domain:ty as $mod_name:ident {
529 valid: [$($valid:literal),* $(,)?],
530 invalid: [$($invalid:literal),* $(,)?] $(,)?
531 }) => {
532 #[cfg(test)]
533 mod $mod_name {
534 use super::*;
535
536 type TestKey = $crate::Key<$domain>;
537
538 #[test]
539 fn test_valid_keys() {
540 $(
541 let key = TestKey::new($valid);
542 assert!(key.is_ok(), "Key '{}' should be valid: {:?}", $valid, key.err());
543 )*
544 }
545
546 #[test]
547 fn test_invalid_keys() {
548 $(
549 let key = TestKey::new($invalid);
550 assert!(key.is_err(), "Key '{}' should be invalid", $invalid);
551 )*
552 }
553
554 #[test]
555 fn test_domain_properties() {
556 use $crate::Domain;
557 use $crate::KeyDomain;
558
559 // Test domain constants
560 assert!(!<$domain>::DOMAIN_NAME.is_empty());
561 assert!(<$domain>::MAX_LENGTH > 0);
562
563 // Test validation help if available
564 if let Some(help) = <$domain>::validation_help() {
565 assert!(!help.is_empty());
566 }
567 }
568 }
569 };
570
571 // Without explicit module name (backward-compatible) — defaults to `domain_tests`.
572 //
573 // Note: Only one such invocation is allowed per module. If you need a second
574 // invocation in the same module, supply an explicit name with the `as` form above.
575 ($domain:ty {
576 valid: [$($valid:literal),* $(,)?],
577 invalid: [$($invalid:literal),* $(,)?] $(,)?
578 }) => {
579 $crate::test_domain!($domain as domain_tests {
580 valid: [$($valid),*],
581 invalid: [$($invalid),*]
582 });
583 };
584}
585
586// ============================================================================
587// TESTS
588// ============================================================================
589
590#[cfg(test)]
591mod tests {
592 use crate::{Domain, Key, KeyDomain};
593 #[cfg(not(feature = "std"))]
594 use alloc::string::ToString;
595
596 // Test define_domain macro
597 define_domain!(MacroTestDomain, "macro_test");
598 type MacroTestKey = Key<MacroTestDomain>;
599
600 // Test define_domain with custom max length
601 define_domain!(LongDomain, "long", 256);
602 #[expect(
603 dead_code,
604 reason = "alias defined to test compile-time macro expansion"
605 )]
606 type LongKey = Key<LongDomain>;
607
608 #[test]
609 fn define_domain_sets_name_and_max_length() {
610 assert_eq!(MacroTestDomain::DOMAIN_NAME, "macro_test");
611 assert_eq!(MacroTestDomain::MAX_LENGTH, crate::DEFAULT_MAX_KEY_LENGTH);
612
613 assert_eq!(LongDomain::DOMAIN_NAME, "long");
614 assert_eq!(LongDomain::MAX_LENGTH, 256);
615 }
616
617 #[test]
618 fn static_key_validates_and_creates_key() {
619 let key = static_key!(MacroTestKey, "static_test");
620 assert_eq!(key.as_str(), "static_test");
621 assert_eq!(key.domain(), "macro_test");
622 }
623
624 #[test]
625 fn key_type_creates_usable_alias() {
626 key_type!(TestKey, MacroTestDomain);
627 let key = TestKey::new("test_key").unwrap();
628 assert_eq!(key.as_str(), "test_key");
629 }
630
631 #[test]
632 fn batch_keys_collects_all_valid_keys() {
633 let result = batch_keys!(MacroTestKey => [
634 "key1",
635 "key2",
636 "key3",
637 ]);
638
639 assert!(result.is_ok());
640 let keys = result.unwrap();
641 assert_eq!(keys.len(), 3);
642 assert_eq!(keys[0].as_str(), "key1");
643 assert_eq!(keys[1].as_str(), "key2");
644 assert_eq!(keys[2].as_str(), "key3");
645 }
646
647 #[test]
648 fn batch_keys_returns_errors_for_invalid_entries() {
649 let result = batch_keys!(MacroTestKey => [
650 "valid_key",
651 "", // This should fail
652 "another_valid",
653 ]);
654
655 assert!(result.is_err());
656 let errors = result.unwrap_err();
657 assert_eq!(errors.len(), 1);
658 assert_eq!(errors[0].0, "");
659 }
660
661 #[test]
662 fn define_id_creates_domain_and_alias() {
663 define_id!(TestIdDomain2 => TestId2);
664 let id = TestId2::new(42).unwrap();
665 assert_eq!(id.get(), 42);
666 assert_eq!(id.domain(), "TestId2");
667 }
668
669 #[cfg(feature = "uuid")]
670 #[test]
671 fn define_uuid_creates_domain_and_alias() {
672 define_uuid!(TestUuidDomain2 => TestUuid2);
673 let id = TestUuid2::nil();
674 assert!(id.is_nil());
675 assert_eq!(id.domain(), "TestUuid2");
676 }
677
678 // Test the test_domain macro - use it at module level
679 #[cfg(test)]
680 mod test_domain_macro_test {
681
682 // Define a test domain specifically for this test
683 define_domain!(pub TestMacroDomain, "test_macro");
684
685 // Apply the test_domain macro
686 test_domain!(TestMacroDomain {
687 valid: ["valid_key", "another_valid", "key123",],
688 invalid: ["",]
689 });
690 }
691}