Skip to main content

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