Skip to main content

trait_kit/core/
macros.rs

1// Copyright (c) 2026 Kirky.X
2// SPDX-License-Identifier: MIT
3//! Declarative macros for reducing boilerplate in module declarations.
4//!
5//! These `macro_rules!` macros generate `ModuleMeta` (and optionally
6//! `AsyncAutoBuilder`) implementations, replacing repetitive hand-written
7//! impl blocks with a single-line invocation.
8
9/// Implements `ModuleMeta` for a module type (no dependencies).
10///
11/// # Syntax
12///
13/// ```text
14/// impl_module_meta!(Type, "name");
15/// impl_module_meta!(Type, "name", deps = [DepA, DepB]);
16/// ```
17///
18/// # Example
19///
20/// ```
21/// use trait_kit::impl_module_meta;
22/// use trait_kit::core::ModuleMeta;
23///
24/// struct MyModule;
25/// impl_module_meta!(MyModule, "my-module");
26///
27/// assert_eq!(MyModule::NAME, "my-module");
28/// assert!(MyModule::dependencies().is_empty());
29/// ```
30#[macro_export]
31macro_rules! impl_module_meta {
32    ($ty:ty, $name:literal) => {
33        impl $crate::core::ModuleMeta for $ty {
34            const NAME: &'static str = $name;
35
36            fn dependencies() -> &'static [(&'static str, std::any::TypeId)] {
37                &[]
38            }
39        }
40    };
41    ($ty:ty, $name:literal, deps = [$($dep:ty),* $(,)?]) => {
42        impl $crate::core::ModuleMeta for $ty {
43            const NAME: &'static str = $name;
44
45            fn dependencies() -> &'static [(&'static str, std::any::TypeId)] {
46                static DEPS: &[(&str, std::any::TypeId)] = &[
47                    $( (<$dep as $crate::core::ModuleMeta>::NAME, std::any::TypeId::of::<$dep>()), )*
48                ];
49                DEPS
50            }
51        }
52    };
53}
54
55/// Implements `AutoBuilder` for a module type (sync counterpart to
56/// `impl_async_auto_builder!`).
57///
58/// # Syntax
59///
60/// ```text
61/// impl_auto_builder!(Type, Capability, Error, |kit| <expr>);
62/// ```
63///
64/// # Example
65///
66/// ```
67/// use std::sync::Arc;
68/// use trait_kit::impl_module_meta;
69/// use trait_kit::impl_auto_builder;
70/// use trait_kit::core::{AutoBuilder, ModuleMeta};
71/// use trait_kit::kit::Kit;
72///
73/// # #[derive(Debug, thiserror::Error)]
74/// # #[error("mock")]
75/// # struct MockErr;
76/// # #[derive(Clone)]
77/// # struct Cap { v: u32 }
78/// struct MyModule;
79/// impl_module_meta!(MyModule, "my-module");
80/// impl_auto_builder!(
81///     MyModule,
82///     Arc<Cap>,
83///     MockErr,
84///     |_kit| Ok(Arc::new(Cap { v: 42 }))
85/// );
86/// ```
87#[macro_export]
88macro_rules! impl_auto_builder {
89    ($ty:ty, $cap:ty, $err:ty, |$kit:ident| $body:expr) => {
90        impl $crate::core::AutoBuilder for $ty {
91            type Capability = $cap;
92            type Error = $err;
93
94            #[track_caller]
95            fn build(
96                $kit: &$crate::kit::Kit,
97            ) -> ::std::result::Result<Self::Capability, Self::Error> {
98                $body
99            }
100        }
101    };
102}
103
104/// Implements `AsyncAutoBuilder` for a module type.
105///
106/// The body expression must evaluate to
107/// `Pin<Box<dyn Future<Output = Result<Capability, Error>> + Send + 'a>>`.
108/// The closure parameter `|kit|` binds the `&AsyncKit` argument, matching
109/// the hand-written impl pattern.
110///
111/// # Syntax
112///
113/// ```text
114/// impl_async_auto_builder!(Type, Capability, Error, |kit| <expr>);
115/// ```
116///
117/// # Example
118///
119/// ```
120/// use std::sync::Arc;
121/// use trait_kit::impl_module_meta;
122/// use trait_kit::impl_async_auto_builder;
123/// use trait_kit::core::{AsyncAutoBuilder, ModuleMeta};
124/// use trait_kit::kit::AsyncKit;
125///
126/// # #[derive(Debug, thiserror::Error)]
127/// # #[error("mock")]
128/// # struct MockErr;
129/// # #[derive(Clone)]
130/// # struct Cap { v: u32 }
131/// struct MyAsyncModule;
132/// impl_module_meta!(MyAsyncModule, "my-async");
133/// impl_async_auto_builder!(
134///     MyAsyncModule,
135///     Arc<Cap>,
136///     MockErr,
137///     |kit| Box::pin(async move {
138///         let _ = kit;
139///         Ok(Arc::new(Cap { v: 42 }))
140///     })
141/// );
142/// ```
143#[cfg(feature = "async")]
144#[macro_export]
145macro_rules! impl_async_auto_builder {
146    ($ty:ty, $cap:ty, $err:ty, |$kit:ident| $body:expr) => {
147        impl $crate::core::AsyncAutoBuilder for $ty {
148            type Capability = $cap;
149            type Error = $err;
150
151            #[track_caller]
152            fn build<'a>(
153                $kit: &'a $crate::kit::AsyncKit,
154            ) -> ::std::pin::Pin<
155                ::std::boxed::Box<
156                    dyn ::std::future::Future<
157                            Output = ::std::result::Result<Self::Capability, Self::Error>,
158                        > + Send
159                        + 'a,
160                >,
161            > {
162                $body
163            }
164        }
165    };
166}
167
168#[cfg(test)]
169mod tests {
170    use crate::core::ModuleMeta;
171
172    // === Fixtures ===
173
174    struct MacroModuleNoDeps;
175    impl_module_meta!(MacroModuleNoDeps, "macro-no-deps");
176
177    struct Dep1;
178    impl_module_meta!(Dep1, "dep1");
179
180    struct Dep2;
181    impl_module_meta!(Dep2, "dep2");
182
183    struct MacroModuleWithDeps;
184    impl_module_meta!(MacroModuleWithDeps, "macro-with-deps", deps = [Dep1, Dep2]);
185
186    // Hand-written equivalents for comparison
187
188    struct HandWrittenNoDeps;
189    impl ModuleMeta for HandWrittenNoDeps {
190        const NAME: &'static str = "macro-no-deps";
191        fn dependencies() -> &'static [(&'static str, std::any::TypeId)] {
192            &[]
193        }
194    }
195
196    struct HandWrittenWithDeps;
197    impl ModuleMeta for HandWrittenWithDeps {
198        const NAME: &'static str = "macro-with-deps";
199        fn dependencies() -> &'static [(&'static str, std::any::TypeId)] {
200            static DEPS: &[(&str, std::any::TypeId)] = &[
201                (<Dep1 as ModuleMeta>::NAME, std::any::TypeId::of::<Dep1>()),
202                (<Dep2 as ModuleMeta>::NAME, std::any::TypeId::of::<Dep2>()),
203            ];
204            DEPS
205        }
206    }
207
208    // === Tests ===
209
210    #[test]
211    fn macro_generates_correct_name_no_deps() {
212        assert_eq!(MacroModuleNoDeps::NAME, "macro-no-deps");
213    }
214
215    #[test]
216    fn macro_generates_empty_dependencies_when_no_deps() {
217        assert!(MacroModuleNoDeps::dependencies().is_empty());
218    }
219
220    #[test]
221    fn macro_generates_correct_name_with_deps() {
222        assert_eq!(MacroModuleWithDeps::NAME, "macro-with-deps");
223    }
224
225    #[test]
226    fn macro_generates_correct_dependency_count() {
227        assert_eq!(MacroModuleWithDeps::dependencies().len(), 2);
228    }
229
230    #[test]
231    fn macro_dependency_names_match_module_meta_names() {
232        let deps = MacroModuleWithDeps::dependencies();
233        assert_eq!(deps[0].0, "dep1");
234        assert_eq!(deps[1].0, "dep2");
235    }
236
237    #[test]
238    fn macro_dependency_type_ids_match_hand_written() {
239        let macro_deps = MacroModuleWithDeps::dependencies();
240        let hand_deps = HandWrittenWithDeps::dependencies();
241        assert_eq!(macro_deps.len(), hand_deps.len());
242        for (i, (m, h)) in macro_deps.iter().zip(hand_deps.iter()).enumerate() {
243            assert_eq!(m.0, h.0, "dep {i}: name mismatch");
244            assert_eq!(m.1, h.1, "dep {i}: TypeId mismatch");
245        }
246    }
247
248    #[test]
249    fn macro_name_equals_hand_written_name() {
250        assert_eq!(MacroModuleNoDeps::NAME, HandWrittenNoDeps::NAME);
251        assert_eq!(MacroModuleWithDeps::NAME, HandWrittenWithDeps::NAME);
252    }
253
254    #[test]
255    fn macro_dependencies_equal_hand_written_no_deps() {
256        let m = MacroModuleNoDeps::dependencies();
257        let h = HandWrittenNoDeps::dependencies();
258        assert_eq!(m.len(), h.len());
259    }
260}
261
262#[cfg(all(test, feature = "async"))]
263mod async_macro_tests {
264    use crate::core::{AsyncAutoBuilder, ModuleMeta};
265    use crate::kit::AsyncKit;
266    use crate::test_helpers::block_on;
267    use std::future::Future;
268    use std::pin::Pin;
269    use std::sync::Arc;
270    use thiserror::Error;
271
272    // === Fixtures ===
273
274    #[derive(Debug, Error)]
275    #[allow(dead_code, reason = "mock error type verifies trait signature only")]
276    enum MockErr {
277        #[error("mock async build failed: {0}")]
278        Failed(String),
279    }
280
281    #[derive(Clone, Debug, PartialEq, Eq)]
282    struct AsyncCap {
283        value: u32,
284    }
285
286    // Macro-generated impl
287    struct MacroAsyncModule;
288    impl_module_meta!(MacroAsyncModule, "macro-async");
289    impl_async_auto_builder!(MacroAsyncModule, Arc<AsyncCap>, MockErr, |kit| Box::pin(
290        async move {
291            let _ = kit;
292            Ok(Arc::new(AsyncCap { value: 42 }))
293        }
294    ));
295
296    // Hand-written impl for comparison
297    struct HandAsyncModule;
298    impl ModuleMeta for HandAsyncModule {
299        const NAME: &'static str = "macro-async";
300        fn dependencies() -> &'static [(&'static str, std::any::TypeId)] {
301            &[]
302        }
303    }
304    impl AsyncAutoBuilder for HandAsyncModule {
305        type Capability = Arc<AsyncCap>;
306        type Error = MockErr;
307        fn build<'a>(
308            kit: &'a AsyncKit,
309        ) -> Pin<Box<dyn Future<Output = Result<Self::Capability, Self::Error>> + Send + 'a>>
310        {
311            let _ = kit;
312            Box::pin(async move { Ok(Arc::new(AsyncCap { value: 42 })) })
313        }
314    }
315
316    // Error-propagation fixture
317    struct ErrAsyncModule;
318    impl_module_meta!(ErrAsyncModule, "err-async");
319    impl_async_auto_builder!(ErrAsyncModule, Arc<AsyncCap>, MockErr, |kit| Box::pin(
320        async move {
321            let _ = kit;
322            Err(MockErr::Failed("intentional".to_string()))
323        }
324    ));
325
326    // === Tests ===
327
328    #[test]
329    fn macro_async_generates_correct_name() {
330        assert_eq!(MacroAsyncModule::NAME, "macro-async");
331    }
332
333    #[test]
334    fn macro_async_generates_empty_dependencies() {
335        assert!(MacroAsyncModule::dependencies().is_empty());
336    }
337
338    #[test]
339    fn macro_async_capability_type_matches_hand_written() {
340        assert_eq!(
341            std::any::TypeId::of::<<MacroAsyncModule as AsyncAutoBuilder>::Capability>(),
342            std::any::TypeId::of::<<HandAsyncModule as AsyncAutoBuilder>::Capability>(),
343        );
344    }
345
346    #[test]
347    fn macro_async_error_type_matches_hand_written() {
348        assert_eq!(
349            std::any::TypeId::of::<<MacroAsyncModule as AsyncAutoBuilder>::Error>(),
350            std::any::TypeId::of::<<HandAsyncModule as AsyncAutoBuilder>::Error>(),
351        );
352    }
353
354    #[test]
355    fn macro_async_build_returns_expected_capability() {
356        let kit = AsyncKit::new();
357        let cap = block_on(MacroAsyncModule::build(&kit)).unwrap();
358        assert_eq!(cap.value, 42);
359    }
360
361    #[test]
362    fn macro_async_build_result_matches_hand_written() {
363        let kit = AsyncKit::new();
364        let macro_cap = block_on(MacroAsyncModule::build(&kit)).unwrap();
365        let hand_cap = block_on(HandAsyncModule::build(&kit)).unwrap();
366        assert_eq!(macro_cap, hand_cap);
367    }
368
369    #[test]
370    fn macro_async_build_propagates_errors() {
371        let kit = AsyncKit::new();
372        let result = block_on(ErrAsyncModule::build(&kit));
373        assert!(result.is_err());
374    }
375
376    #[test]
377    fn macro_async_name_equals_hand_written_name() {
378        assert_eq!(MacroAsyncModule::NAME, HandAsyncModule::NAME);
379    }
380
381    #[test]
382    fn hand_written_async_module_dependencies_empty() {
383        assert!(HandAsyncModule::dependencies().is_empty());
384    }
385}
386
387#[cfg(test)]
388mod sync_auto_builder_tests {
389    use crate::core::{AutoBuilder, ModuleMeta};
390    use crate::kit::Kit;
391    use std::sync::Arc;
392    use thiserror::Error;
393
394    // === Fixtures ===
395
396    #[derive(Debug, Error)]
397    #[allow(dead_code, reason = "mock error type verifies trait signature only")]
398    enum MockErr {
399        #[error("mock build failed: {0}")]
400        Failed(String),
401    }
402
403    #[derive(Clone, Debug, PartialEq, Eq)]
404    struct SyncCap {
405        value: u32,
406    }
407
408    // Macro-generated impl
409    struct MacroSyncModule;
410    impl_module_meta!(MacroSyncModule, "macro-sync");
411    impl_auto_builder!(MacroSyncModule, Arc<SyncCap>, MockErr, |_kit| Ok(Arc::new(
412        SyncCap { value: 42 }
413    )));
414
415    // Hand-written impl for comparison
416    struct HandSyncModule;
417    impl ModuleMeta for HandSyncModule {
418        const NAME: &'static str = "macro-sync";
419        fn dependencies() -> &'static [(&'static str, std::any::TypeId)] {
420            &[]
421        }
422    }
423    impl AutoBuilder for HandSyncModule {
424        type Capability = Arc<SyncCap>;
425        type Error = MockErr;
426        fn build(_kit: &Kit) -> Result<Self::Capability, Self::Error> {
427            Ok(Arc::new(SyncCap { value: 42 }))
428        }
429    }
430
431    // Error-propagation fixture
432    struct ErrSyncModule;
433    impl_module_meta!(ErrSyncModule, "err-sync");
434    impl_auto_builder!(ErrSyncModule, Arc<SyncCap>, MockErr, |_kit| Err(
435        MockErr::Failed("intentional".to_string())
436    ));
437
438    // === Tests ===
439
440    #[test]
441    fn macro_sync_generates_correct_name() {
442        assert_eq!(MacroSyncModule::NAME, "macro-sync");
443    }
444
445    #[test]
446    fn macro_sync_generates_empty_dependencies() {
447        assert!(MacroSyncModule::dependencies().is_empty());
448    }
449
450    #[test]
451    fn macro_sync_capability_type_matches_hand_written() {
452        assert_eq!(
453            std::any::TypeId::of::<<MacroSyncModule as AutoBuilder>::Capability>(),
454            std::any::TypeId::of::<<HandSyncModule as AutoBuilder>::Capability>(),
455        );
456    }
457
458    #[test]
459    fn macro_sync_error_type_matches_hand_written() {
460        assert_eq!(
461            std::any::TypeId::of::<<MacroSyncModule as AutoBuilder>::Error>(),
462            std::any::TypeId::of::<<HandSyncModule as AutoBuilder>::Error>(),
463        );
464    }
465
466    #[test]
467    fn macro_sync_build_returns_expected_capability() {
468        let kit = Kit::new();
469        let cap = MacroSyncModule::build(&kit).unwrap();
470        assert_eq!(cap.value, 42);
471    }
472
473    #[test]
474    fn macro_sync_build_result_matches_hand_written() {
475        let kit = Kit::new();
476        let macro_cap = MacroSyncModule::build(&kit).unwrap();
477        let hand_cap = HandSyncModule::build(&kit).unwrap();
478        assert_eq!(macro_cap, hand_cap);
479    }
480
481    #[test]
482    fn macro_sync_build_propagates_errors() {
483        let kit = Kit::new();
484        let result = ErrSyncModule::build(&kit);
485        assert!(result.is_err());
486    }
487
488    #[test]
489    fn macro_sync_name_equals_hand_written_name() {
490        assert_eq!(MacroSyncModule::NAME, HandSyncModule::NAME);
491    }
492
493    #[test]
494    fn hand_written_sync_module_dependencies_empty() {
495        assert!(HandSyncModule::dependencies().is_empty());
496    }
497}