spikard 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +659 -659
  4. data/ext/spikard_rb/Cargo.toml +17 -17
  5. data/ext/spikard_rb/extconf.rb +10 -10
  6. data/ext/spikard_rb/src/lib.rs +6 -6
  7. data/lib/spikard/app.rb +386 -386
  8. data/lib/spikard/background.rb +27 -27
  9. data/lib/spikard/config.rb +396 -396
  10. data/lib/spikard/converters.rb +13 -13
  11. data/lib/spikard/handler_wrapper.rb +113 -113
  12. data/lib/spikard/provide.rb +214 -214
  13. data/lib/spikard/response.rb +173 -173
  14. data/lib/spikard/schema.rb +243 -243
  15. data/lib/spikard/sse.rb +111 -111
  16. data/lib/spikard/streaming_response.rb +44 -44
  17. data/lib/spikard/testing.rb +221 -221
  18. data/lib/spikard/upload_file.rb +131 -131
  19. data/lib/spikard/version.rb +5 -5
  20. data/lib/spikard/websocket.rb +59 -59
  21. data/lib/spikard.rb +43 -43
  22. data/sig/spikard.rbs +360 -360
  23. data/vendor/crates/spikard-core/Cargo.toml +40 -40
  24. data/vendor/crates/spikard-core/src/bindings/mod.rs +3 -3
  25. data/vendor/crates/spikard-core/src/bindings/response.rs +133 -133
  26. data/vendor/crates/spikard-core/src/debug.rs +63 -63
  27. data/vendor/crates/spikard-core/src/di/container.rs +726 -726
  28. data/vendor/crates/spikard-core/src/di/dependency.rs +273 -273
  29. data/vendor/crates/spikard-core/src/di/error.rs +118 -118
  30. data/vendor/crates/spikard-core/src/di/factory.rs +538 -538
  31. data/vendor/crates/spikard-core/src/di/graph.rs +545 -545
  32. data/vendor/crates/spikard-core/src/di/mod.rs +192 -192
  33. data/vendor/crates/spikard-core/src/di/resolved.rs +411 -411
  34. data/vendor/crates/spikard-core/src/di/value.rs +283 -283
  35. data/vendor/crates/spikard-core/src/errors.rs +39 -39
  36. data/vendor/crates/spikard-core/src/http.rs +153 -153
  37. data/vendor/crates/spikard-core/src/lib.rs +29 -29
  38. data/vendor/crates/spikard-core/src/lifecycle.rs +422 -422
  39. data/vendor/crates/spikard-core/src/parameters.rs +722 -722
  40. data/vendor/crates/spikard-core/src/problem.rs +310 -310
  41. data/vendor/crates/spikard-core/src/request_data.rs +189 -189
  42. data/vendor/crates/spikard-core/src/router.rs +249 -249
  43. data/vendor/crates/spikard-core/src/schema_registry.rs +183 -183
  44. data/vendor/crates/spikard-core/src/type_hints.rs +304 -304
  45. data/vendor/crates/spikard-core/src/validation.rs +699 -699
  46. data/vendor/crates/spikard-http/Cargo.toml +58 -58
  47. data/vendor/crates/spikard-http/src/auth.rs +247 -247
  48. data/vendor/crates/spikard-http/src/background.rs +249 -249
  49. data/vendor/crates/spikard-http/src/bindings/mod.rs +3 -3
  50. data/vendor/crates/spikard-http/src/bindings/response.rs +1 -1
  51. data/vendor/crates/spikard-http/src/body_metadata.rs +8 -8
  52. data/vendor/crates/spikard-http/src/cors.rs +490 -490
  53. data/vendor/crates/spikard-http/src/debug.rs +63 -63
  54. data/vendor/crates/spikard-http/src/di_handler.rs +423 -423
  55. data/vendor/crates/spikard-http/src/handler_response.rs +190 -190
  56. data/vendor/crates/spikard-http/src/handler_trait.rs +228 -228
  57. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +284 -284
  58. data/vendor/crates/spikard-http/src/lib.rs +529 -529
  59. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +149 -149
  60. data/vendor/crates/spikard-http/src/lifecycle.rs +428 -428
  61. data/vendor/crates/spikard-http/src/middleware/mod.rs +285 -285
  62. data/vendor/crates/spikard-http/src/middleware/multipart.rs +86 -86
  63. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +147 -147
  64. data/vendor/crates/spikard-http/src/middleware/validation.rs +287 -287
  65. data/vendor/crates/spikard-http/src/openapi/mod.rs +309 -309
  66. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +190 -190
  67. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +308 -308
  68. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +195 -195
  69. data/vendor/crates/spikard-http/src/parameters.rs +1 -1
  70. data/vendor/crates/spikard-http/src/problem.rs +1 -1
  71. data/vendor/crates/spikard-http/src/query_parser.rs +369 -369
  72. data/vendor/crates/spikard-http/src/response.rs +399 -399
  73. data/vendor/crates/spikard-http/src/router.rs +1 -1
  74. data/vendor/crates/spikard-http/src/schema_registry.rs +1 -1
  75. data/vendor/crates/spikard-http/src/server/handler.rs +87 -87
  76. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +98 -98
  77. data/vendor/crates/spikard-http/src/server/mod.rs +805 -805
  78. data/vendor/crates/spikard-http/src/server/request_extraction.rs +119 -119
  79. data/vendor/crates/spikard-http/src/sse.rs +447 -447
  80. data/vendor/crates/spikard-http/src/testing/form.rs +14 -14
  81. data/vendor/crates/spikard-http/src/testing/multipart.rs +60 -60
  82. data/vendor/crates/spikard-http/src/testing/test_client.rs +285 -285
  83. data/vendor/crates/spikard-http/src/testing.rs +377 -377
  84. data/vendor/crates/spikard-http/src/type_hints.rs +1 -1
  85. data/vendor/crates/spikard-http/src/validation.rs +1 -1
  86. data/vendor/crates/spikard-http/src/websocket.rs +324 -324
  87. data/vendor/crates/spikard-rb/Cargo.toml +42 -42
  88. data/vendor/crates/spikard-rb/build.rs +8 -8
  89. data/vendor/crates/spikard-rb/src/background.rs +63 -63
  90. data/vendor/crates/spikard-rb/src/config.rs +294 -294
  91. data/vendor/crates/spikard-rb/src/conversion.rs +453 -453
  92. data/vendor/crates/spikard-rb/src/di.rs +409 -409
  93. data/vendor/crates/spikard-rb/src/handler.rs +625 -625
  94. data/vendor/crates/spikard-rb/src/lib.rs +2771 -2771
  95. data/vendor/crates/spikard-rb/src/lifecycle.rs +274 -274
  96. data/vendor/crates/spikard-rb/src/server.rs +283 -283
  97. data/vendor/crates/spikard-rb/src/sse.rs +231 -231
  98. data/vendor/crates/spikard-rb/src/test_client.rs +404 -404
  99. data/vendor/crates/spikard-rb/src/test_sse.rs +143 -143
  100. data/vendor/crates/spikard-rb/src/test_websocket.rs +221 -221
  101. data/vendor/crates/spikard-rb/src/websocket.rs +233 -233
  102. data/vendor/spikard-core/Cargo.toml +40 -40
  103. data/vendor/spikard-core/src/bindings/mod.rs +3 -3
  104. data/vendor/spikard-core/src/bindings/response.rs +133 -133
  105. data/vendor/spikard-core/src/debug.rs +63 -63
  106. data/vendor/spikard-core/src/di/container.rs +726 -726
  107. data/vendor/spikard-core/src/di/dependency.rs +273 -273
  108. data/vendor/spikard-core/src/di/error.rs +118 -118
  109. data/vendor/spikard-core/src/di/factory.rs +538 -538
  110. data/vendor/spikard-core/src/di/graph.rs +545 -545
  111. data/vendor/spikard-core/src/di/mod.rs +192 -192
  112. data/vendor/spikard-core/src/di/resolved.rs +411 -411
  113. data/vendor/spikard-core/src/di/value.rs +283 -283
  114. data/vendor/spikard-core/src/http.rs +153 -153
  115. data/vendor/spikard-core/src/lib.rs +28 -28
  116. data/vendor/spikard-core/src/lifecycle.rs +422 -422
  117. data/vendor/spikard-core/src/parameters.rs +719 -719
  118. data/vendor/spikard-core/src/problem.rs +310 -310
  119. data/vendor/spikard-core/src/request_data.rs +189 -189
  120. data/vendor/spikard-core/src/router.rs +249 -249
  121. data/vendor/spikard-core/src/schema_registry.rs +183 -183
  122. data/vendor/spikard-core/src/type_hints.rs +304 -304
  123. data/vendor/spikard-core/src/validation.rs +699 -699
  124. data/vendor/spikard-http/Cargo.toml +58 -58
  125. data/vendor/spikard-http/src/auth.rs +247 -247
  126. data/vendor/spikard-http/src/background.rs +249 -249
  127. data/vendor/spikard-http/src/bindings/mod.rs +3 -3
  128. data/vendor/spikard-http/src/bindings/response.rs +1 -1
  129. data/vendor/spikard-http/src/body_metadata.rs +8 -8
  130. data/vendor/spikard-http/src/cors.rs +490 -490
  131. data/vendor/spikard-http/src/debug.rs +63 -63
  132. data/vendor/spikard-http/src/di_handler.rs +423 -423
  133. data/vendor/spikard-http/src/handler_response.rs +190 -190
  134. data/vendor/spikard-http/src/handler_trait.rs +228 -228
  135. data/vendor/spikard-http/src/handler_trait_tests.rs +284 -284
  136. data/vendor/spikard-http/src/lib.rs +529 -529
  137. data/vendor/spikard-http/src/lifecycle/adapter.rs +149 -149
  138. data/vendor/spikard-http/src/lifecycle.rs +428 -428
  139. data/vendor/spikard-http/src/middleware/mod.rs +285 -285
  140. data/vendor/spikard-http/src/middleware/multipart.rs +86 -86
  141. data/vendor/spikard-http/src/middleware/urlencoded.rs +147 -147
  142. data/vendor/spikard-http/src/middleware/validation.rs +287 -287
  143. data/vendor/spikard-http/src/openapi/mod.rs +309 -309
  144. data/vendor/spikard-http/src/openapi/parameter_extraction.rs +190 -190
  145. data/vendor/spikard-http/src/openapi/schema_conversion.rs +308 -308
  146. data/vendor/spikard-http/src/openapi/spec_generation.rs +195 -195
  147. data/vendor/spikard-http/src/parameters.rs +1 -1
  148. data/vendor/spikard-http/src/problem.rs +1 -1
  149. data/vendor/spikard-http/src/query_parser.rs +369 -369
  150. data/vendor/spikard-http/src/response.rs +399 -399
  151. data/vendor/spikard-http/src/router.rs +1 -1
  152. data/vendor/spikard-http/src/schema_registry.rs +1 -1
  153. data/vendor/spikard-http/src/server/handler.rs +80 -80
  154. data/vendor/spikard-http/src/server/lifecycle_execution.rs +98 -98
  155. data/vendor/spikard-http/src/server/mod.rs +805 -805
  156. data/vendor/spikard-http/src/server/request_extraction.rs +119 -119
  157. data/vendor/spikard-http/src/sse.rs +447 -447
  158. data/vendor/spikard-http/src/testing/form.rs +14 -14
  159. data/vendor/spikard-http/src/testing/multipart.rs +60 -60
  160. data/vendor/spikard-http/src/testing/test_client.rs +285 -285
  161. data/vendor/spikard-http/src/testing.rs +377 -377
  162. data/vendor/spikard-http/src/type_hints.rs +1 -1
  163. data/vendor/spikard-http/src/validation.rs +1 -1
  164. data/vendor/spikard-http/src/websocket.rs +324 -324
  165. data/vendor/spikard-rb/Cargo.toml +42 -42
  166. data/vendor/spikard-rb/build.rs +8 -8
  167. data/vendor/spikard-rb/src/background.rs +63 -63
  168. data/vendor/spikard-rb/src/config.rs +294 -294
  169. data/vendor/spikard-rb/src/conversion.rs +392 -392
  170. data/vendor/spikard-rb/src/di.rs +409 -409
  171. data/vendor/spikard-rb/src/handler.rs +534 -534
  172. data/vendor/spikard-rb/src/lib.rs +2020 -2020
  173. data/vendor/spikard-rb/src/lifecycle.rs +267 -267
  174. data/vendor/spikard-rb/src/server.rs +283 -283
  175. data/vendor/spikard-rb/src/sse.rs +231 -231
  176. data/vendor/spikard-rb/src/test_client.rs +404 -404
  177. data/vendor/spikard-rb/src/test_sse.rs +143 -143
  178. data/vendor/spikard-rb/src/test_websocket.rs +221 -221
  179. data/vendor/spikard-rb/src/websocket.rs +233 -233
  180. metadata +1 -1
