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