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