@@ -1,423 +1,423 @@
1
- //! Dependency Injection Handler Wrapper
2
- //!
3
- //! This module provides a handler wrapper that integrates the DI system with the HTTP
4
- //! handler pipeline. It follows the same composition pattern as `ValidatingHandler`.
5
- //!
6
- //! # Architecture
7
- //!
8
- //! The `DependencyInjectingHandler` wraps any `Handler` and:
9
- //! 1. Resolves required dependencies in parallel batches before calling the handler
10
- //! 2. Attaches resolved dependencies to `RequestData`
11
- //! 3. Calls the inner handler with the enriched request data
12
- //! 4. Cleans up dependencies after the handler completes (async Drop pattern)
13
- //!
14
- //! # Performance
15
- //!
16
- //! - **Zero overhead when no DI**: If no container is provided, DI is skipped entirely
17
- //! - **Parallel resolution**: Independent dependencies are resolved concurrently
18
- //! - **Efficient caching**: Singleton and per-request caching minimize redundant work
19
- //! - **Composable**: Works seamlessly with `ValidatingHandler` and lifecycle hooks
20
- //!
21
- //! # Examples
22
- //!
23
- //! ```ignore
24
- //! use spikard_http::di_handler::DependencyInjectingHandler;
25
- //! use spikard_core::di::DependencyContainer;
26
- //! use std::sync::Arc;
27
- //!
28
- //! # tokio_test::block_on(async {
29
- //! let container = Arc::new(DependencyContainer::new());
30
- //! let handler = Arc::new(MyHandler::new());
31
- //!
32
- //! let di_handler = DependencyInjectingHandler::new(
33
- //! handler,
34
- //! container,
35
- //! vec!["database".to_string(), "cache".to_string()],
36
- //! );
37
- //! # });
38
- //! ```
39
-
40
- use crate::handler_trait::{Handler, HandlerResult, RequestData};
41
- use axum::body::Body;
42
- use axum::http::{Request, StatusCode};
43
- use spikard_core::di::{DependencyContainer, DependencyError};
44
- use std::future::Future;
45
- use std::pin::Pin;
46
- use std::sync::Arc;
47
- use tracing::{debug, info_span, instrument};
48
-
49
- /// Handler wrapper that resolves dependencies before calling the inner handler
50
- ///
51
- /// This wrapper follows the composition pattern used by `ValidatingHandler`:
52
- /// it wraps an existing handler and enriches the request with resolved dependencies.
53
- ///
54
- /// # Thread Safety
55
- ///
56
- /// This struct is `Send + Sync` and can be safely shared across threads.
57
- /// The container is shared via `Arc`, and all dependencies must be `Send + Sync`.
58
- pub struct DependencyInjectingHandler {
59
- /// The wrapped handler that will receive the enriched request
60
- inner: Arc<dyn Handler>,
61
- /// Shared dependency container for resolution
62
- container: Arc<DependencyContainer>,
63
- /// List of dependency names required by this handler
64
- required_dependencies: Vec<String>,
65
- }
66
-
67
- impl DependencyInjectingHandler {
68
- /// Create a new dependency-injecting handler wrapper
69
- ///
70
- /// # Arguments
71
- ///
72
- /// * `handler` - The handler to wrap
73
- /// * `container` - Shared dependency container
74
- /// * `required_dependencies` - Names of dependencies to resolve for this handler
75
- ///
76
- /// # Examples
77
- ///
78
- /// ```ignore
79
- /// use spikard_http::di_handler::DependencyInjectingHandler;
80
- /// use spikard_core::di::DependencyContainer;
81
- /// use std::sync::Arc;
82
- ///
83
- /// # tokio_test::block_on(async {
84
- /// let container = Arc::new(DependencyContainer::new());
85
- /// let handler = Arc::new(MyHandler::new());
86
- ///
87
- /// let di_handler = DependencyInjectingHandler::new(
88
- /// handler,
89
- /// container,
90
- /// vec!["db".to_string()],
91
- /// );
92
- /// # });
93
- /// ```
94
- pub fn new(
95
- handler: Arc<dyn Handler>,
96
- container: Arc<DependencyContainer>,
97
- required_dependencies: Vec<String>,
98
- ) -> Self {
99
- Self {
100
- inner: handler,
101
- container,
102
- required_dependencies,
103
- }
104
- }
105
-
106
- /// Get the list of required dependencies
107
- pub fn required_dependencies(&self) -> &[String] {
108
- &self.required_dependencies
109
- }
110
- }
111
-
112
- impl Handler for DependencyInjectingHandler {
113
- #[instrument(
114
- skip(self, request, request_data),
115
- fields(
116
- required_deps = %self.required_dependencies.len(),
117
- deps = ?self.required_dependencies
118
- )
119
- )]
120
- fn call(
121
- &self,
122
- request: Request<Body>,
123
- mut request_data: RequestData,
124
- ) -> Pin<Box<dyn Future<Output = HandlerResult> + Send + '_>> {
125
- eprintln!(
126
- "[spikard-di] entering DI handler, required_deps={:?}",
127
- self.required_dependencies
128
- );
129
- let inner = self.inner.clone();
130
- let container = self.container.clone();
131
- let required_dependencies = self.required_dependencies.clone();
132
-
133
- Box::pin(async move {
134
- debug!(
135
- "DI handler invoked for {} deps; container keys: {:?}",
136
- required_dependencies.len(),
137
- container.keys()
138
- );
139
- // Span for dependency resolution timing
140
- let resolution_span = info_span!(
141
- "resolve_dependencies",
142
- count = %required_dependencies.len()
143
- );
144
- let _enter = resolution_span.enter();
145
-
146
- debug!(
147
- "Resolving {} dependencies: {:?}",
148
- required_dependencies.len(),
149
- required_dependencies
150
- );
151
-
152
- let start = std::time::Instant::now();
153
-
154
- // Convert RequestData to spikard_core::RequestData for DI
155
- let core_request_data = spikard_core::RequestData {
156
- path_params: Arc::clone(&request_data.path_params),
157
- query_params: request_data.query_params.clone(),
158
- raw_query_params: Arc::clone(&request_data.raw_query_params),
159
- body: request_data.body.clone(),
160
- raw_body: request_data.raw_body.clone(),
161
- headers: Arc::clone(&request_data.headers),
162
- cookies: Arc::clone(&request_data.cookies),
163
- method: request_data.method.clone(),
164
- path: request_data.path.clone(),
165
- #[cfg(feature = "di")]
166
- dependencies: None,
167
- };
168
-
169
- // Convert Request<Body> to Request<()> for DI (body not needed for resolution)
170
- let (parts, _body) = request.into_parts();
171
- let core_request = Request::from_parts(parts.clone(), ());
172
-
173
- // Restore original request for handler
174
- let request = Request::from_parts(parts, axum::body::Body::default());
175
-
176
- // Resolve dependencies in parallel batches
177
- let resolved = match container
178
- .resolve_for_handler(&required_dependencies, &core_request, &core_request_data)
179
- .await
180
- {
181
- Ok(resolved) => resolved,
182
- Err(e) => {
183
- debug!("DI error: {}", e);
184
-
185
- // Convert DI errors to proper JSON HTTP responses
186
- let (status, json_body) = match e {
187
- DependencyError::NotFound { ref key } => {
188
- let body = serde_json::json!({
189
- "detail": "Required dependency not found",
190
- "errors": [{
191
- "dependency_key": key,
192
- "msg": format!("Dependency '{}' is not registered", key),
193
- "type": "missing_dependency"
194
- }],
195
- "status": 500,
196
- "title": "Dependency Resolution Failed",
197
- "type": "https://spikard.dev/errors/dependency-error"
198
- });
199
- (StatusCode::INTERNAL_SERVER_ERROR, body)
200
- }
201
- DependencyError::CircularDependency { ref cycle } => {
202
- let body = serde_json::json!({
203
- "detail": "Circular dependency detected",
204
- "errors": [{
205
- "cycle": cycle,
206
- "msg": "Circular dependency detected in dependency graph",
207
- "type": "circular_dependency"
208
- }],
209
- "status": 500,
210
- "title": "Dependency Resolution Failed",
211
- "type": "https://spikard.dev/errors/dependency-error"
212
- });
213
- (StatusCode::INTERNAL_SERVER_ERROR, body)
214
- }
215
- DependencyError::ResolutionFailed { ref message } => {
216
- let body = serde_json::json!({
217
- "detail": "Dependency resolution failed",
218
- "errors": [{
219
- "msg": message,
220
- "type": "resolution_failed"
221
- }],
222
- "status": 503,
223
- "title": "Service Unavailable",
224
- "type": "https://spikard.dev/errors/dependency-error"
225
- });
226
- (StatusCode::SERVICE_UNAVAILABLE, body)
227
- }
228
- _ => {
229
- let body = serde_json::json!({
230
- "detail": "Dependency resolution failed",
231
- "errors": [{
232
- "msg": e.to_string(),
233
- "type": "unknown"
234
- }],
235
- "status": 500,
236
- "title": "Dependency Resolution Failed",
237
- "type": "https://spikard.dev/errors/dependency-error"
238
- });
239
- (StatusCode::INTERNAL_SERVER_ERROR, body)
240
- }
241
- };
242
-
243
- // Return JSON error response
244
- let response = axum::http::Response::builder()
245
- .status(status)
246
- .header("Content-Type", "application/json")
247
- .body(Body::from(json_body.to_string()))
248
- .unwrap();
249
-
250
- return Ok(response);
251
- }
252
- };
253
-
254
- let duration = start.elapsed();
255
- debug!(
256
- "Dependencies resolved in {:?} ({} dependencies)",
257
- duration,
258
- required_dependencies.len()
259
- );
260
-
261
- drop(_enter);
262
-
263
- // Attach resolved dependencies to request_data
264
- request_data.dependencies = Some(Arc::new(resolved));
265
-
266
- // Call the inner handler with enriched request data
267
- let result = inner.call(request, request_data.clone()).await;
268
-
269
- // Cleanup: Execute cleanup tasks after handler completes
270
- // This implements the async Drop pattern for generator-style dependencies
271
- if let Some(deps) = request_data.dependencies.take() {
272
- // Try to get exclusive ownership for cleanup
273
- if let Ok(deps) = Arc::try_unwrap(deps) {
274
- let cleanup_span = info_span!("cleanup_dependencies");
275
- let _enter = cleanup_span.enter();
276
-
277
- debug!("Running dependency cleanup tasks");
278
- deps.cleanup().await;
279
- } else {
280
- // Dependencies are still shared (shouldn't happen in normal flow)
281
- debug!("Skipping cleanup: dependencies still shared");
282
- }
283
- }
284
-
285
- result
286
- })
287
- }
288
- }
289
-
290
- #[cfg(test)]
291
- mod tests {
292
- use super::*;
293
- use crate::handler_trait::RequestData;
294
- use axum::http::Response;
295
- use spikard_core::di::ValueDependency;
296
- use std::collections::HashMap;
297
-
298
- /// Test handler that checks for dependency presence
299
- struct TestHandler;
300
-
301
- impl Handler for TestHandler {
302
- fn call(
303
- &self,
304
- _request: Request<Body>,
305
- request_data: RequestData,
306
- ) -> Pin<Box<dyn Future<Output = HandlerResult> + Send + '_>> {
307
- Box::pin(async move {
308
- // Verify dependencies are present
309
- if request_data.dependencies.is_some() {
310
- let response = Response::builder()
311
- .status(StatusCode::OK)
312
- .body(Body::from("dependencies present"))
313
- .unwrap();
314
- Ok(response)
315
- } else {
316
- Err((StatusCode::INTERNAL_SERVER_ERROR, "no dependencies".to_string()))
317
- }
318
- })
319
- }
320
- }
321
-
322
- #[tokio::test]
323
- async fn test_di_handler_resolves_dependencies() {
324
- // Setup
325
- let mut container = DependencyContainer::new();
326
- container
327
- .register(
328
- "config".to_string(),
329
- Arc::new(ValueDependency::new("config", "test_value")),
330
- )
331
- .unwrap();
332
-
333
- let handler = Arc::new(TestHandler);
334
- let di_handler = DependencyInjectingHandler::new(handler, Arc::new(container), vec!["config".to_string()]);
335
-
336
- // Execute
337
- let request = Request::builder().body(Body::empty()).unwrap();
338
- let request_data = RequestData {
339
- path_params: Arc::new(HashMap::new()),
340
- query_params: serde_json::Value::Null,
341
- raw_query_params: Arc::new(HashMap::new()),
342
- body: serde_json::Value::Null,
343
- raw_body: None,
344
- headers: Arc::new(HashMap::new()),
345
- cookies: Arc::new(HashMap::new()),
346
- method: "GET".to_string(),
347
- path: "/".to_string(),
348
- #[cfg(feature = "di")]
349
- dependencies: None,
350
- };
351
-
352
- let result = di_handler.call(request, request_data).await;
353
-
354
- // Verify
355
- assert!(result.is_ok());
356
- let response = result.unwrap();
357
- assert_eq!(response.status(), StatusCode::OK);
358
- }
359
-
360
- #[tokio::test]
361
- async fn test_di_handler_error_on_missing_dependency() {
362
- // Setup: empty container, but handler requires "database"
363
- let container = DependencyContainer::new();
364
- let handler = Arc::new(TestHandler);
365
- let di_handler = DependencyInjectingHandler::new(handler, Arc::new(container), vec!["database".to_string()]);
366
-
367
- // Execute
368
- let request = Request::builder().body(Body::empty()).unwrap();
369
- let request_data = RequestData {
370
- path_params: Arc::new(HashMap::new()),
371
- query_params: serde_json::Value::Null,
372
- raw_query_params: Arc::new(HashMap::new()),
373
- body: serde_json::Value::Null,
374
- raw_body: None,
375
- headers: Arc::new(HashMap::new()),
376
- cookies: Arc::new(HashMap::new()),
377
- method: "GET".to_string(),
378
- path: "/".to_string(),
379
- #[cfg(feature = "di")]
380
- dependencies: None,
381
- };
382
-
383
- let result = di_handler.call(request, request_data).await;
384
-
385
- // Verify: should return structured error response
386
- assert!(result.is_ok());
387
- let response = result.unwrap();
388
- assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
389
- }
390
-
391
- #[tokio::test]
392
- async fn test_di_handler_empty_dependencies() {
393
- // Setup: no dependencies required
394
- let container = DependencyContainer::new();
395
- let handler = Arc::new(TestHandler);
396
- let di_handler = DependencyInjectingHandler::new(
397
- handler,
398
- Arc::new(container),
399
- vec![], // No dependencies
400
- );
401
-
402
- // Execute
403
- let request = Request::builder().body(Body::empty()).unwrap();
404
- let request_data = RequestData {
405
- path_params: Arc::new(HashMap::new()),
406
- query_params: serde_json::Value::Null,
407
- raw_query_params: Arc::new(HashMap::new()),
408
- body: serde_json::Value::Null,
409
- raw_body: None,
410
- headers: Arc::new(HashMap::new()),
411
- cookies: Arc::new(HashMap::new()),
412
- method: "GET".to_string(),
413
- path: "/".to_string(),
414
- #[cfg(feature = "di")]
415
- dependencies: None,
416
- };
417
-
418
- let result = di_handler.call(request, request_data).await;
419
-
420
- // Verify: should succeed even with empty dependencies
421
- assert!(result.is_ok());
422
- }
423
- }
1
+ //! Dependency Injection Handler Wrapper
2
+ //!
3
+ //! This module provides a handler wrapper that integrates the DI system with the HTTP
4
+ //! handler pipeline. It follows the same composition pattern as `ValidatingHandler`.
5
+ //!
6
+ //! # Architecture
7
+ //!
8
+ //! The `DependencyInjectingHandler` wraps any `Handler` and:
9
+ //! 1. Resolves required dependencies in parallel batches before calling the handler
10
+ //! 2. Attaches resolved dependencies to `RequestData`
11
+ //! 3. Calls the inner handler with the enriched request data
12
+ //! 4. Cleans up dependencies after the handler completes (async Drop pattern)
13
+ //!
14
+ //! # Performance
15
+ //!
16
+ //! - **Zero overhead when no DI**: If no container is provided, DI is skipped entirely
17
+ //! - **Parallel resolution**: Independent dependencies are resolved concurrently
18
+ //! - **Efficient caching**: Singleton and per-request caching minimize redundant work
19
+ //! - **Composable**: Works seamlessly with `ValidatingHandler` and lifecycle hooks
20
+ //!
21
+ //! # Examples
22
+ //!
23
+ //! ```ignore
24
+ //! use spikard_http::di_handler::DependencyInjectingHandler;
25
+ //! use spikard_core::di::DependencyContainer;
26
+ //! use std::sync::Arc;
27
+ //!
28
+ //! # tokio_test::block_on(async {
29
+ //! let container = Arc::new(DependencyContainer::new());
30
+ //! let handler = Arc::new(MyHandler::new());
31
+ //!
32
+ //! let di_handler = DependencyInjectingHandler::new(
33
+ //! handler,
34
+ //! container,
35
+ //! vec!["database".to_string(), "cache".to_string()],
36
+ //! );
37
+ //! # });
38
+ //! ```
39
+
40
+ use crate::handler_trait::{Handler, HandlerResult, RequestData};
41
+ use axum::body::Body;
42
+ use axum::http::{Request, StatusCode};
43
+ use spikard_core::di::{DependencyContainer, DependencyError};
44
+ use std::future::Future;
45
+ use std::pin::Pin;
46
+ use std::sync::Arc;
47
+ use tracing::{debug, info_span, instrument};
48
+
49
+ /// Handler wrapper that resolves dependencies before calling the inner handler
50
+ ///
51
+ /// This wrapper follows the composition pattern used by `ValidatingHandler`:
52
+ /// it wraps an existing handler and enriches the request with resolved dependencies.
53
+ ///
54
+ /// # Thread Safety
55
+ ///
56
+ /// This struct is `Send + Sync` and can be safely shared across threads.
57
+ /// The container is shared via `Arc`, and all dependencies must be `Send + Sync`.
58
+ pub struct DependencyInjectingHandler {
59
+ /// The wrapped handler that will receive the enriched request
60
+ inner: Arc<dyn Handler>,
61
+ /// Shared dependency container for resolution
62
+ container: Arc<DependencyContainer>,
63
+ /// List of dependency names required by this handler
64
+ required_dependencies: Vec<String>,
65
+ }
66
+
67
+ impl DependencyInjectingHandler {
68
+ /// Create a new dependency-injecting handler wrapper
69
+ ///
70
+ /// # Arguments
71
+ ///
72
+ /// * `handler` - The handler to wrap
73
+ /// * `container` - Shared dependency container
74
+ /// * `required_dependencies` - Names of dependencies to resolve for this handler
75
+ ///
76
+ /// # Examples
77
+ ///
78
+ /// ```ignore
79
+ /// use spikard_http::di_handler::DependencyInjectingHandler;
80
+ /// use spikard_core::di::DependencyContainer;
81
+ /// use std::sync::Arc;
82
+ ///
83
+ /// # tokio_test::block_on(async {
84
+ /// let container = Arc::new(DependencyContainer::new());
85
+ /// let handler = Arc::new(MyHandler::new());
86
+ ///
87
+ /// let di_handler = DependencyInjectingHandler::new(
88
+ /// handler,
89
+ /// container,
90
+ /// vec!["db".to_string()],
91
+ /// );
92
+ /// # });
93
+ /// ```
94
+ pub fn new(
95
+ handler: Arc<dyn Handler>,
96
+ container: Arc<DependencyContainer>,
97
+ required_dependencies: Vec<String>,
98
+ ) -> Self {
99
+ Self {
100
+ inner: handler,
101
+ container,
102
+ required_dependencies,
103
+ }
104
+ }
105
+
106
+ /// Get the list of required dependencies
107
+ pub fn required_dependencies(&self) -> &[String] {
108
+ &self.required_dependencies
109
+ }
110
+ }
111
+
112
+ impl Handler for DependencyInjectingHandler {
113
+ #[instrument(
114
+ skip(self, request, request_data),
115
+ fields(
116
+ required_deps = %self.required_dependencies.len(),
117
+ deps = ?self.required_dependencies
118
+ )
119
+ )]
120
+ fn call(
121
+ &self,
122
+ request: Request<Body>,
123
+ mut request_data: RequestData,
124
+ ) -> Pin<Box<dyn Future<Output = HandlerResult> + Send + '_>> {
125
+ eprintln!(
126
+ "[spikard-di] entering DI handler, required_deps={:?}",
127
+ self.required_dependencies
128
+ );
129
+ let inner = self.inner.clone();
130
+ let container = self.container.clone();
131
+ let required_dependencies = self.required_dependencies.clone();
132
+
133
+ Box::pin(async move {
134
+ debug!(
135
+ "DI handler invoked for {} deps; container keys: {:?}",
136
+ required_dependencies.len(),
137
+ container.keys()
138
+ );
139
+ // Span for dependency resolution timing
140
+ let resolution_span = info_span!(
141
+ "resolve_dependencies",
142
+ count = %required_dependencies.len()
143
+ );
144
+ let _enter = resolution_span.enter();
145
+
146
+ debug!(
147
+ "Resolving {} dependencies: {:?}",
148
+ required_dependencies.len(),
149
+ required_dependencies
150
+ );
151
+
152
+ let start = std::time::Instant::now();
153
+
154
+ // Convert RequestData to spikard_core::RequestData for DI
155
+ let core_request_data = spikard_core::RequestData {
156
+ path_params: Arc::clone(&request_data.path_params),
157
+ query_params: request_data.query_params.clone(),
158
+ raw_query_params: Arc::clone(&request_data.raw_query_params),
159
+ body: request_data.body.clone(),
160
+ raw_body: request_data.raw_body.clone(),
161
+ headers: Arc::clone(&request_data.headers),
162
+ cookies: Arc::clone(&request_data.cookies),
163
+ method: request_data.method.clone(),
164
+ path: request_data.path.clone(),
165
+ #[cfg(feature = "di")]
166
+ dependencies: None,
167
+ };
168
+
169
+ // Convert Request<Body> to Request<()> for DI (body not needed for resolution)
170
+ let (parts, _body) = request.into_parts();
171
+ let core_request = Request::from_parts(parts.clone(), ());
172
+
173
+ // Restore original request for handler
174
+ let request = Request::from_parts(parts, axum::body::Body::default());
175
+
176
+ // Resolve dependencies in parallel batches
177
+ let resolved = match container
178
+ .resolve_for_handler(&required_dependencies, &core_request, &core_request_data)
179
+ .await
180
+ {
181
+ Ok(resolved) => resolved,
182
+ Err(e) => {
183
+ debug!("DI error: {}", e);
184
+
185
+ // Convert DI errors to proper JSON HTTP responses
186
+ let (status, json_body) = match e {
187
+ DependencyError::NotFound { ref key } => {
188
+ let body = serde_json::json!({
189
+ "detail": "Required dependency not found",
190
+ "errors": [{
191
+ "dependency_key": key,
192
+ "msg": format!("Dependency '{}' is not registered", key),
193
+ "type": "missing_dependency"
194
+ }],
195
+ "status": 500,
196
+ "title": "Dependency Resolution Failed",
197
+ "type": "https://spikard.dev/errors/dependency-error"
198
+ });
199
+ (StatusCode::INTERNAL_SERVER_ERROR, body)
200
+ }
201
+ DependencyError::CircularDependency { ref cycle } => {
202
+ let body = serde_json::json!({
203
+ "detail": "Circular dependency detected",
204
+ "errors": [{
205
+ "cycle": cycle,
206
+ "msg": "Circular dependency detected in dependency graph",
207
+ "type": "circular_dependency"
208
+ }],
209
+ "status": 500,
210
+ "title": "Dependency Resolution Failed",
211
+ "type": "https://spikard.dev/errors/dependency-error"
212
+ });
213
+ (StatusCode::INTERNAL_SERVER_ERROR, body)
214
+ }
215
+ DependencyError::ResolutionFailed { ref message } => {
216
+ let body = serde_json::json!({
217
+ "detail": "Dependency resolution failed",
218
+ "errors": [{
219
+ "msg": message,
220
+ "type": "resolution_failed"
221
+ }],
222
+ "status": 503,
223
+ "title": "Service Unavailable",
224
+ "type": "https://spikard.dev/errors/dependency-error"
225
+ });
226
+ (StatusCode::SERVICE_UNAVAILABLE, body)
227
+ }
228
+ _ => {
229
+ let body = serde_json::json!({
230
+ "detail": "Dependency resolution failed",
231
+ "errors": [{
232
+ "msg": e.to_string(),
233
+ "type": "unknown"
234
+ }],
235
+ "status": 500,
236
+ "title": "Dependency Resolution Failed",
237
+ "type": "https://spikard.dev/errors/dependency-error"
238
+ });
239
+ (StatusCode::INTERNAL_SERVER_ERROR, body)
240
+ }
241
+ };
242
+
243
+ // Return JSON error response
244
+ let response = axum::http::Response::builder()
245
+ .status(status)
246
+ .header("Content-Type", "application/json")
247
+ .body(Body::from(json_body.to_string()))
248
+ .unwrap();
249
+
250
+ return Ok(response);
251
+ }
252
+ };
253
+
254
+ let duration = start.elapsed();
255
+ debug!(
256
+ "Dependencies resolved in {:?} ({} dependencies)",
257
+ duration,
258
+ required_dependencies.len()
259
+ );
260
+
261
+ drop(_enter);
262
+
263
+ // Attach resolved dependencies to request_data
264
+ request_data.dependencies = Some(Arc::new(resolved));
265
+
266
+ // Call the inner handler with enriched request data
267
+ let result = inner.call(request, request_data.clone()).await;
268
+
269
+ // Cleanup: Execute cleanup tasks after handler completes
270
+ // This implements the async Drop pattern for generator-style dependencies
271
+ if let Some(deps) = request_data.dependencies.take() {
272
+ // Try to get exclusive ownership for cleanup
273
+ if let Ok(deps) = Arc::try_unwrap(deps) {
274
+ let cleanup_span = info_span!("cleanup_dependencies");
275
+ let _enter = cleanup_span.enter();
276
+
277
+ debug!("Running dependency cleanup tasks");
278
+ deps.cleanup().await;
279
+ } else {
280
+ // Dependencies are still shared (shouldn't happen in normal flow)
281
+ debug!("Skipping cleanup: dependencies still shared");
282
+ }
283
+ }
284
+
285
+ result
286
+ })
287
+ }
288
+ }
289
+
290
+ #[cfg(test)]
291
+ mod tests {
292
+ use super::*;
293
+ use crate::handler_trait::RequestData;
294
+ use axum::http::Response;
295
+ use spikard_core::di::ValueDependency;
296
+ use std::collections::HashMap;
297
+
298
+ /// Test handler that checks for dependency presence
299
+ struct TestHandler;
300
+
301
+ impl Handler for TestHandler {
302
+ fn call(
303
+ &self,
304
+ _request: Request<Body>,
305
+ request_data: RequestData,
306
+ ) -> Pin<Box<dyn Future<Output = HandlerResult> + Send + '_>> {
307
+ Box::pin(async move {
308
+ // Verify dependencies are present
309
+ if request_data.dependencies.is_some() {
310
+ let response = Response::builder()
311
+ .status(StatusCode::OK)
312
+ .body(Body::from("dependencies present"))
313
+ .unwrap();
314
+ Ok(response)
315
+ } else {
316
+ Err((StatusCode::INTERNAL_SERVER_ERROR, "no dependencies".to_string()))
317
+ }
318
+ })
319
+ }
320
+ }
321
+
322
+ #[tokio::test]
323
+ async fn test_di_handler_resolves_dependencies() {
324
+ // Setup
325
+ let mut container = DependencyContainer::new();
326
+ container
327
+ .register(
328
+ "config".to_string(),
329
+ Arc::new(ValueDependency::new("config", "test_value")),
330
+ )
331
+ .unwrap();
332
+
333
+ let handler = Arc::new(TestHandler);
334
+ let di_handler = DependencyInjectingHandler::new(handler, Arc::new(container), vec!["config".to_string()]);
335
+
336
+ // Execute
337
+ let request = Request::builder().body(Body::empty()).unwrap();
338
+ let request_data = RequestData {
339
+ path_params: Arc::new(HashMap::new()),
340
+ query_params: serde_json::Value::Null,
341
+ raw_query_params: Arc::new(HashMap::new()),
342
+ body: serde_json::Value::Null,
343
+ raw_body: None,
344
+ headers: Arc::new(HashMap::new()),
345
+ cookies: Arc::new(HashMap::new()),
346
+ method: "GET".to_string(),
347
+ path: "/".to_string(),
348
+ #[cfg(feature = "di")]
349
+ dependencies: None,
350
+ };
351
+
352
+ let result = di_handler.call(request, request_data).await;
353
+
354
+ // Verify
355
+ assert!(result.is_ok());
356
+ let response = result.unwrap();
357
+ assert_eq!(response.status(), StatusCode::OK);
358
+ }
359
+
360
+ #[tokio::test]
361
+ async fn test_di_handler_error_on_missing_dependency() {
362
+ // Setup: empty container, but handler requires "database"
363
+ let container = DependencyContainer::new();
364
+ let handler = Arc::new(TestHandler);
365
+ let di_handler = DependencyInjectingHandler::new(handler, Arc::new(container), vec!["database".to_string()]);
366
+
367
+ // Execute
368
+ let request = Request::builder().body(Body::empty()).unwrap();
369
+ let request_data = RequestData {
370
+ path_params: Arc::new(HashMap::new()),
371
+ query_params: serde_json::Value::Null,
372
+ raw_query_params: Arc::new(HashMap::new()),
373
+ body: serde_json::Value::Null,
374
+ raw_body: None,
375
+ headers: Arc::new(HashMap::new()),
376
+ cookies: Arc::new(HashMap::new()),
377
+ method: "GET".to_string(),
378
+ path: "/".to_string(),
379
+ #[cfg(feature = "di")]
380
+ dependencies: None,
381
+ };
382
+
383
+ let result = di_handler.call(request, request_data).await;
384
+
385
+ // Verify: should return structured error response
386
+ assert!(result.is_ok());
387
+ let response = result.unwrap();
388
+ assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
389
+ }
390
+
391
+ #[tokio::test]
392
+ async fn test_di_handler_empty_dependencies() {
393
+ // Setup: no dependencies required
394
+ let container = DependencyContainer::new();
395
+ let handler = Arc::new(TestHandler);
396
+ let di_handler = DependencyInjectingHandler::new(
397
+ handler,
398
+ Arc::new(container),
399
+ vec![], // No dependencies
400
+ );
401
+
402
+ // Execute
403
+ let request = Request::builder().body(Body::empty()).unwrap();
404
+ let request_data = RequestData {
405
+ path_params: Arc::new(HashMap::new()),
406
+ query_params: serde_json::Value::Null,
407
+ raw_query_params: Arc::new(HashMap::new()),
408
+ body: serde_json::Value::Null,
409
+ raw_body: None,
410
+ headers: Arc::new(HashMap::new()),
411
+ cookies: Arc::new(HashMap::new()),
412
+ method: "GET".to_string(),
413
+ path: "/".to_string(),
414
+ #[cfg(feature = "di")]
415
+ dependencies: None,
416
+ };
417
+
418
+ let result = di_handler.call(request, request_data).await;
419
+
420
+ // Verify: should succeed even with empty dependencies
421
+ assert!(result.is_ok());
422
+ }
423
+ }