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<::std::boxed::Box<
105                dyn ::std::future::Future<
106                    Output = ::std::result::Result<Self::Capability, Self::Error>,
107                > + Send + 'a,
108            >> {
109                $body
110            }
111        }
112    };
113}
114
115#[cfg(test)]
116mod tests {
117    use crate::core::ModuleMeta;
118
119    // === Fixtures ===
120
121    struct MacroModuleNoDeps;
122    impl_module_meta!(MacroModuleNoDeps, "macro-no-deps");
123
124    struct Dep1;
125    impl_module_meta!(Dep1, "dep1");
126
127    struct Dep2;
128    impl_module_meta!(Dep2, "dep2");
129
130    struct MacroModuleWithDeps;
131    impl_module_meta!(MacroModuleWithDeps, "macro-with-deps", deps = [Dep1, Dep2]);
132
133    // Hand-written equivalents for comparison
134
135    struct HandWrittenNoDeps;
136    impl ModuleMeta for HandWrittenNoDeps {
137        const NAME: &'static str = "macro-no-deps";
138        fn dependencies() -> &'static [(&'static str, std::any::TypeId)] {
139            &[]
140        }
141    }
142
143    struct HandWrittenWithDeps;
144    impl ModuleMeta for HandWrittenWithDeps {
145        const NAME: &'static str = "macro-with-deps";
146        fn dependencies() -> &'static [(&'static str, std::any::TypeId)] {
147            static DEPS: &[(&str, std::any::TypeId)] = &[
148                ("Dep1", std::any::TypeId::of::<Dep1>()),
149                ("Dep2", std::any::TypeId::of::<Dep2>()),
150            ];
151            DEPS
152        }
153    }
154
155    // === Tests ===
156
157    #[test]
158    fn macro_generates_correct_name_no_deps() {
159        assert_eq!(MacroModuleNoDeps::NAME, "macro-no-deps");
160    }
161
162    #[test]
163    fn macro_generates_empty_dependencies_when_no_deps() {
164        assert!(MacroModuleNoDeps::dependencies().is_empty());
165    }
166
167    #[test]
168    fn macro_generates_correct_name_with_deps() {
169        assert_eq!(MacroModuleWithDeps::NAME, "macro-with-deps");
170    }
171
172    #[test]
173    fn macro_generates_correct_dependency_count() {
174        assert_eq!(MacroModuleWithDeps::dependencies().len(), 2);
175    }
176
177    #[test]
178    fn macro_dependency_names_match_stringified_types() {
179        let deps = MacroModuleWithDeps::dependencies();
180        assert_eq!(deps[0].0, "Dep1");
181        assert_eq!(deps[1].0, "Dep2");
182    }
183
184    #[test]
185    fn macro_dependency_type_ids_match_hand_written() {
186        let macro_deps = MacroModuleWithDeps::dependencies();
187        let hand_deps = HandWrittenWithDeps::dependencies();
188        assert_eq!(macro_deps.len(), hand_deps.len());
189        for (i, (m, h)) in macro_deps.iter().zip(hand_deps.iter()).enumerate() {
190            assert_eq!(m.0, h.0, "dep {i}: name mismatch");
191            assert_eq!(m.1, h.1, "dep {i}: TypeId mismatch");
192        }
193    }
194
195    #[test]
196    fn macro_name_equals_hand_written_name() {
197        assert_eq!(MacroModuleNoDeps::NAME, HandWrittenNoDeps::NAME);
198        assert_eq!(MacroModuleWithDeps::NAME, HandWrittenWithDeps::NAME);
199    }
200
201    #[test]
202    fn macro_dependencies_equal_hand_written_no_deps() {
203        let m = MacroModuleNoDeps::dependencies();
204        let h = HandWrittenNoDeps::dependencies();
205        assert_eq!(m.len(), h.len());
206    }
207}
208
209#[cfg(all(test, feature = "async"))]
210mod async_macro_tests {
211    use crate::core::{AsyncAutoBuilder, ModuleMeta};
212    use crate::kit::AsyncKit;
213    use crate::test_helpers::block_on;
214    use std::future::Future;
215    use std::pin::Pin;
216    use std::sync::Arc;
217    use thiserror::Error;
218
219    // === Fixtures ===
220
221    #[derive(Debug, Error)]
222    #[allow(dead_code, reason = "mock error type verifies trait signature only")]
223    enum MockErr {
224        #[error("mock async build failed: {0}")]
225        Failed(String),
226    }
227
228    #[derive(Clone, Debug, PartialEq, Eq)]
229    struct AsyncCap {
230        value: u32,
231    }
232
233    // Macro-generated impl
234    struct MacroAsyncModule;
235    impl_module_meta!(MacroAsyncModule, "macro-async");
236    impl_async_auto_builder!(
237        MacroAsyncModule,
238        Arc<AsyncCap>,
239        MockErr,
240        |kit| Box::pin(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            let _ = kit;
261            Box::pin(async move { Ok(Arc::new(AsyncCap { value: 42 })) })
262        }
263    }
264
265    // Error-propagation fixture
266    struct ErrAsyncModule;
267    impl_module_meta!(ErrAsyncModule, "err-async");
268    impl_async_auto_builder!(
269        ErrAsyncModule,
270        Arc<AsyncCap>,
271        MockErr,
272        |kit| Box::pin(async move {
273            let _ = kit;
274            Err(MockErr::Failed("intentional".to_string()))
275        })
276    );
277
278    // === Tests ===
279
280    #[test]
281    fn macro_async_generates_correct_name() {
282        assert_eq!(MacroAsyncModule::NAME, "macro-async");
283    }
284
285    #[test]
286    fn macro_async_generates_empty_dependencies() {
287        assert!(MacroAsyncModule::dependencies().is_empty());
288    }
289
290    #[test]
291    fn macro_async_capability_type_matches_hand_written() {
292        assert_eq!(
293            std::any::TypeId::of::<<MacroAsyncModule as AsyncAutoBuilder>::Capability>(),
294            std::any::TypeId::of::<<HandAsyncModule as AsyncAutoBuilder>::Capability>(),
295        );
296    }
297
298    #[test]
299    fn macro_async_error_type_matches_hand_written() {
300        assert_eq!(
301            std::any::TypeId::of::<<MacroAsyncModule as AsyncAutoBuilder>::Error>(),
302            std::any::TypeId::of::<<HandAsyncModule as AsyncAutoBuilder>::Error>(),
303        );
304    }
305
306    #[test]
307    fn macro_async_build_returns_expected_capability() {
308        let kit = AsyncKit::new();
309        let cap = block_on(MacroAsyncModule::build(&kit)).unwrap();
310        assert_eq!(cap.value, 42);
311    }
312
313    #[test]
314    fn macro_async_build_result_matches_hand_written() {
315        let kit = AsyncKit::new();
316        let macro_cap = block_on(MacroAsyncModule::build(&kit)).unwrap();
317        let hand_cap = block_on(HandAsyncModule::build(&kit)).unwrap();
318        assert_eq!(macro_cap, hand_cap);
319    }
320
321    #[test]
322    fn macro_async_build_propagates_errors() {
323        let kit = AsyncKit::new();
324        let result = block_on(ErrAsyncModule::build(&kit));
325        assert!(result.is_err());
326    }
327
328    #[test]
329    fn macro_async_name_equals_hand_written_name() {
330        assert_eq!(MacroAsyncModule::NAME, HandAsyncModule::NAME);
331    }
332}