reinhardt_views/viewsets/injectable.rs
1//! Dependency injection support for ViewSets
2//!
3//! This module provides the `InjectableViewSet` extension trait that enables
4//! dependency injection in ViewSet methods.
5//!
6//! # Usage
7//!
8//! ```ignore
9//! use reinhardt_views::viewsets::{InjectableViewSet, ModelViewSet, ViewSet};
10//! use reinhardt_di::Injectable;
11//! use std::sync::Arc;
12//!
13//! impl MyViewSet {
14//! async fn handle_list(&self, request: Request) -> Result<Response> {
15//! // Resolve dependencies from the request's DI context
16//! let db: Arc<DatabaseConnection> = self.resolve(&request).await?;
17//! let cache: CacheService = self.resolve_uncached(&request).await?;
18//!
19//! // Use the dependencies
20//! let items = db.fetch_all().await?;
21//! Ok(Response::ok().with_json(&items)?)
22//! }
23//! }
24//! ```
25
26use crate::ViewSet;
27use async_trait::async_trait;
28use reinhardt_di::{Injectable, InjectionContext};
29use reinhardt_http::{Request, Result};
30use std::sync::Arc;
31
32/// Extension trait for ViewSets that enables dependency injection
33///
34/// This trait is automatically implemented for all types that implement `ViewSet`.
35/// It provides helper methods to resolve dependencies from the request's DI context.
36///
37/// # Examples
38///
39/// ```ignore
40/// # #[tokio::main]
41/// # async fn main() {
42/// use reinhardt_views::viewsets::{InjectableViewSet, ModelViewSet, ViewSet};
43/// use std::sync::Arc;
44///
45/// struct UserViewSet {
46/// basename: String,
47/// }
48///
49/// impl UserViewSet {
50/// async fn handle_list(&self, request: Request) -> Result<Response> {
51/// // Resolve with caching (default)
52/// let db: Arc<DatabaseConnection> = self.resolve(&request).await?;
53///
54/// // Resolve without caching
55/// let fresh_config: Config = self.resolve_uncached(&request).await?;
56///
57/// // Use dependencies...
58/// Ok(Response::ok())
59/// }
60/// }
61/// # }
62/// ```
63#[async_trait]
64pub trait InjectableViewSet: ViewSet {
65 /// Resolve a dependency from the request's DI context with caching
66 ///
67 /// This method extracts the `InjectionContext` from the request and resolves
68 /// the requested dependency type. The resolved dependency is cached for the
69 /// duration of the request.
70 ///
71 /// # Errors
72 ///
73 /// Returns an error if:
74 /// - The DI context is not set on the request (router misconfiguration)
75 /// - The dependency cannot be resolved (not registered, circular dependency, etc.)
76 ///
77 /// # Examples
78 ///
79 /// ```ignore
80 /// let db: Arc<DatabaseConnection> = self.resolve(&request).await?;
81 /// ```
82 async fn resolve<T>(&self, request: &Request) -> Result<T>
83 where
84 T: Injectable + Clone + Send + Sync + 'static,
85 {
86 let di_ctx = request
87 .get_di_context::<Arc<InjectionContext>>()
88 .ok_or_else(|| {
89 reinhardt_core::exception::Error::Internal(
90 "DI context not set. Ensure the router is configured with .with_di_context()"
91 .to_string(),
92 )
93 })?;
94
95 match di_ctx.resolve::<T>().await {
96 Ok(injected) => Ok(injected.as_ref().clone()),
97 Err(reinhardt_di::DiError::DependencyNotRegistered { .. }) => {
98 T::inject(&di_ctx).await.map_err(|e| {
99 reinhardt_core::exception::Error::Internal(format!(
100 "Dependency injection failed for {}: {:?}",
101 std::any::type_name::<T>(),
102 e
103 ))
104 })
105 }
106 Err(e) => Err(reinhardt_core::exception::Error::Internal(format!(
107 "Dependency injection failed for {}: {:?}",
108 std::any::type_name::<T>(),
109 e
110 ))),
111 }
112 }
113
114 /// Resolve a dependency from the request's DI context without caching
115 ///
116 /// This method is similar to `resolve()` but creates a fresh instance
117 /// of the dependency each time, bypassing the cache.
118 ///
119 /// Use this when you need:
120 /// - A fresh instance that won't share state with other resolutions
121 /// - To avoid caching for dependencies with mutable state
122 ///
123 /// # Errors
124 ///
125 /// Returns an error if:
126 /// - The DI context is not set on the request (router misconfiguration)
127 /// - The dependency cannot be resolved (not registered, circular dependency, etc.)
128 ///
129 /// # Examples
130 ///
131 /// ```ignore
132 /// let fresh_service: MyService = self.resolve_uncached(&request).await?;
133 /// ```
134 async fn resolve_uncached<T>(&self, request: &Request) -> Result<T>
135 where
136 T: Injectable + Clone + Send + Sync + 'static,
137 {
138 let di_ctx = request
139 .get_di_context::<Arc<InjectionContext>>()
140 .ok_or_else(|| {
141 reinhardt_core::exception::Error::Internal(
142 "DI context not set. Ensure the router is configured with .with_di_context()"
143 .to_string(),
144 )
145 })?;
146
147 T::inject_uncached(&di_ctx).await.map_err(|e| {
148 reinhardt_core::exception::Error::Internal(format!(
149 "Dependency injection failed for {}: {:?}",
150 std::any::type_name::<T>(),
151 e
152 ))
153 })
154 }
155
156 /// Try to resolve a dependency, returning None if DI context is not available
157 ///
158 /// This is useful for optional dependencies or when you want to gracefully
159 /// handle the case where DI is not configured.
160 ///
161 /// # Examples
162 ///
163 /// ```ignore
164 /// if let Some(cache) = self.try_resolve::<CacheService>(&request).await {
165 /// // Use cache
166 /// } else {
167 /// // Fallback without cache
168 /// }
169 /// ```
170 async fn try_resolve<T>(&self, request: &Request) -> Option<T>
171 where
172 T: Injectable + Clone + Send + Sync + 'static,
173 {
174 let di_ctx = request.get_di_context::<Arc<InjectionContext>>()?;
175
176 match di_ctx.resolve::<T>().await {
177 Ok(injected) => Some(injected.as_ref().clone()),
178 Err(reinhardt_di::DiError::DependencyNotRegistered { .. }) => {
179 T::inject(&di_ctx).await.ok()
180 }
181 Err(_) => None,
182 }
183 }
184}
185
186// Blanket implementation for all ViewSet types
187impl<V: ViewSet> InjectableViewSet for V {}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use crate::viewsets::GenericViewSet;
193
194 // Basic compilation test - InjectableViewSet is automatically implemented
195 #[test]
196 fn test_injectable_viewset_trait_is_implemented() {
197 fn assert_injectable<T: InjectableViewSet>() {}
198
199 // GenericViewSet should implement InjectableViewSet
200 assert_injectable::<GenericViewSet<()>>();
201 }
202}