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}