Skip to main content

trait_kit/core/
meta.rs

1// Copyright (c) 2026 Kirky.X
2//
3// Licensed under the MIT License
4// See LICENSE file in the project root for full license information.
5
6//! Core traits for module declaration and dependency management.
7
8#[cfg(feature = "async")]
9use std::future::Future;
10#[cfg(feature = "async")]
11use std::pin::Pin;
12
13/// Metadata trait for module registration.
14pub trait ModuleMeta: 'static {
15    /// The diagnostic name of this module.
16    const NAME: &'static str;
17
18    /// Returns (name, `TypeId`) pairs for modules this module depends on.
19    fn dependencies() -> &'static [(&'static str, std::any::TypeId)];
20}
21
22/// Builder trait for module construction.
23///
24/// Implemented by the user for each module.
25pub trait AutoBuilder: ModuleMeta {
26    /// The capability type this module provides. Must be Clone.
27    type Capability: Clone + 'static;
28
29    /// The error type returned on build failure.
30    type Error: std::error::Error + Send + 'static;
31
32    /// Build the module's capability using the provided Kit.
33    ///
34    /// # Errors
35    ///
36    /// Returns `Self::Error` if the module fails to build.
37    fn build(kit: &crate::kit::Kit) -> Result<Self::Capability, Self::Error>;
38}
39
40/// Async builder trait for module construction in async context.
41///
42/// Async counterpart of [`AutoBuilder`]. Implement this for modules requiring
43/// async initialization (database pools, HTTP clients, cache backends).
44///
45/// The `build` method returns a `Pin<Box<dyn Future + Send>>` rather than using
46/// native `async fn` in trait so that the trait can be type-erased through the
47/// `AsyncBuildFn` stored in `AsyncKit`'s dependency graph (Phase 1b). Rust
48/// 1.91 supports `async fn` in trait (stable since 1.75), but `dyn`-compatible
49/// dispatch still requires the explicit `Pin<Box>` indirection.
50///
51/// Compared to [`AutoBuilder`], the associated types tighten bounds:
52/// - `Capability: Clone + Send + Sync + 'static` (cross-thread sharing).
53/// - `Error: std::error::Error + Send + 'static` (cross-thread error propagation).
54///
55/// Requires the `async` feature on the crate.
56#[cfg(feature = "async")]
57pub trait AsyncAutoBuilder: ModuleMeta {
58    /// The capability type this module provides. Must be `Clone + Send + Sync`.
59    type Capability: Clone + Send + Sync + 'static;
60
61    /// The error type returned on build failure. Must be `Send + 'static`.
62    type Error: std::error::Error + Send + 'static;
63
64    /// Build the module's capability using the provided `AsyncKit`.
65    ///
66    /// The returned future borrows the kit for lifetime `'a`, allowing
67    /// the build callback to read configs / require dependencies from the kit
68    /// during async construction.
69    ///
70    /// # Errors
71    ///
72    /// Returns `Self::Error` if the module fails to build.
73    #[allow(
74        clippy::type_complexity,
75        reason = "Pin<Box<dyn Future + Send>> is the canonical dyn-compatible async trait dispatch type"
76    )]
77    fn build<'a>(
78        kit: &'a crate::kit::AsyncKit,
79    ) -> Pin<Box<dyn Future<Output = Result<Self::Capability, Self::Error>> + Send + 'a>>;
80}
81
82/// Type-erased build function stored in the dependency graph.
83///
84/// Takes `&Kit<Unbuilt>` (same memory layout as `&Kit<Ready>`)
85/// because during the build phase we only have the unbuilt Kit.
86pub(crate) type BuildFn = Box<
87    dyn FnOnce(
88        &crate::kit::Kit,
89    ) -> Result<Box<dyn std::any::Any>, Box<dyn std::error::Error + Send + 'static>>,
90>;
91
92#[cfg(all(test, feature = "async"))]
93mod async_tests {
94    use super::*;
95    use crate::kit::async_kit::AsyncKit;
96    use crate::test_helpers::{block_on, MockError};
97    use std::future::Future;
98    use std::pin::Pin;
99    use std::sync::Arc;
100
101    #[derive(Debug, Clone, PartialEq)]
102    struct LoggerCapability {
103        name: String,
104    }
105
106    struct MockLoggerModule;
107
108    impl ModuleMeta for MockLoggerModule {
109        const NAME: &'static str = "mock-logger";
110        fn dependencies() -> &'static [(&'static str, std::any::TypeId)] {
111            &[]
112        }
113    }
114
115    impl AsyncAutoBuilder for MockLoggerModule {
116        type Capability = Arc<LoggerCapability>;
117        type Error = MockError;
118
119        fn build<'a>(
120            kit: &'a AsyncKit,
121        ) -> Pin<Box<dyn Future<Output = Result<Self::Capability, Self::Error>> + Send + 'a>>
122        {
123            let _ = kit;
124            Box::pin(async move {
125                Ok(Arc::new(LoggerCapability {
126                    name: "mock".to_string(),
127                }))
128            })
129        }
130    }
131
132    #[test]
133    fn async_auto_builder_returns_pin_box_future() {
134        let kit = AsyncKit::new();
135        let fut = MockLoggerModule::build(&kit);
136        let result = block_on(fut);
137        assert!(result.is_ok());
138        let cap = result.expect("build future returned Ok");
139        assert_eq!(cap.name, "mock");
140    }
141
142    #[test]
143    fn async_auto_builder_capability_is_send_sync() {
144        fn assert_send_sync<T: Send + Sync>() {}
145        assert_send_sync::<LoggerCapability>();
146        assert_send_sync::<Arc<LoggerCapability>>();
147    }
148
149    #[test]
150    fn async_auto_builder_error_is_send_static() {
151        fn assert_send_static<T: Send + 'static>() {}
152        assert_send_static::<MockError>();
153    }
154}