injectable_rs/container.rs
1//! Container and ContainerBuilder — the main entry points for DI resolution.
2
3use std::sync::Arc;
4
5use injectable_rs_runtime::{
6 DynProvider, EmptySingletonStore, Injectable, InjectableError, InjectableResult, Provider,
7 ProviderRegistry, ResolveContext, SingletonStore,
8};
9
10/// The dependency injection container.
11///
12/// The container holds the typed singleton store, the provider registry,
13/// and registered destructors for `#[injectable(pre_destruct)]` hooks. It is
14/// constructed via [`Container::builder()`].
15///
16/// # Resolution Strategy
17///
18/// - Types implementing `Injectable` are resolved via static providers
19/// - Types registered via `ContainerBuilder::register()` are resolved
20/// via the dynamic provider registry
21/// - All other types return `MissingDependency` errors
22///
23/// # Lifecycle
24///
25/// - Use [`Container::resolve`] to obtain instances
26/// - Use [`Container::shutdown`] to run `#[injectable(pre_destruct)]` hooks
27/// in reverse construction order
28///
29/// # Example
30///
31/// ```rust,ignore
32/// // Types you own: derive Injectable
33/// #[injectable]
34/// #[derive(Default)]
35/// pub struct UserService { ... }
36///
37/// // Types you don't own: register a provider
38/// let container = Container::builder()
39/// .register(DynProvider::new(|| {
40/// Ok(reqwest::Client::new())
41/// }))
42/// .build()
43/// .await?;
44///
45/// let service = container.resolve::<UserService>().await?;
46/// let client = container.resolve_external::<reqwest::Client>().await?;
47///
48/// // On shutdown, call pre_destruct hooks
49/// container.shutdown().await;
50/// ```
51#[derive(Debug, Clone)]
52pub struct Container {
53 ctx: ResolveContext,
54}
55
56impl Container {
57 /// Create a new container builder.
58 pub fn builder() -> ContainerBuilder {
59 ContainerBuilder::new()
60 }
61
62 /// Resolve a type that implements `Injectable`.
63 ///
64 /// This is the primary resolution method for types you own that
65 /// use `#[injectable]`.
66 ///
67 /// # Example
68 ///
69 /// ```rust,ignore
70 /// let service = app.resolve::<UserService>().await?;
71 /// ```
72 pub async fn resolve<T: Injectable>(&self) -> InjectableResult<T> {
73 T::Provider::provide(&self.ctx).await
74 }
75
76 /// Resolve an external type from the provider registry.
77 ///
78 /// Use this for types that don't implement `Injectable` but have
79 /// been registered via [`ContainerBuilder::register`].
80 ///
81 /// # Example
82 ///
83 /// ```rust,ignore
84 /// let client = app.resolve_external::<reqwest::Client>().await?;
85 /// ```
86 pub async fn resolve_external<T: Send + Sync + 'static>(&self) -> InjectableResult<T> {
87 self.ctx.resolve_external::<T>().await
88 }
89
90 /// Resolve a named external type by token.
91 ///
92 /// Use this when multiple providers of the same type were registered with
93 /// different tokens via `ContainerBuilder::register("token", DynProvider::…)`.
94 ///
95 /// # Example
96 ///
97 /// ```rust,ignore
98 /// let primary: Pool = container.resolve_external_with_token("primary").await?;
99 /// let replica: Pool = container.resolve_external_with_token("replica").await?;
100 /// ```
101 pub async fn resolve_external_with_token<T: Send + Sync + 'static>(
102 &self,
103 token: &str,
104 ) -> InjectableResult<T> {
105 self.ctx.resolve_external_with_token::<T>(token).await
106 }
107
108 /// Resolve a named external type by token, returning `None` if not registered.
109 pub async fn try_resolve_external_with_token<T: Send + Sync + 'static>(
110 &self,
111 token: &str,
112 ) -> InjectableResult<Option<T>> {
113 match self.resolve_external_with_token::<T>(token).await {
114 Ok(v) => Ok(Some(v)),
115 Err(InjectableError::MissingDependency { .. }) => Ok(None),
116 Err(e) => Err(e),
117 }
118 }
119
120 /// Get a reference to the internal resolve context.
121 ///
122 /// Useful for manual extraction in advanced scenarios.
123 pub fn context(&self) -> &ResolveContext {
124 &self.ctx
125 }
126
127 /// Returns the names of all `#[injectable]` types registered in the container.
128 ///
129 /// This includes every type that was annotated with `#[injectable]` and
130 /// linked into the binary — useful for debugging `MissingDependency` errors
131 /// and asserting DI registration in tests.
132 ///
133 /// # Example
134 ///
135 /// ```rust,ignore
136 /// let container = Container::builder().build().await?;
137 /// assert!(container.registered_types().contains(&"Database"));
138 /// ```
139 pub fn registered_types(&self) -> Vec<&'static str> {
140 injectable_rs_runtime::inventory::iter::<injectable_rs_runtime::InjectableArcFactory>()
141 .map(|f| f.type_name)
142 .collect()
143 }
144
145 /// Resolve a type, returning `None` instead of an error if it is not registered.
146 ///
147 /// Maps `MissingDependency → Ok(None)` and propagates all other errors.
148 pub async fn try_resolve<T: Injectable>(&self) -> InjectableResult<Option<T>> {
149 match self.resolve::<T>().await {
150 Ok(v) => Ok(Some(v)),
151 Err(InjectableError::MissingDependency { .. }) => Ok(None),
152 Err(e) => Err(e),
153 }
154 }
155
156 /// Resolve an external type, returning `None` instead of an error if not registered.
157 pub async fn try_resolve_external<T: Send + Sync + 'static>(
158 &self,
159 ) -> InjectableResult<Option<T>> {
160 match self.resolve_external::<T>().await {
161 Ok(v) => Ok(Some(v)),
162 Err(InjectableError::MissingDependency { .. }) => Ok(None),
163 Err(e) => Err(e),
164 }
165 }
166
167 /// Shut down the container, running all `#[injectable(pre_destruct)]` hooks.
168 ///
169 /// Hooks are called in reverse construction order — the most
170 /// recently constructed instance is destroyed first. This ensures
171 /// that dependencies are not destroyed before the types that
172 /// depend on them.
173 ///
174 /// All destructors are called even if some fail (best-effort cleanup).
175 /// If any hooks fail, returns [`InjectableError::ShutdownFailed`]
176 /// containing all accumulated errors.
177 ///
178 /// # Example
179 ///
180 /// ```rust,ignore
181 /// let container = Container::builder()
182 /// .build()
183 /// .await?;
184 ///
185 /// let service = container.resolve::<Database>().await?;
186 ///
187 /// // On application shutdown:
188 /// container.shutdown().await?;
189 /// ```
190 pub async fn shutdown(&self) -> InjectableResult<()> {
191 match self.ctx.run_destructors().await {
192 Ok(()) => Ok(()),
193 Err(errors) => Err(InjectableError::ShutdownFailed { errors }),
194 }
195 }
196
197 /// Returns the number of registered destructors.
198 ///
199 /// This counts instances that have `#[injectable(has_pre_destruct)]`
200 /// and have been resolved through this container.
201 pub async fn destructor_count(&self) -> usize {
202 self.ctx.destructor_count().await
203 }
204}
205
206/// Builder for constructing a [`Container`].
207///
208/// The builder supports:
209/// - Registering dynamic providers for external types
210/// - Setting a custom singleton store
211/// - Startup validation
212///
213/// # Registering External Types
214///
215/// Use [`register`](ContainerBuilder::register) to provide a closure-based
216/// provider for types you don't control:
217///
218/// ```rust,ignore
219/// let container = Container::builder()
220/// // Simple: no dependencies
221/// .register(DynProvider::new(|| {
222/// Ok(reqwest::Client::new())
223/// }))
224/// // With context: depends on other injectables
225/// .register(DynProvider::with_ctx(|ctx| async move {
226/// let config = ctx.resolve::<AppConfig>().await?;
227/// Ok(sqlx::SqlitePool::connect(&config.db_url).await?)
228/// }))
229/// .build()
230/// .await?;
231/// ```
232///
233/// Then resolve with [`Container::resolve_external`]:
234///
235/// ```rust,ignore
236/// let client: reqwest::Client = container.resolve_external().await?;
237/// ```
238pub struct ContainerBuilder {
239 store: Option<Arc<dyn SingletonStore>>,
240 registry: ProviderRegistry,
241}
242
243impl std::fmt::Debug for ContainerBuilder {
244 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245 f.debug_struct("ContainerBuilder")
246 .field(
247 "store",
248 &self.store.as_ref().map(|_| "Arc<dyn SingletonStore>"),
249 )
250 .field("registry", &self.registry)
251 .finish()
252 }
253}
254
255impl ContainerBuilder {
256 /// Create a new container builder.
257 pub fn new() -> Self {
258 Self {
259 store: None,
260 registry: ProviderRegistry::new(),
261 }
262 }
263
264 /// Set a custom singleton store for the container.
265 ///
266 /// The store is typically auto-generated by the macro. Use this
267 /// method only for custom store implementations.
268 pub fn with_store(mut self, store: Arc<dyn SingletonStore>) -> Self {
269 self.store = Some(store);
270 self
271 }
272
273 /// Register a dynamic provider for an external type under the given `token`.
274 ///
275 /// Every registration is keyed by `(TypeId<T>, token)`, so multiple providers
276 /// of the same type can coexist as long as they carry different tokens. Use
277 /// [`DEFAULT_TOKEN`](injectable_rs_runtime::DEFAULT_TOKEN) (`""`) for the
278 /// canonical, unnamed registration.
279 ///
280 /// # Simple Registration (default token)
281 ///
282 /// ```rust,ignore
283 /// builder.register("", DynProvider::sync(|| Ok(reqwest::Client::new())));
284 /// // later:
285 /// let client = container.resolve_external::<reqwest::Client>().await?;
286 /// ```
287 ///
288 /// # Multiple Providers of the Same Type
289 ///
290 /// ```rust,ignore
291 /// builder
292 /// .register("primary", DynProvider::new(|| async { Ok(primary_pool()) }))
293 /// .register("replica", DynProvider::new(|| async { Ok(replica_pool()) }));
294 /// // later:
295 /// let primary: Pool = container.resolve_external_with_token("primary").await?;
296 /// let replica: Pool = container.resolve_external_with_token("replica").await?;
297 /// ```
298 ///
299 /// # Context-Aware Registration
300 ///
301 /// ```rust,ignore
302 /// builder.register("", DynProvider::with_ctx(|ctx| async move {
303 /// let config = ctx.extract::<Inject<AppConfig>>().await?;
304 /// Ok(Database::connect(&config.db_url).await?)
305 /// }));
306 /// ```
307 pub fn register<T: Send + Sync + 'static>(
308 mut self,
309 token: impl Into<String>,
310 provider: DynProvider<T>,
311 ) -> Self {
312 self.registry.register(token, provider);
313 self
314 }
315
316 /// Register a dynamic provider, silently replacing any existing provider
317 /// for the same `(type, token)` pair.
318 ///
319 /// Use this in tests or layered-config scenarios where you intentionally
320 /// want to override an existing registration.
321 pub fn register_or_replace<T: Send + Sync + 'static>(
322 mut self,
323 token: impl Into<String>,
324 provider: DynProvider<T>,
325 ) -> Self {
326 self.registry.register_or_replace(token, provider);
327 self
328 }
329
330 /// Build the container.
331 ///
332 /// This performs startup validation of the dependency graph (collected
333 /// automatically from all `#[injectable]` and `#[injectable]`
334 /// types via the `inventory` crate) and the singleton store, then
335 /// returns a ready-to-use container.
336 ///
337 /// # Validation
338 ///
339 /// The dependency graph is validated at build time for:
340 /// - Circular dependencies
341 /// - Scope mismatches (singleton depending on transient)
342 /// - Missing dependencies
343 /// - Duplicate registrations
344 ///
345 /// If any validation errors are found, the build fails with
346 /// [`InjectableError::ConstructionFailed`].
347 pub async fn build(self) -> InjectableResult<Container> {
348 let store = self.store.unwrap_or_else(|| Arc::new(EmptySingletonStore));
349
350 // Validate the singleton store
351 if let Err(e) = store.validate() {
352 return Err(InjectableError::ConstructionFailed {
353 type_name: "Container",
354 reason: format!("singleton store validation failed: {e}"),
355 });
356 }
357
358 // Validate the dependency graph collected from inventory.
359 // Every #[injectable] and #[injectable] submits a
360 // GraphNode via inventory::submit!, which is automatically
361 // gathered here at build time.
362 let nodes: Vec<injectable_rs_graph::GraphNode> =
363 inventory::iter::<injectable_rs_graph::GraphNode>()
364 .cloned()
365 .collect();
366
367 if !nodes.is_empty() {
368 let graph = injectable_rs_graph::DependencyGraph::new(nodes);
369 if let Err(errors) = graph.validate() {
370 return Err(InjectableError::GraphValidationFailed {
371 errors: errors.iter().map(|e| e.to_string()).collect(),
372 });
373 }
374 }
375
376 // Surface duplicate DynProvider registrations alongside graph errors.
377 let dups = self.registry.duplicates();
378 if !dups.is_empty() {
379 let errors: Vec<String> = dups
380 .iter()
381 .map(|t| format!("DynProvider registered more than once for type `{t}`"))
382 .collect();
383 return Err(InjectableError::GraphValidationFailed { errors });
384 }
385
386 let ctx = ResolveContext::new(store, Arc::new(self.registry));
387 Ok(Container { ctx })
388 }
389}
390
391impl Default for ContainerBuilder {
392 fn default() -> Self {
393 Self::new()
394 }
395}