Skip to main content

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}