reinhardt_di/lib.rs
1//! # Reinhardt Dependency Injection
2//!
3//! FastAPI-inspired dependency injection system for Reinhardt.
4//!
5//! ## Features
6//!
7//! - **Type-safe**: Full compile-time type checking
8//! - **Async-first**: Built for async/await
9//! - **Scoped**: Request-scoped and singleton dependencies
10//! - **Composable**: Dependencies can depend on other dependencies
11//! - **Cache**: Automatic caching within request scope
12//! - **Circular Dependency Detection**: Automatic runtime detection with optimized performance
13//!
14//! ## Cargo features
15//!
16//! - `testing` — exposes [`DependencyRegistry::register_override`] and
17//! [`testing::OverrideGuard`] for use by `reinhardt-testkit` and other
18//! test harnesses. Tests using these APIs must run inside
19//! `#[serial(di_registry)]`.
20//!
21//! ## Development Tools (dev-tools feature)
22//!
23//! When the `dev-tools` feature is enabled, additional debugging and profiling tools are available:
24//!
25//! - **Visualization**: Generate dependency graphs in DOT format for Graphviz
26//! - **Profiling**: Track dependency resolution performance and identify bottlenecks
27//! - **Advanced Caching**: LRU and TTL-based caching strategies
28//!
29//! ## Generator Support (generator feature) ✅
30//!
31//! Generator-based dependency resolution for lazy, streaming dependency injection.
32//!
33//! **Note**: Uses `genawaiter` crate as a workaround for unstable native async yield.
34//! Will be migrated to native syntax when Rust stabilizes async generators.
35//!
36//! ```rust,no_run
37//! # #[cfg(feature = "generator")]
38//! # use reinhardt_di::generator::DependencyGenerator;
39//! # #[cfg(feature = "generator")]
40//! # async fn example() {
41//! // let gen = DependencyGenerator::new(|co| async move {
42//! // let db = resolve_database().await;
43//! // co.yield_(db).await;
44//! //
45//! // let cache = resolve_cache().await;
46//! // co.yield_(cache).await;
47//! // });
48//! # }
49//! ```
50//!
51//! ## Example
52//!
53//! ```rust,no_run
54//! # use reinhardt_di::{Depends, Injectable};
55//! # #[tokio::main]
56//! # async fn main() {
57//! // Define a dependency
58//! // struct Database {
59//! // pool: DbPool,
60//! // }
61//! //
62//! // #[async_trait]
63//! // impl Injectable for Database {
64//! // async fn inject(ctx: &InjectionContext) -> Result<Self> {
65//! // Ok(Database {
66//! // pool: get_pool().await?,
67//! // })
68//! // }
69//! // }
70//! //
71//! // Use in endpoint
72//! // #[endpoint(GET "/users")]
73//! // async fn list_users(
74//! // db: Depends<Database>,
75//! // ) -> Result<Vec<User>> {
76//! // db.query("SELECT * FROM users").await
77//! // }
78//! # }
79//! ```
80//!
81//! ## InjectionContext Construction
82//!
83//! InjectionContext is constructed using the builder pattern with a required singleton scope:
84//!
85//! ```rust
86//! use reinhardt_di::{InjectionContext, SingletonScope};
87//! use std::sync::Arc;
88//!
89//! // Create singleton scope
90//! let singleton = Arc::new(SingletonScope::new());
91//!
92//! // Build injection context with singleton scope
93//! let ctx = InjectionContext::builder(singleton).build();
94//! ```
95//!
96//! Optional request and param context can be added:
97//!
98//! ```no_run
99//! use reinhardt_di::{InjectionContext, SingletonScope};
100//! use reinhardt_http::Request;
101//! use std::sync::Arc;
102//!
103//! let singleton = Arc::new(SingletonScope::new());
104//!
105//! // Create a dummy request for demonstration
106//! let request = Request::builder()
107//! .method(hyper::Method::GET)
108//! .uri("/")
109//! .version(hyper::Version::HTTP_11)
110//! .headers(hyper::HeaderMap::new())
111//! .body(bytes::Bytes::new())
112//! .build()
113//! .unwrap();
114//!
115//! let ctx = InjectionContext::builder(singleton)
116//! .with_request(request)
117//! .build();
118//! ```
119//!
120//! ## Resolve Context
121//!
122//! The [`get_di_context`] function provides access to the active
123//! [`InjectionContext`] within `#[injectable_factory]` and `#[injectable]`
124//! function bodies, without requiring `#[inject]`.
125//!
126//! This enables factories to access the DI context for purposes like
127//! passing it to downstream consumers:
128//!
129//! ```rust,ignore
130//! use reinhardt_di::{ContextLevel, Depends, get_di_context};
131//!
132//! #[injectable_factory(scope = "transient")]
133//! async fn make_router(
134//! #[inject] config: Depends<AppConfig>,
135//! ) -> Router {
136//! let di_ctx = get_di_context(ContextLevel::Current);
137//! Router::new().with_di_context(di_ctx)
138//! }
139//! ```
140//!
141//! [`ContextLevel::Root`] returns the application-level context, while
142//! [`ContextLevel::Current`] returns the currently active context
143//! (which may be a request-scoped fork).
144//!
145//! Use [`try_get_di_context`] for a non-panicking variant that returns
146//! `None` when called outside of a DI resolution context.
147//!
148//! ## Circular Dependency Detection
149//!
150//! The DI system automatically detects circular dependencies at runtime using an optimized
151//! thread-local mechanism:
152//!
153//! ```ignore
154//! # use reinhardt_di::{Injectable, InjectionContext, SingletonScope, DiResult};
155//! # use async_trait::async_trait;
156//! # use std::sync::Arc;
157//! #[derive(Clone)]
158//! struct ServiceA {
159//! b: Arc<ServiceB>,
160//! }
161//!
162//! #[derive(Clone)]
163//! struct ServiceB {
164//! a: Arc<ServiceA>, // Circular dependency!
165//! }
166//!
167//! #[async_trait]
168//! impl Injectable for ServiceA {
169//! async fn inject(ctx: &InjectionContext) -> DiResult<Self> {
170//! let b = ctx.resolve::<ServiceB>().await?;
171//! Ok(ServiceA { b })
172//! }
173//! }
174//!
175//! #[async_trait]
176//! impl Injectable for ServiceB {
177//! async fn inject(ctx: &InjectionContext) -> DiResult<Self> {
178//! let a = ctx.resolve::<ServiceA>().await?;
179//! Ok(ServiceB { a })
180//! }
181//! }
182//!
183//! let singleton = Arc::new(SingletonScope::new());
184//! let ctx = InjectionContext::builder(singleton).build();
185//!
186//! // This will return Err with DiError::CircularDependency
187//! let result = ctx.resolve::<ServiceA>().await;
188//! assert!(result.is_err());
189//! ```
190//!
191//! ### Performance Characteristics
192//!
193//! - **Cache Hit**: < 5% overhead (cycle detection completely skipped)
194//! - **Cache Miss**: 10-20% overhead (O(1) detection using HashSet)
195//! - **Deep Chains**: Sampling reduces linear cost (checks every 10th at depth 50+)
196//! - **Thread Safety**: Thread-local storage eliminates lock contention
197//!
198//! ## Development Tools Example
199//!
200//! ```no_run
201//! # #[cfg(feature = "dev-tools")]
202//! # use reinhardt_di::{visualization::DependencyGraph, profiling::DependencyProfiler};
203//! # #[cfg(feature = "dev-tools")]
204//! # fn main() {
205//! // fn visualize_dependencies() {
206//! // let mut graph = DependencyGraph::new();
207//! // graph.add_node("Database", "singleton");
208//! // graph.add_node("UserService", "request");
209//! // graph.add_dependency("UserService", "Database");
210//! //
211//! // println!("{}", graph.to_dot());
212//! // }
213//! //
214//! // fn profile_resolution() {
215//! // let mut profiler = DependencyProfiler::new();
216//! // profiler.start_resolve("Database");
217//! // // ... perform resolution ...
218//! // profiler.end_resolve("Database");
219//! //
220//! // let report = profiler.generate_report();
221//! // println!("{}", report.to_string());
222//! // }
223//! # }
224//! ```
225//!
226//! ## Auth Extractor DI Context Requirements
227//!
228//! The `reinhardt-auth` crate provides injectable auth extractors that depend on
229//! specific DI context configuration. Understanding these requirements is essential
230//! for proper authentication integration.
231//!
232//! ### `AuthUser<U>` (recommended)
233//!
234//! Loads the full user model from the database. Requires:
235//!
236//! - **`DatabaseConnection`** registered as a singleton in `InjectionContext`
237//! - **`AuthState`** present in request extensions (set by authentication middleware)
238//! - Feature `params` enabled on `reinhardt-auth`
239//!
240//! Returns an injection error if any requirement is missing (fail-fast behavior).
241//!
242//! ```ignore
243//! use reinhardt_auth::AuthUser;
244//! use reinhardt_auth::DefaultUser;
245//!
246//! #[get("/profile/")]
247//! pub async fn profile(
248//! #[inject] AuthUser(user): AuthUser<DefaultUser>,
249//! ) -> ViewResult<Response> {
250//! let username = user.get_username();
251//! // ...
252//! }
253//! ```
254//!
255//! ### `AuthInfo` (lightweight alternative)
256//!
257//! Extracts authentication metadata without a database query. Requires:
258//!
259//! - **`AuthState`** present in request extensions (set by authentication middleware)
260//! - No `DatabaseConnection` needed
261//!
262//! ### `CurrentUser<U>` (deprecated)
263//!
264//! Deprecated in favor of `AuthUser<U>`. Unlike `AuthUser<U>`, missing context
265//! causes silent fallback to anonymous instead of returning an error.
266//!
267//! ### Startup Validation
268//!
269//! Call `reinhardt_auth::validate_auth_extractors()` during application startup
270//! to verify that required dependencies (e.g., `DatabaseConnection`) are registered
271//! before the first request arrives.
272
273#![warn(missing_docs)]
274
275pub mod params;
276
277pub mod context;
278pub mod cycle_detection;
279pub mod depends;
280pub mod function_handle;
281pub mod graph;
282pub mod injectable;
283pub mod injected;
284pub mod override_registry;
285pub mod provider;
286pub mod registration;
287pub mod registry;
288pub mod resolve_context;
289pub mod scope;
290#[cfg(feature = "testing")]
291pub mod testing;
292pub mod validation;
293
294use thiserror::Error;
295
296pub use context::{InjectionContext, InjectionContextBuilder, RequestContext};
297pub use cycle_detection::{
298 CycleError, ResolutionGuard, begin_resolution, register_type_name, with_cycle_detection_scope,
299};
300pub use function_handle::FunctionHandle;
301pub use override_registry::OverrideRegistry;
302
303#[cfg(feature = "params")]
304pub use context::{ParamContext, Request};
305pub use depends::{Depends, DependsBuilder};
306pub use injectable::Injectable;
307#[allow(deprecated)]
308pub use injected::{
309 DependencyScope as InjectedScope, Injected, InjectionMetadata, OptionalInjected,
310};
311pub use provider::{Provider, ProviderFn};
312pub use registration::DiRegistrationList;
313pub use registry::{
314 DependencyRegistration, DependencyRegistry, DependencyScope, FactoryTrait, InjectableFactory,
315 InjectableRegistration, global_registry,
316};
317pub use resolve_context::{ContextLevel, get_di_context, try_get_di_context};
318pub use scope::{RequestScope, Scope, SingletonScope};
319#[cfg(feature = "testing")]
320pub use testing::OverrideGuard;
321pub use validation::{RegistryValidator, ValidationError, ValidationErrorKind};
322
323// Re-export inventory and async_trait for macro use
324pub use async_trait;
325pub use inventory;
326
327// Re-export macros
328#[cfg(feature = "macros")]
329pub use reinhardt_di_macros::{injectable, injectable_factory};
330
331/// Errors that can occur during dependency injection resolution.
332#[derive(Debug, Error)]
333#[non_exhaustive]
334pub enum DiError {
335 /// The requested dependency was not found in the container.
336 #[error("Dependency not found: {0}")]
337 NotFound(String),
338
339 /// A circular dependency chain was detected during resolution.
340 #[error("Circular dependency detected: {0}")]
341 CircularDependency(String),
342
343 /// An error occurred in a dependency provider function.
344 #[error("Provider error: {0}")]
345 ProviderError(String),
346
347 /// The resolved type did not match the expected type.
348 #[error("Type mismatch: expected {expected}, got {actual}")]
349 TypeMismatch {
350 /// The type that was expected.
351 expected: String,
352 /// The type that was actually resolved.
353 actual: String,
354 },
355
356 /// An error related to dependency scoping (request vs singleton).
357 #[error("Scope error: {0}")]
358 ScopeError(String),
359
360 /// The requested type was not registered in the dependency registry.
361 #[error("Type '{type_name}' not registered. {hint}")]
362 NotRegistered {
363 /// The name of the unregistered type.
364 type_name: String,
365 /// A hint message suggesting how to register the type.
366 hint: String,
367 },
368
369 /// A required dependency was not registered.
370 #[error("Dependency not registered: {type_name}")]
371 DependencyNotRegistered {
372 /// The name of the unregistered dependency type.
373 type_name: String,
374 },
375
376 /// An internal error in the DI system.
377 #[error("Internal error: {message}")]
378 Internal {
379 /// A description of the internal error.
380 message: String,
381 },
382
383 /// An authorization error (insufficient permissions).
384 #[error("Authorization error: {0}")]
385 Authorization(String),
386
387 /// An authentication error (user not authenticated).
388 #[error("Authentication error: {0}")]
389 Authentication(String),
390}
391
392impl From<DiError> for reinhardt_core::exception::Error {
393 fn from(err: DiError) -> Self {
394 match &err {
395 DiError::NotFound(_)
396 | DiError::NotRegistered { .. }
397 | DiError::DependencyNotRegistered { .. } => reinhardt_core::exception::Error::NotFound(
398 format!("Dependency injection error: {}", err),
399 ),
400 DiError::Authorization(msg) => {
401 reinhardt_core::exception::Error::Authorization(msg.clone())
402 }
403 DiError::Authentication(msg) => {
404 reinhardt_core::exception::Error::Authentication(msg.clone())
405 }
406 _ => reinhardt_core::exception::Error::Internal(format!(
407 "Dependency injection error: {}",
408 err
409 )),
410 }
411 }
412}
413
414/// A specialized `Result` type for dependency injection operations.
415pub type DiResult<T> = std::result::Result<T, DiError>;
416
417// Generator support
418#[cfg(feature = "generator")]
419pub mod generator;
420
421#[cfg(test)]
422mod tests {
423 use rstest::rstest;
424
425 use super::*;
426
427 #[rstest]
428 #[case::not_found(DiError::NotFound("missing".to_string()), 404)]
429 #[case::not_registered(DiError::NotRegistered { type_name: "Foo".to_string(), hint: "".to_string() }, 404)]
430 #[case::dependency_not_registered(DiError::DependencyNotRegistered { type_name: "Bar".to_string() }, 404)]
431 #[case::authorization(DiError::Authorization("forbidden".to_string()), 403)]
432 #[case::authentication(DiError::Authentication("not authenticated".to_string()), 401)]
433 #[case::circular_dependency(DiError::CircularDependency("A -> B -> A".to_string()), 500)]
434 #[case::provider_error(DiError::ProviderError("boom".to_string()), 500)]
435 #[case::type_mismatch(DiError::TypeMismatch { expected: "A".to_string(), actual: "B".to_string() }, 500)]
436 #[case::scope_error(DiError::ScopeError("wrong scope".to_string()), 500)]
437 #[case::internal(DiError::Internal { message: "oops".to_string() }, 500)]
438 fn test_di_error_to_http_error_status_mapping(
439 #[case] di_err: DiError,
440 #[case] expected_status: u16,
441 ) {
442 // Arrange (provided by #[case])
443
444 // Act
445 let err: reinhardt_core::exception::Error = di_err.into();
446
447 // Assert
448 assert_eq!(err.status_code(), expected_status);
449 }
450}
451
452// Development tools
453#[cfg(feature = "dev-tools")]
454pub mod visualization;
455
456#[cfg(feature = "dev-tools")]
457pub mod profiling;
458
459#[cfg(feature = "dev-tools")]
460pub mod advanced_cache;