Skip to main content

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}