Skip to main content

trait_kit/core/
meta.rs

1// Copyright (c) 2026 Kirky.X
2// SPDX-License-Identifier: MIT
3//! Core traits for module declaration and dependency management.
4
5#[cfg(feature = "async")]
6use std::future::Future;
7#[cfg(feature = "async")]
8use std::pin::Pin;
9#[cfg(feature = "interface")]
10use std::sync::Arc;
11
12/// Metadata trait for module registration.
13pub trait ModuleMeta: 'static {
14    /// The diagnostic name of this module.
15    const NAME: &'static str;
16
17    /// Returns (name, `TypeId`) pairs for modules this module depends on.
18    fn dependencies() -> &'static [(&'static str, std::any::TypeId)];
19}
20
21/// Builder trait for module construction.
22///
23/// Implemented by the user for each module.
24pub trait AutoBuilder: ModuleMeta {
25    /// The capability type this module provides. Must be Clone.
26    type Capability: Clone + 'static;
27
28    /// The error type returned on build failure.
29    type Error: std::error::Error + Send + 'static;
30
31    /// Build the module's capability using the provided Kit.
32    ///
33    /// # Errors
34    ///
35    /// Returns `Self::Error` if the module fails to build.
36    fn build(kit: &crate::kit::Kit) -> Result<Self::Capability, Self::Error>;
37}
38
39/// Marker trait for interface/implementation separation.
40///
41/// Automatically implemented for all `'static` types (including `?Sized`
42/// trait objects like `dyn MyTrait`). Used by the `interface` feature to
43/// enable `register_as<M, I>()` and `resolve<I>()` for type-erased
44/// dependency injection behind a `dyn Trait` interface.
45#[cfg(feature = "interface")]
46pub trait Interface: 'static {}
47
48#[cfg(feature = "interface")]
49impl<T: ?Sized + 'static> Interface for T {}
50
51/// Extension trait for interface/implementation separation.
52///
53/// Unlike [`AutoBuilder`], this trait associates a concrete `Capability`
54/// type with a `?Sized` `Interface` type (e.g., `dyn Logger`). The
55/// [`into_interface`](InterfaceBuilder::into_interface) method performs the
56/// type erasure, converting the concrete capability into
57/// `Arc<Self::Interface>`.
58///
59/// Used by `register_as<M>()` and `resolve<I>()` behind the `interface`
60/// feature. This trait does **not** modify [`AutoBuilder`], so existing
61/// module impls are unaffected.
62#[cfg(feature = "interface")]
63pub trait InterfaceBuilder: ModuleMeta {
64    /// The interface type (e.g., `dyn Logger`). Must be `?Sized + 'static`.
65    type Interface: ?Sized + 'static;
66
67    /// The concrete capability type. Must be `Clone + 'static`.
68    type Capability: Clone + 'static;
69
70    /// The error type returned on build failure.
71    type Error: std::error::Error + Send + 'static;
72
73    /// Build the module's concrete capability using the provided Kit.
74    ///
75    /// # Errors
76    ///
77    /// Returns `Self::Error` if the module fails to build.
78    fn build(kit: &crate::kit::Kit) -> Result<Self::Capability, Self::Error>;
79
80    /// Convert the concrete capability into a type-erased interface object.
81    ///
82    /// Typically implemented as `Ok(cap)` relying on `Arc<T> → Arc<dyn Trait>`
83    /// unsized coercion, where `T: Self::Interface`.
84    fn into_interface(cap: Self::Capability) -> Arc<Self::Interface>;
85}
86
87/// Async builder trait for module construction in async context.
88///
89/// Async counterpart of [`AutoBuilder`]. Implement this for modules requiring
90/// async initialization (database pools, HTTP clients, cache backends).
91///
92/// The `build` method returns a `Pin<Box<dyn Future + Send>>` rather than using
93/// native `async fn` in trait so that the trait can be type-erased through the
94/// `AsyncBuildFn` stored in `AsyncKit`'s dependency graph (Phase 1b). Rust
95/// 1.91 supports `async fn` in trait (stable since 1.75), but `dyn`-compatible
96/// dispatch still requires the explicit `Pin<Box>` indirection.
97///
98/// Compared to [`AutoBuilder`], the associated types tighten bounds:
99/// - `Capability: Clone + Send + Sync + 'static` (cross-thread sharing).
100/// - `Error: std::error::Error + Send + 'static` (cross-thread error propagation).
101///
102/// Requires the `async` feature on the crate.
103#[cfg(feature = "async")]
104pub trait AsyncAutoBuilder: ModuleMeta {
105    /// The capability type this module provides. Must be `Clone + Send + Sync`.
106    type Capability: Clone + Send + Sync + 'static;
107
108    /// The error type returned on build failure. Must be `Send + 'static`.
109    type Error: std::error::Error + Send + 'static;
110
111    /// Build the module's capability using the provided `AsyncKit`.
112    ///
113    /// The returned future borrows the kit for lifetime `'a`, allowing
114    /// the build callback to read configs / require dependencies from the kit
115    /// during async construction.
116    ///
117    /// # Errors
118    ///
119    /// Returns `Self::Error` if the module fails to build.
120    #[allow(
121        clippy::type_complexity,
122        reason = "Pin<Box<dyn Future + Send>> is the canonical dyn-compatible async trait dispatch type"
123    )]
124    fn build<'a>(
125        kit: &'a crate::kit::AsyncKit,
126    ) -> Pin<Box<dyn Future<Output = Result<Self::Capability, Self::Error>> + Send + 'a>>;
127}
128
129/// Type-erased build function stored in the dependency graph.
130///
131/// Takes `&Kit<Unbuilt>` (same memory layout as `&Kit<Ready>`)
132/// because during the build phase we only have the unbuilt Kit.
133pub(crate) type BuildFn = Box<
134    dyn FnOnce(
135        &crate::kit::Kit,
136    ) -> Result<Box<dyn std::any::Any>, Box<dyn std::error::Error + Send + 'static>>,
137>;
138
139#[cfg(all(test, feature = "async"))]
140mod async_tests {
141    use super::*;
142    use crate::kit::AsyncKit;
143    use crate::test_helpers::{MockError, block_on};
144    use std::future::Future;
145    use std::pin::Pin;
146    use std::sync::Arc;
147
148    #[derive(Debug, Clone, PartialEq)]
149    struct LoggerCapability {
150        name: String,
151    }
152
153    struct MockLoggerModule;
154
155    impl ModuleMeta for MockLoggerModule {
156        const NAME: &'static str = "mock-logger";
157        fn dependencies() -> &'static [(&'static str, std::any::TypeId)] {
158            &[]
159        }
160    }
161
162    impl AsyncAutoBuilder for MockLoggerModule {
163        type Capability = Arc<LoggerCapability>;
164        type Error = MockError;
165
166        fn build<'a>(
167            kit: &'a AsyncKit,
168        ) -> Pin<Box<dyn Future<Output = Result<Self::Capability, Self::Error>> + Send + 'a>>
169        {
170            let _ = kit;
171            Box::pin(async move {
172                Ok(Arc::new(LoggerCapability {
173                    name: "mock".to_string(),
174                }))
175            })
176        }
177    }
178
179    #[test]
180    fn async_auto_builder_returns_pin_box_future() {
181        let kit = AsyncKit::new();
182        let fut = MockLoggerModule::build(&kit);
183        let result = block_on(fut);
184        assert!(result.is_ok());
185        let cap = result.expect("build future returned Ok");
186        assert_eq!(cap.name, "mock");
187    }
188
189    #[test]
190    fn async_auto_builder_capability_is_send_sync() {
191        fn assert_send_sync<T: Send + Sync>() {}
192        assert_send_sync::<LoggerCapability>();
193        assert_send_sync::<Arc<LoggerCapability>>();
194    }
195
196    #[test]
197    fn async_auto_builder_error_is_send_static() {
198        fn assert_send_static<T: Send + 'static>() {}
199        assert_send_static::<MockError>();
200    }
201}
202
203#[cfg(all(test, feature = "interface"))]
204mod interface_tests {
205    use super::*;
206
207    #[test]
208    fn interface_auto_implemented_for_primitive_types() {
209        fn assert_interface<T: Interface>() {}
210        assert_interface::<i32>();
211        assert_interface::<u64>();
212        assert_interface::<String>();
213        assert_interface::<Vec<u8>>();
214        assert_interface::<bool>();
215    }
216
217    #[test]
218    fn interface_auto_implemented_for_custom_types() {
219        struct MyType;
220        #[allow(dead_code)]
221        enum MyEnum {
222            A,
223            B,
224        }
225
226        fn assert_interface<T: Interface>() {}
227        assert_interface::<MyType>();
228        assert_interface::<MyEnum>();
229    }
230
231    #[test]
232    fn interface_auto_implemented_for_reference_types() {
233        trait MyTrait {}
234        fn assert_interface<T: Interface + ?Sized>() {}
235        assert_interface::<dyn MyTrait>();
236    }
237}
238
239#[cfg(all(test, feature = "interface"))]
240mod interface_builder_tests {
241    use super::*;
242    use crate::kit::Kit;
243    use std::sync::atomic::{AtomicUsize, Ordering};
244    use std::sync::Arc;
245
246    /// Test interface: a simple Logger trait.
247    trait Logger: 'static {
248        fn log(&self, msg: &str);
249    }
250
251    /// Concrete implementation of Logger.
252    struct ConsoleLogger {
253        counter: AtomicUsize,
254    }
255
256    impl Logger for ConsoleLogger {
257        fn log(&self, msg: &str) {
258            let _ = msg;
259            self.counter.fetch_add(1, Ordering::Relaxed);
260        }
261    }
262
263    /// Test error type (test_helpers::MockError is gated on `async` feature).
264    #[derive(Debug)]
265    struct InterfaceTestError;
266
267    impl std::fmt::Display for InterfaceTestError {
268        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269            write!(f, "interface test error")
270        }
271    }
272
273    impl std::error::Error for InterfaceTestError {}
274
275    /// Module that provides a ConsoleLogger behind the dyn Logger interface.
276    struct ConsoleLoggerModule;
277
278    impl ModuleMeta for ConsoleLoggerModule {
279        const NAME: &'static str = "console-logger";
280        fn dependencies() -> &'static [(&'static str, std::any::TypeId)] {
281            &[]
282        }
283    }
284
285    impl InterfaceBuilder for ConsoleLoggerModule {
286        type Interface = dyn Logger;
287        type Capability = Arc<ConsoleLogger>;
288        type Error = InterfaceTestError;
289
290        fn build(_kit: &Kit) -> Result<Arc<ConsoleLogger>, InterfaceTestError> {
291            Ok(Arc::new(ConsoleLogger {
292                counter: AtomicUsize::new(0),
293            }))
294        }
295
296        fn into_interface(cap: Arc<ConsoleLogger>) -> Arc<dyn Logger> {
297            cap
298        }
299    }
300
301    #[test]
302    fn interface_builder_build_returns_concrete_capability() {
303        let kit = Kit::new();
304        let cap = ConsoleLoggerModule::build(&kit).expect("build succeeds");
305        assert_eq!(Arc::strong_count(&cap), 1);
306    }
307
308    #[test]
309    fn interface_builder_into_interface_produces_trait_object() {
310        let kit = Kit::new();
311        let cap = ConsoleLoggerModule::build(&kit).expect("build succeeds");
312        let iface: Arc<dyn Logger> = ConsoleLoggerModule::into_interface(cap);
313        iface.log("hello");
314        iface.log("world");
315    }
316
317    #[test]
318    fn interface_builder_interface_type_is_dyn_compatible() {
319        fn assert_dyn_compatible<T: ?Sized + 'static>() {}
320        assert_dyn_compatible::<dyn Logger>();
321    }
322
323    #[test]
324    fn interface_builder_capability_is_clone() {
325        let cap = Arc::new(ConsoleLogger {
326            counter: AtomicUsize::new(0),
327        });
328        let cloned = cap.clone();
329        assert_eq!(Arc::strong_count(&cloned), 2);
330        drop(cap);
331        assert_eq!(Arc::strong_count(&cloned), 1);
332    }
333
334    #[test]
335    fn interface_builder_does_not_require_autobuilder() {
336        // InterfaceBuilder is an independent trait — a module can implement
337        // InterfaceBuilder without implementing AutoBuilder. Verify
338        // ConsoleLoggerModule does NOT impl AutoBuilder by checking that
339        // calling AutoBuilder::build would not compile (negative verification
340        // via trait bound assertion).
341        fn requires_interface_builder<T: InterfaceBuilder>() {}
342        requires_interface_builder::<ConsoleLoggerModule>();
343    }
344}