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