rust_dix/lib.rs
1#![forbid(unsafe_code)]
2
3//! rust-dix — Rust Dependency Injection Framework
4//!
5//! A dependency injection container for Rust inspired by
6//! Microsoft.Extensions.DependencyInjection (MEDI).
7//!
8//! # Quick start
9//!
10//! ```rust
11//! use rust_dix::*;
12//! use std::sync::Arc;
13//!
14//! // Trait-based interface (recommended)
15//! trait ILogger: Send + Sync {
16//! fn log(&self, msg: &str);
17//! }
18//!
19//! struct ConsoleLogger;
20//! impl ILogger for ConsoleLogger {
21//! fn log(&self, msg: &str) { println!("[LOG] {}", msg); }
22//! }
23//!
24//! // Register as trait interface
25//! let provider = ServiceCollection::new()
26//! .singleton::<dyn ILogger>(|_| Arc::new(ConsoleLogger))
27//! .build()
28//! .unwrap();
29//!
30//! // Resolve as trait
31//! let logger: Arc<dyn ILogger> = provider.get().unwrap();
32//! logger.log("Hello");
33//! ```
34//!
35//! # Keyed services: typed keys and `AnyKey` fallbacks
36//!
37//! Keys can be any `Any + Send + Sync + Eq + Hash + Debug` value
38//! ([`ServiceKey::new`]); string keys (`"fast"`) remain supported and are stored
39//! as `String` keys. Key equality includes the value type, so
40//! `ServiceKey::new(1u32)` is **not** equal to `"1"`; use
41//! `ServiceKey::from("name")` / `"name".into()` for string-keyed registrations.
42//!
43//! A registration with [`KeyedService::any_key`] (or
44//! [`ServiceKey::any_key`]) acts as a fallback for keys that have no exact
45//! registration. Singleton/scoped fallback instances are cached per
46//! **(registration, requested key)**; `get_all_keyed` never enumerates them.
47//!
48//! ```rust
49//! use rust_dix::*;
50//! use std::sync::Arc;
51//!
52//! #[derive(Debug, PartialEq, Eq, Hash)]
53//! enum Kind { Fast, Safe }
54//!
55//! trait Strategy: Send + Sync { fn name(&self) -> &'static str; }
56//! struct Fast;
57//! impl Strategy for Fast { fn name(&self) -> &'static str { "fast" } }
58//! struct Fallback;
59//! impl Strategy for Fallback { fn name(&self) -> &'static str { "fallback" } }
60//!
61//! let provider = ServiceCollection::new()
62//! .keyed_singleton::<dyn Strategy>(ServiceKey::new(Kind::Fast), |_| Arc::new(Fast))
63//! .keyed_any_singleton::<dyn Strategy>(|_, _| Arc::new(Fallback))
64//! .build()
65//! .unwrap();
66//!
67//! let fast = provider.get_keyed::<dyn Strategy>(ServiceKey::new(Kind::Fast)).unwrap();
68//! assert_eq!(fast.name(), "fast");
69//! // No exact registration for "unknown": the AnyKey fallback is used.
70//! let fallback = provider.get_keyed::<dyn Strategy>("unknown").unwrap();
71//! assert_eq!(fallback.name(), "fallback");
72//! ```
73//!
74//! # Build-time validation
75//!
76//! rust-dix validates dependencies during `build()` to catch errors early:
77//!
78//! ```rust
79//! # use rust_dix::*;
80//! # use std::sync::Arc;
81//! # struct A;
82//! # struct B;
83//! // Circular dependency detection — factories resolve deps via `get_any`
84//! // (the `get::<T>()` method requires `Self: Sized` and cannot be called
85//! // on `&dyn IServiceResolver`).
86//! let result = ServiceCollection::new()
87//! .singleton(|r| {
88//! let _b = r.get_any(std::any::type_name::<B>()); // A depends on B
89//! Arc::new(A)
90//! })
91//! .singleton(|r| {
92//! let _a = r.get_any(std::any::type_name::<A>()); // B depends on A → CIRCULAR!
93//! Arc::new(B)
94//! })
95//! .build();
96//!
97//! assert!(result.is_err());
98//! if let Err(RdiError::CircularDependency(cycle)) = result {
99//! println!("Circular dependency detected: {}", cycle);
100//! }
101//! ```
102//!
103//! # Async services
104//!
105//! For services that require async initialization (e.g., database
106//! connections, remote config loading), use `build_async()` and the `async_*`
107//! registration methods. Async singletons are initialized **dependency-first**
108//! (registration order does not matter), so a factory may `await` another async
109//! singleton registered later. `build_async()` returns `Arc<ServiceProvider>`.
110//!
111//! Async resolution is **chain-based**: each `get_async()` future captures the
112//! current resolution chain when constructed, so self/cross dependency cycles
113//! are reported as [`RdiError::CircularDependency`] instead of dead-locking.
114//! To detect cycles across detached tasks, propagate
115//! [`AsyncFactoryContext`] (see [`ServiceProvider::async_context`]) into the
116//! spawned task; without it, a fully detached task cannot inherit the chain.
117//!
118//! ```rust
119//! use rust_dix::*;
120//! use std::sync::Arc;
121//!
122//! struct DbPool { url: String }
123//!
124//! async fn connect() -> DbPool { DbPool { url: "sqlite::memory:".into() } }
125//!
126//! let provider = futures::executor::block_on(
127//! ServiceCollection::new()
128//! .async_singleton(|_| Box::pin(async { Arc::new(connect().await) }))
129//! .build_async(),
130//! )
131//! .unwrap();
132//!
133//! let pool: Arc<DbPool> = futures::executor::block_on(provider.get_async()).unwrap();
134//! assert_eq!(pool.url, "sqlite::memory:");
135//! ```
136//!
137//! # Features
138//!
139//! - **Three lifetimes**: Singleton, Scoped, Transient
140//! - **Keyed services**: multiple implementations by key
141//! - string keys (`"fast"`) and typed keys ([`ServiceKey::new`], e.g. enums)
142//! - [`KeyedService::any_key`] / [`ServiceKey::any_key`] fallback registrations
143//! (`keyed_any_singleton` / `keyed_any_scoped` / `keyed_any_transient`)
144//! - per-`(registration, requested key)` caching for AnyKey fallbacks; exact
145//! registrations take precedence; `get_all_keyed` excludes fallbacks
146//! - **Constructor injection**: `#[derive(Inject)]` on structs
147//! - **Attribute-based auto-registration**: `#[rust_dix::inject]` on structs or
148//! trait impls (`#[inject(key = "...")]`, `#[inject(owned)]`,
149//! `#[inject(owned, key = "...")]` supported)
150//! - **Compile-time module scanning**: `#[rust_dix::module]` collects
151//! `register!()` declarations
152//! - **Cross-DLL support**: named service registry for cdylib plugins
153//! - **Layered containers**: child-first resolution via
154//! [`ServiceProviderWrapper`]
155//! - **Build-time validation**: sync/async dependency checks (cycles, captive
156//! dependencies, sync singleton depending on async services) during build
157//! - **Async resolution with cycle detection**: [`ServiceProvider::get_async`] /
158//! [`ServiceProvider::get_keyed_async`]; propagate [`AsyncFactoryContext`] into
159//! detached tasks so cross-task cycles also return
160//! [`RdiError::CircularDependency`]
161//! - **Scope validation**: scoped services cannot be resolved from the root
162//! provider by default (MEDI `ValidateScopes` parity; opt out with
163//! [`ServiceProviderOptions::lenient`]); `get_owned` /
164//! `get_keyed_owned` are the documented exception
165//! - **Deterministic lifecycle**: eager singletons are created in registration
166//! order (async singletons dependency-first); [`ServiceProvider::dispose`] /
167//! [`Scope::dispose`] release cached instances in reverse creation order
168//! - **MEDI registration semantics**: the last registration wins for a single
169//! service; `get_all` returns unkeyed implementations in registration order
170//! (`get_all_keyed` for keyed ones)
171//! - **Typed diagnostics**: [`RdiError`] exposes dedicated variants such as
172//! `SyncSingletonDependsOnAsyncService` and `AsyncServiceRequiresGetAsync`,
173//! plus [`RdiError::is_circular`] (heuristic for factory-panic-wrapped cycles)
174//! - **Engineering baseline**: MSRV **1.95**, `#![forbid(unsafe_code)]`,
175//! fmt/clippy/doc `-D warnings` + tests + RustSec `cargo audit` in CI
176//!
177//! # Diagnostics
178//!
179//! Prefer matching the typed [`RdiError`] variants over strings:
180//!
181//! ```rust
182//! use rust_dix::*;
183//! use std::sync::Arc;
184//!
185//! struct A;
186//! struct B;
187//! let result = ServiceCollection::new()
188//! .singleton(|r| {
189//! let _ = r.get_any(std::any::type_name::<B>());
190//! Arc::new(A)
191//! })
192//! .singleton(|r| {
193//! let _ = r.get_any(std::any::type_name::<A>());
194//! Arc::new(B)
195//! })
196//! .build();
197//!
198//! let err = result.err().expect("cycle must be rejected");
199//! assert!(err.is_circular());
200//! if let RdiError::CircularDependency(cycle) = err {
201//! assert!(cycle.contains("->"));
202//! }
203//! ```
204//!
205//! Sync resolution of an async service returns
206//! `RdiError::AsyncServiceRequiresGetAsync` (use `get_async` / `get_keyed_async`);
207//! registering `async_*` services and calling [`ServiceCollection::build`]
208//! returns `RdiError::AsyncServiceRequiresBuildAsync`.
209//!
210//! # Crate layout
211//!
212//! | Module | Description |
213//! |--------|-------------|
214//! | [`ServiceCollection`] | Register services with a builder API |
215//! | [`ServiceProvider`] | The root DI container |
216//! | [`Scope`] / [`ServiceScope`] | Scoped container (one per scope) |
217//! | [`IServiceResolver`] | Resolution trait (implemented by `ServiceProvider` & `Scope`) |
218//! | [`ServiceProviderWrapper`] | Child-first layered container |
219//! | [`ServiceLifetime`] | Enum: Singleton, Scoped, Transient |
220//! | [`ServiceKey`] / [`KeyedService`] | Typed keys and the `AnyKey` sentinel |
221//! | [`AsyncFactoryContext`] | Propagate the async resolution chain into tasks |
222//! | [`ServiceProviderOptions`] | Scope-validation options (`lenient()`) |
223//! | [`IProvider`] | Unified trait for resolution + named registry |
224//! | [`RdiError`] | Typed errors (cycles, disposed, async diagnostics, ...) |
225//!
226//! # MSRV and security
227//!
228//! - MSRV: **Rust 1.95** (`rust-version` in `Cargo.toml`, verified in CI).
229//! - `#![forbid(unsafe_code)]` in both `rust-dix` and `rust-dix-macros`.
230//! - No network/filesystem access; containers only manage in-memory services.
231//! - Release/security policy: <https://github.com/Rust-Framework/rust-dix/blob/main/SECURITY.md>
232//!
233//! # Proc-macros
234//!
235//! - `#[derive(Inject)]` — auto-generates constructor injection code
236//! - `#[rust_dix::inject]` — auto-registration on structs or trait impls
237//! - `#[rust_dix::module]` — compile-time module scanning
238//! - `rust_dix::register!(...)` — declare services inside `#[rust_dix::module]`
239
240pub mod bridge;
241mod cache;
242pub mod collection;
243pub mod entry;
244pub mod error;
245pub mod key;
246#[doc(hidden)]
247pub mod macros_support;
248pub mod provider;
249pub mod registration;
250pub mod scope;
251mod validation;
252pub mod wrapper;
253
254// ── Core types (MEDI-inspired naming) ──
255
256/// Service collection — register services, then call `.build()`.
257///
258/// Analogous to `IServiceCollection` in Microsoft.Extensions.DependencyInjection.
259pub use collection::ServiceCollection;
260
261/// Built service provider — the root DI container (read-only after build).
262///
263/// 兼具 MEDI 中 `IServiceProvider` 与 root scope 双重身份:
264/// - 作为 `IServiceProvider`:提供类型/键控/命名解析入口。
265/// - 作为 root scope:**默认启用严格 scope 校验**(对齐 MEDI/ASP.NET Core 开发环境的
266/// `ValidateScopes`),从根解析 Scoped 服务返回 [`RdiError::ScopedFromRootProvider`];
267/// 在 [`ServiceProvider::scope`] 创建的 Scope 内解析则正常缓存复用。若确实需要
268/// 0.7 的"根 scope 内单例化"行为,可用
269/// [`ServiceProviderOptions::lenient`] + [`ServiceCollection::build_with`]。
270/// - 与 MEDI 对齐,容器自身可作为内建服务解析:`provider.get::<ServiceProvider>()`
271/// 与 `#[inject] Arc<ServiceProvider>`(或专用 `#[inject(provider)]`)均可用,
272/// eager singleton 与懒解析服务一致。
273/// - [`ServiceProvider::dispose`] 按创建逆序释放 singleton / root-scoped 实例并
274/// 标记容器不可用(后续解析返回 [`RdiError::ProviderDisposed`])。
275/// - ⚠️ singleton 持有 `Arc<ServiceProvider>` 会形成 `容器 → singleton → 容器` 的
276/// 强引用环,容器不再可能被回收;需要按任务创建 scope 的 singleton 应优先注入
277/// [`ScopeFactory`](内置实现为弱引用,无环)。构造函数内也不要通过注入的
278/// provider 解析服务(会被拒绝),请保存句柄并在方法中延迟解析。
279///
280/// Analogous to `IServiceProvider` in MEDI.
281pub use provider::ServiceProvider;
282
283/// Key used by keyed registrations, including the `AnyKey` fallback sentinel.
284pub use key::{KeyedService, ServiceKey};
285
286/// Handle for propagating the async resolution chain into detached tasks
287/// (enables cross-task dependency cycle detection).
288pub use provider::AsyncFactoryContext;
289
290/// Options for [`ServiceCollection::build_with`] /
291/// [`ServiceCollection::build_async_with`].
292///
293/// Defaults match MEDI/ASP.NET Core development-time scope validation:
294/// `validate_scopes = true`. Use [`ServiceProviderOptions::lenient`] to allow
295/// resolving scoped services from the root provider (pre-0.8 behavior).
296pub use provider::ServiceProviderOptions;
297
298/// Service descriptor containing registration metadata.
299pub use entry::ServiceDescriptor;
300
301/// Service lifetime enum: [`Singleton`](ServiceLifetime::Singleton),
302/// [`Scoped`](ServiceLifetime::Scoped), [`Transient`](ServiceLifetime::Transient).
303pub use entry::ServiceLifetime;
304
305/// Core resolution trait. Both [`ServiceProvider`] and [`Scope`] implement this.
306pub use entry::IServiceResolver;
307
308/// A scoped service provider — created via [`ServiceProvider::scope`].
309///
310/// Analogous to `IServiceScope` in MEDI.
311pub use scope::Scope;
312
313/// Alias for [`Scope`] with MEDI-inspired naming (`IServiceScope`).
314pub use scope::Scope as ServiceScope;
315
316/// Factory trait for creating independent scopes.
317///
318/// The built provider always offers an implementation (no manual registration
319/// required): `provider.get::<dyn ScopeFactory>()` and
320/// `#[inject] Arc<dyn ScopeFactory>` fields both work, including inside
321/// eagerly built singletons (e.g. background workers that need one scope per
322/// tick). Explicit `singleton::<dyn ScopeFactory>(...)` registrations take
323/// precedence.
324///
325/// The built-in implementation holds a `Weak` reference to the container, so a
326/// singleton may store it without creating an `Arc` cycle; the container must
327/// therefore outlive services created from it.
328pub use scope::ScopeFactory;
329
330/// Internal types used by the container.
331pub use entry::{ServiceEntry, ServiceFactory};
332
333/// Wrapper combining child + root provider with child-first resolution.
334pub use wrapper::ServiceProviderWrapper;
335
336// ── Plugin / cross-DLL support ──
337
338/// Unified trait for service resolution and named registry access.
339pub use bridge::IProvider;
340
341// ── Error types ──
342
343/// Error types returned from service resolution.
344pub use error::{inject_arc, RdiError};
345
346// ── Runtime registration ──
347
348pub use registration::ServiceRegistration;
349
350/// Re-export of `inventory` so that `#[rust_dix::inject]` generated code
351/// can call `rust_dix::inventory::submit!` without requiring downstream crates
352/// to add `inventory` to their own `Cargo.toml`.
353#[doc(hidden)]
354pub use inventory;
355
356// ── Proc-macros ──
357
358// `inject` is the attribute macro (placed on structs or trait impls).
359// `register` is the function-like macro used inside `#[rust_dix::module]`.
360pub use rust_dix_macros::{inject, module, register, Inject};