Skip to main content

injectable_rs_runtime/
factory_ctx.rs

1//! Scope-safe context for factory closures and `DynProvider::with_ctx`.
2//!
3//! [`FactoryCtx`] is passed to `DynProvider::with_ctx` closures instead of
4//! the raw `Arc<ResolveContext>`.  It exposes only operations that respect
5//! singleton / transient scope semantics.
6
7use std::sync::Arc;
8
9use crate::{Extract, InjectableResult, ResolveContext};
10
11/// Scope-safe resolution context for factory closures.
12///
13/// Passed to [`DynProvider::with_ctx`](crate::DynProvider::with_ctx) closures
14/// instead of the raw `Arc<ResolveContext>`.  Only exposes operations that go
15/// through the full `Extract` machinery and therefore respect singleton /
16/// transient scope.
17///
18/// # What is intentionally absent
19///
20/// `FactoryCtx` does **not** expose:
21///
22/// - `resolve::<T>()` — calls the provider directly, bypassing the singleton
23///   cache and creating a fresh instance on every call regardless of scope.
24/// - `resolve_singleton_arc::<T>()` — accesses the raw singleton cache, which
25///   would allow users to pull a singleton `Arc` for a transient type.
26///
27/// Use [`extract`](FactoryCtx::extract) to resolve any injectable type through
28/// the correct scope-aware path.
29pub struct FactoryCtx(pub(crate) Arc<ResolveContext>);
30
31impl FactoryCtx {
32    /// Create a `FactoryCtx` from an `Arc<ResolveContext>`.
33    ///
34    /// Called by `DynProvider::with_ctx` before invoking the user closure.
35    pub(crate) fn new(ctx: Arc<ResolveContext>) -> Self {
36        Self(ctx)
37    }
38
39    /// Extract any type that implements [`Extract`].
40    ///
41    /// This is the scope-safe extraction path — identical to what the
42    /// `#[injectable(inject)]` annotation generates for struct fields and constructor
43    /// parameters.  Singleton types return the cached `Arc`; transient types
44    /// get a fresh instance.
45    ///
46    /// # Example
47    ///
48    /// ```rust,ignore
49    /// DynProvider::with_ctx(|ctx| async move {
50    ///     let config: Inject<AppConfig> = ctx.extract().await?;
51    ///     Ok(Database::connect(&config.db_url).await?)
52    /// })
53    /// ```
54    pub async fn extract<T>(&self) -> InjectableResult<T>
55    where
56        T: Extract + Send + Sync + 'static,
57    {
58        T::extract(&self.0).await
59    }
60
61    /// Resolve a type registered via [`DynProvider`](crate::DynProvider).
62    ///
63    /// Use this when you need a value that was registered with
64    /// `ContainerBuilder::register(DynProvider::…)` rather than via
65    /// `#[injectable]`.
66    ///
67    /// # Example
68    ///
69    /// ```rust,ignore
70    /// DynProvider::with_ctx(|ctx| async move {
71    ///     let pool: sqlx::SqlitePool = ctx.resolve_external().await?;
72    ///     Ok(MyRepo::new(pool))
73    /// })
74    /// ```
75    pub async fn resolve_external<T>(&self) -> InjectableResult<T>
76    where
77        T: Send + Sync + 'static,
78    {
79        self.0.resolve_external::<T>().await
80    }
81
82    /// Resolve a type registered via a named [`DynProvider`](crate::DynProvider) token.
83    ///
84    /// Use this when multiple providers of the same type are registered under
85    /// different tokens.
86    ///
87    /// # Example
88    ///
89    /// ```rust,ignore
90    /// DynProvider::with_ctx(|ctx| async move {
91    ///     let primary: Pool = ctx.resolve_external_with_token("primary").await?;
92    ///     let replica:  Pool = ctx.resolve_external_with_token("replica").await?;
93    ///     Ok(Router::new(primary, replica))
94    /// })
95    /// ```
96    pub async fn resolve_external_with_token<T>(&self, token: &str) -> InjectableResult<T>
97    where
98        T: Send + Sync + 'static,
99    {
100        self.0.resolve_external_with_token::<T>(token).await
101    }
102}
103
104// ─── Unit tests ──────────────────────────────────────────────────────────────
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::{
110        DynProvider, EmptySingletonStore, Injectable, InjectableError, InjectableResult, Provider,
111        ProviderRegistry,
112    };
113    use std::sync::Arc;
114
115    // ── Minimal injectable leaf type for testing ─────────────────────────────
116
117    #[derive(Debug, Default, Clone)]
118    struct Leaf;
119
120    struct LeafProvider;
121
122    #[async_trait::async_trait]
123    impl Provider<Leaf> for LeafProvider {
124        async fn provide(_ctx: &ResolveContext) -> InjectableResult<Leaf> {
125            Ok(Leaf)
126        }
127    }
128
129    impl Injectable for Leaf {
130        type Provider = LeafProvider;
131        const IS_SINGLETON: bool = true;
132    }
133
134    fn make_ctx() -> Arc<ResolveContext> {
135        Arc::new(ResolveContext::new(
136            Arc::new(EmptySingletonStore),
137            Arc::new(ProviderRegistry::new()),
138        ))
139    }
140
141    // ── extract<Arc<T>> respects the singleton cache ────────────────────────
142    // Note: Inject<T> extraction requires InjectableArcFactory entries in
143    // inventory (submitted by the #[injectable] macro). In runtime unit tests
144    // we use Arc<T> directly, which goes through the pub(crate)
145    // resolve_singleton_arc path without needing the macro.
146
147    #[tokio::test]
148    async fn extract_arc_t_returns_singleton() {
149        let ctx = make_ctx();
150        let fctx = FactoryCtx::new(Arc::clone(&ctx));
151
152        let a: Arc<Leaf> = fctx.extract().await.expect("first extraction");
153        let b: Arc<Leaf> = fctx.extract().await.expect("second extraction");
154
155        assert!(
156            Arc::ptr_eq(&a, &b),
157            "FactoryCtx::extract::<Arc<T>> should return the cached singleton Arc"
158        );
159    }
160
161    // ── Two FactoryCtx instances from the same Arc share the singleton ───────
162
163    #[tokio::test]
164    async fn two_factory_ctx_share_singleton() {
165        let ctx = make_ctx();
166        let fctx1 = FactoryCtx::new(Arc::clone(&ctx));
167        let fctx2 = FactoryCtx::new(Arc::clone(&ctx));
168
169        let a: Arc<Leaf> = fctx1.extract().await.expect("first ctx");
170        let b: Arc<Leaf> = fctx2.extract().await.expect("second ctx");
171
172        assert!(
173            Arc::ptr_eq(&a, &b),
174            "both FactoryCtx instances must return the same singleton (same underlying context)"
175        );
176    }
177
178    // ── resolve_external returns a registered DynProvider value ─────────────
179
180    #[tokio::test]
181    async fn resolve_external_returns_registered_value() {
182        let mut registry = ProviderRegistry::new();
183        registry.register("", DynProvider::from_value(42u32));
184
185        let ctx = Arc::new(ResolveContext::new(
186            Arc::new(EmptySingletonStore),
187            Arc::new(registry),
188        ));
189        let fctx = FactoryCtx::new(ctx);
190
191        let val: u32 = fctx.resolve_external().await.expect("registered u32");
192        assert_eq!(val, 42);
193    }
194
195    // ── resolve_external returns MissingDependency for unregistered types ────
196
197    #[tokio::test]
198    async fn resolve_external_missing_returns_error() {
199        let fctx = FactoryCtx::new(make_ctx());
200        let result: InjectableResult<String> = fctx.resolve_external().await;
201
202        assert!(
203            matches!(result, Err(InjectableError::MissingDependency { .. })),
204            "unregistered type should yield MissingDependency, got: {:?}",
205            result.unwrap_err()
206        );
207    }
208}