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                    $((stringify!($dep), std::any::TypeId::of::<$dep>()),)*
48                ];
49                DEPS
50            }
51        }
52    };
53}
54
55/// Implements `AsyncAutoBuilder` for a module type.
56///
57/// The body expression must evaluate to
58/// `Pin<Box<dyn Future<Output = Result<Capability, Error>> + Send + 'a>>`.
59/// The closure parameter `|kit|` binds the `&AsyncKit` argument, matching
60/// the hand-written impl pattern.
61///
62/// # Syntax
63///
64/// ```text
65/// impl_async_auto_builder!(Type, Capability, Error, |kit| <expr>);
66/// ```
67///
68/// # Example
69///
70/// ```
71/// use std::sync::Arc;
72/// use trait_kit::impl_module_meta;
73/// use trait_kit::impl_async_auto_builder;
74/// use trait_kit::core::{AsyncAutoBuilder, ModuleMeta};
75/// use trait_kit::kit::AsyncKit;
76///
77/// # #[derive(Debug, thiserror::Error)]
78/// # #[error("mock")]
79/// # struct MockErr;
80/// # #[derive(Clone)]
81/// # struct Cap { v: u32 }
82/// struct MyAsyncModule;
83/// impl_module_meta!(MyAsyncModule, "my-async");
84/// impl_async_auto_builder!(
85///     MyAsyncModule,
86///     Arc<Cap>,
87///     MockErr,
88///     |kit| Box::pin(async move {
89///         let _ = kit;
90///         Ok(Arc::new(Cap { v: 42 }))
91///     })
92/// );
93/// ```
94#[cfg(feature = "async")]
95#[macro_export]
96macro_rules! impl_async_auto_builder {
97    ($ty:ty, $cap:ty, $err:ty, |$kit:ident| $body:expr) => {
98        impl $crate::core::AsyncAutoBuilder for $ty {
99            type Capability = $cap;
100            type Error = $err;
101
102            fn build<'a>(
103                $kit: &'a $crate::kit::AsyncKit,
104            ) -> ::std::pin::Pin<
105                ::std::boxed::Box<
106                    dyn ::std::future::Future<
107                            Output = ::std::result::Result<Self::Capability, Self::Error>,
108                        > + Send
109                        + 'a,
110                >,
111            > {
112                $body
113            }
114        }
115    };
116}
117
118#[cfg(test)]
119mod tests {
120    use crate::core::ModuleMeta;
121
122    // === Fixtures ===
123
124    struct MacroModuleNoDeps;
125    impl_module_meta!(MacroModuleNoDeps, "macro-no-deps");
126
127    struct Dep1;
128    impl_module_meta!(Dep1, "dep1");
129
130    struct Dep2;
131    impl_module_meta!(Dep2, "dep2");
132
133    struct MacroModuleWithDeps;
134    impl_module_meta!(MacroModuleWithDeps, "macro-with-deps", deps = [Dep1, Dep2]);
135
136    // Hand-written equivalents for comparison
137
138    struct HandWrittenNoDeps;
139    impl ModuleMeta for HandWrittenNoDeps {
140        const NAME: &'static str = "macro-no-deps";
141        fn dependencies() -> &'static [(&'static str, std::any::TypeId)] {
142            &[]
143        }
144    }
145
146    struct HandWrittenWithDeps;
147    impl ModuleMeta for HandWrittenWithDeps {
148        const NAME: &'static str = "macro-with-deps";
149        fn dependencies() -> &'static [(&'static str, std::any::TypeId)] {
150            static DEPS: &[(&str, std::any::TypeId)] = &[
151                ("Dep1", std::any::TypeId::of::<Dep1>()),
152                ("Dep2", std::any::TypeId::of::<Dep2>()),
153            ];
154            DEPS
155        }
156    }
157
158    // === Tests ===
159
160    #[test]
161    fn macro_generates_correct_name_no_deps() {
162        assert_eq!(MacroModuleNoDeps::NAME, "macro-no-deps");
163    }
164
165    #[test]
166    fn macro_generates_empty_dependencies_when_no_deps() {
167        assert!(MacroModuleNoDeps::dependencies().is_empty());
168    }
169
170    #[test]
171    fn macro_generates_correct_name_with_deps() {
172        assert_eq!(MacroModuleWithDeps::NAME, "macro-with-deps");
173    }
174
175    #[test]
176    fn macro_generates_correct_dependency_count() {
177        assert_eq!(MacroModuleWithDeps::dependencies().len(), 2);
178    }
179
180    #[test]
181    fn macro_dependency_names_match_stringified_types() {
182        let deps = MacroModuleWithDeps::dependencies();
183        assert_eq!(deps[0].0, "Dep1");
184        assert_eq!(deps[1].0, "Dep2");
185    }
186
187    #[test]
188    fn macro_dependency_type_ids_match_hand_written() {
189        let macro_deps = MacroModuleWithDeps::dependencies();
190        let hand_deps = HandWrittenWithDeps::dependencies();
191        assert_eq!(macro_deps.len(), hand_deps.len());
192        for (i, (m, h)) in macro_deps.iter().zip(hand_deps.iter()).enumerate() {
193            assert_eq!(m.0, h.0, "dep {i}: name mismatch");
194            assert_eq!(m.1, h.1, "dep {i}: TypeId mismatch");
195        }
196    }
197
198    #[test]
199    fn macro_name_equals_hand_written_name() {
200        assert_eq!(MacroModuleNoDeps::NAME, HandWrittenNoDeps::NAME);
201        assert_eq!(MacroModuleWithDeps::NAME, HandWrittenWithDeps::NAME);
202    }
203
204    #[test]
205    fn macro_dependencies_equal_hand_written_no_deps() {
206        let m = MacroModuleNoDeps::dependencies();
207        let h = HandWrittenNoDeps::dependencies();
208        assert_eq!(m.len(), h.len());
209    }
210}
211
212#[cfg(all(test, feature = "async"))]
213mod async_macro_tests {
214    use crate::core::{AsyncAutoBuilder, ModuleMeta};
215    use crate::kit::AsyncKit;
216    use crate::test_helpers::block_on;
217    use std::future::Future;
218    use std::pin::Pin;
219    use std::sync::Arc;
220    use thiserror::Error;
221
222    // === Fixtures ===
223
224    #[derive(Debug, Error)]
225    #[allow(dead_code, reason = "mock error type verifies trait signature only")]
226    enum MockErr {
227        #[error("mock async build failed: {0}")]
228        Failed(String),
229    }
230
231    #[derive(Clone, Debug, PartialEq, Eq)]
232    struct AsyncCap {
233        value: u32,
234    }
235
236    // Macro-generated impl
237    struct MacroAsyncModule;
238    impl_module_meta!(MacroAsyncModule, "macro-async");
239    impl_async_auto_builder!(MacroAsyncModule, Arc<AsyncCap>, MockErr, |kit| Box::pin(
240        async move {
241            let _ = kit;
242            Ok(Arc::new(AsyncCap { value: 42 }))
243        }
244    ));
245
246    // Hand-written impl for comparison
247    struct HandAsyncModule;
248    impl ModuleMeta for HandAsyncModule {
249        const NAME: &'static str = "macro-async";
250        fn dependencies() -> &'static [(&'static str, std::any::TypeId)] {
251            &[]
252        }
253    }
254    impl AsyncAutoBuilder for HandAsyncModule {
255        type Capability = Arc<AsyncCap>;
256        type Error = MockErr;
257        fn build<'a>(
258            kit: &'a AsyncKit,
259        ) -> Pin<Box<dyn Future<Output = Result<Self::Capability, Self::Error>> + Send + 'a>>
260        {
261            let _ = kit;
262            Box::pin(async move { Ok(Arc::new(AsyncCap { value: 42 })) })
263        }
264    }
265
266    // Error-propagation fixture
267    struct ErrAsyncModule;
268    impl_module_meta!(ErrAsyncModule, "err-async");
269    impl_async_auto_builder!(ErrAsyncModule, Arc<AsyncCap>, MockErr, |kit| Box::pin(
270        async move {
271            let _ = kit;
272            Err(MockErr::Failed("intentional".to_string()))
273        }
274    ));
275
276    // === Tests ===
277
278    #[test]
279    fn macro_async_generates_correct_name() {
280        assert_eq!(MacroAsyncModule::NAME, "macro-async");
281    }
282
283    #[test]
284    fn macro_async_generates_empty_dependencies() {
285        assert!(MacroAsyncModule::dependencies().is_empty());
286    }
287
288    #[test]
289    fn macro_async_capability_type_matches_hand_written() {
290        assert_eq!(
291            std::any::TypeId::of::<<MacroAsyncModule as AsyncAutoBuilder>::Capability>(),
292            std::any::TypeId::of::<<HandAsyncModule as AsyncAutoBuilder>::Capability>(),
293        );
294    }
295
296    #[test]
297    fn macro_async_error_type_matches_hand_written() {
298        assert_eq!(
299            std::any::TypeId::of::<<MacroAsyncModule as AsyncAutoBuilder>::Error>(),
300            std::any::TypeId::of::<<HandAsyncModule as AsyncAutoBuilder>::Error>(),
301        );
302    }
303
304    #[test]
305    fn macro_async_build_returns_expected_capability() {
306        let kit = AsyncKit::new();
307        let cap = block_on(MacroAsyncModule::build(&kit)).unwrap();
308        assert_eq!(cap.value, 42);
309    }
310
311    #[test]
312    fn macro_async_build_result_matches_hand_written() {
313        let kit = AsyncKit::new();
314        let macro_cap = block_on(MacroAsyncModule::build(&kit)).unwrap();
315        let hand_cap = block_on(HandAsyncModule::build(&kit)).unwrap();
316        assert_eq!(macro_cap, hand_cap);
317    }
318
319    #[test]
320    fn macro_async_build_propagates_errors() {
321        let kit = AsyncKit::new();
322        let result = block_on(ErrAsyncModule::build(&kit));
323        assert!(result.is_err());
324    }
325
326    #[test]
327    fn macro_async_name_equals_hand_written_name() {
328        assert_eq!(MacroAsyncModule::NAME, HandAsyncModule::NAME);
329    }
330}