spikard 0.2.0 → 0.2.5

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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/ext/spikard_rb/Cargo.toml +3 -2
  4. data/lib/spikard/version.rb +1 -1
  5. data/vendor/bundle/ruby/3.3.0/gems/diff-lcs-1.6.2/mise.toml +5 -0
  6. data/vendor/crates/spikard-core/Cargo.toml +40 -0
  7. data/vendor/crates/spikard-core/src/bindings/mod.rs +3 -0
  8. data/vendor/crates/spikard-core/src/bindings/response.rs +133 -0
  9. data/vendor/crates/spikard-core/src/debug.rs +63 -0
  10. data/vendor/crates/spikard-core/src/di/container.rs +726 -0
  11. data/vendor/crates/spikard-core/src/di/dependency.rs +273 -0
  12. data/vendor/crates/spikard-core/src/di/error.rs +118 -0
  13. data/vendor/crates/spikard-core/src/di/factory.rs +538 -0
  14. data/vendor/crates/spikard-core/src/di/graph.rs +545 -0
  15. data/vendor/crates/spikard-core/src/di/mod.rs +192 -0
  16. data/vendor/crates/spikard-core/src/di/resolved.rs +411 -0
  17. data/vendor/crates/spikard-core/src/di/value.rs +283 -0
  18. data/vendor/crates/spikard-core/src/http.rs +153 -0
  19. data/vendor/crates/spikard-core/src/lib.rs +28 -0
  20. data/vendor/crates/spikard-core/src/lifecycle.rs +422 -0
  21. data/vendor/crates/spikard-core/src/parameters.rs +719 -0
  22. data/vendor/crates/spikard-core/src/problem.rs +310 -0
  23. data/vendor/crates/spikard-core/src/request_data.rs +189 -0
  24. data/vendor/crates/spikard-core/src/router.rs +249 -0
  25. data/vendor/crates/spikard-core/src/schema_registry.rs +183 -0
  26. data/vendor/crates/spikard-core/src/type_hints.rs +304 -0
  27. data/vendor/crates/spikard-core/src/validation.rs +699 -0
  28. data/vendor/crates/spikard-http/Cargo.toml +58 -0
  29. data/vendor/crates/spikard-http/src/auth.rs +247 -0
  30. data/vendor/crates/spikard-http/src/background.rs +249 -0
  31. data/vendor/crates/spikard-http/src/bindings/mod.rs +3 -0
  32. data/vendor/crates/spikard-http/src/bindings/response.rs +1 -0
  33. data/vendor/crates/spikard-http/src/body_metadata.rs +8 -0
  34. data/vendor/crates/spikard-http/src/cors.rs +490 -0
  35. data/vendor/crates/spikard-http/src/debug.rs +63 -0
  36. data/vendor/crates/spikard-http/src/di_handler.rs +423 -0
  37. data/vendor/crates/spikard-http/src/handler_response.rs +190 -0
  38. data/vendor/crates/spikard-http/src/handler_trait.rs +228 -0
  39. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +284 -0
  40. data/vendor/crates/spikard-http/src/lib.rs +529 -0
  41. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +149 -0
  42. data/vendor/crates/spikard-http/src/lifecycle.rs +428 -0
  43. data/vendor/crates/spikard-http/src/middleware/mod.rs +285 -0
  44. data/vendor/crates/spikard-http/src/middleware/multipart.rs +86 -0
  45. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +147 -0
  46. data/vendor/crates/spikard-http/src/middleware/validation.rs +287 -0
  47. data/vendor/crates/spikard-http/src/openapi/mod.rs +309 -0
  48. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +190 -0
  49. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +308 -0
  50. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +195 -0
  51. data/vendor/crates/spikard-http/src/parameters.rs +1 -0
  52. data/vendor/crates/spikard-http/src/problem.rs +1 -0
  53. data/vendor/crates/spikard-http/src/query_parser.rs +369 -0
  54. data/vendor/crates/spikard-http/src/response.rs +399 -0
  55. data/vendor/crates/spikard-http/src/router.rs +1 -0
  56. data/vendor/crates/spikard-http/src/schema_registry.rs +1 -0
  57. data/vendor/crates/spikard-http/src/server/handler.rs +80 -0
  58. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +98 -0
  59. data/vendor/crates/spikard-http/src/server/mod.rs +805 -0
  60. data/vendor/crates/spikard-http/src/server/request_extraction.rs +119 -0
  61. data/vendor/crates/spikard-http/src/sse.rs +447 -0
  62. data/vendor/crates/spikard-http/src/testing/form.rs +14 -0
  63. data/vendor/crates/spikard-http/src/testing/multipart.rs +60 -0
  64. data/vendor/crates/spikard-http/src/testing/test_client.rs +285 -0
  65. data/vendor/crates/spikard-http/src/testing.rs +377 -0
  66. data/vendor/crates/spikard-http/src/type_hints.rs +1 -0
  67. data/vendor/crates/spikard-http/src/validation.rs +1 -0
  68. data/vendor/crates/spikard-http/src/websocket.rs +324 -0
  69. data/vendor/crates/spikard-rb/Cargo.toml +42 -0
  70. data/vendor/crates/spikard-rb/build.rs +8 -0
  71. data/vendor/crates/spikard-rb/src/background.rs +63 -0
  72. data/vendor/crates/spikard-rb/src/config.rs +294 -0
  73. data/vendor/crates/spikard-rb/src/conversion.rs +392 -0
  74. data/vendor/crates/spikard-rb/src/di.rs +409 -0
  75. data/vendor/crates/spikard-rb/src/handler.rs +534 -0
  76. data/vendor/crates/spikard-rb/src/lib.rs +2020 -0
  77. data/vendor/crates/spikard-rb/src/lifecycle.rs +267 -0
  78. data/vendor/crates/spikard-rb/src/server.rs +283 -0
  79. data/vendor/crates/spikard-rb/src/sse.rs +231 -0
  80. data/vendor/crates/spikard-rb/src/test_client.rs +404 -0
  81. data/vendor/crates/spikard-rb/src/test_sse.rs +143 -0
  82. data/vendor/crates/spikard-rb/src/test_websocket.rs +221 -0
  83. data/vendor/crates/spikard-rb/src/websocket.rs +233 -0
  84. metadata +81 -2
@@ -0,0 +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
+ }
@@ -0,0 +1,190 @@
1
+ use axum::{
2
+ BoxError,
3
+ body::Body,
4
+ http::{HeaderMap, HeaderName, HeaderValue, StatusCode},
5
+ response::Response as AxumResponse,
6
+ };
7
+ use bytes::Bytes;
8
+ use futures::{Stream, StreamExt};
9
+ use std::pin::Pin;
10
+
11
+ /// Unified response type that can represent either a ready response or a streaming body.
12
+ ///
13
+ /// This enum allows handlers to return either:
14
+ /// - A complete response that's ready to send (`Response` variant)
15
+ /// - A streaming response with potentially unbounded data (`Stream` variant)
16
+ ///
17
+ /// # Variants
18
+ ///
19
+ /// * `Response` - A complete Axum response ready to send to the client. Use this for
20
+ /// responses where you have all the data ready (files, JSON bodies, HTML, etc.)
21
+ ///
22
+ /// * `Stream` - A streaming response that produces data chunks over time. Use this for:
23
+ /// - Large files (avoid loading entire file in memory)
24
+ /// - Server-Sent Events (SSE)
25
+ /// - Long-polling responses
26
+ /// - Real-time data feeds
27
+ /// - Any unbounded or very large responses
28
+ ///
29
+ /// # Examples
30
+ ///
31
+ /// ```ignore
32
+ /// // Regular response
33
+ /// let response = AxumResponse::builder()
34
+ /// .status(StatusCode::OK)
35
+ /// .body(Body::from("Hello"))
36
+ /// .unwrap();
37
+ /// let handler_response = HandlerResponse::from(response);
38
+ ///
39
+ /// // Streaming response
40
+ /// let stream = futures::stream::iter(vec![
41
+ /// Ok::<_, Box<dyn std::error::Error>>(Bytes::from("chunk1")),
42
+ /// Ok(Bytes::from("chunk2")),
43
+ /// ]);
44
+ /// let response = HandlerResponse::stream(stream)
45
+ /// .with_status(StatusCode::OK);
46
+ /// ```
47
+ pub enum HandlerResponse {
48
+ /// A complete response ready to send
49
+ Response(AxumResponse<Body>),
50
+ /// A streaming response with custom status and headers
51
+ Stream {
52
+ /// The byte stream that will be sent to the client
53
+ stream: Pin<Box<dyn Stream<Item = Result<Bytes, BoxError>> + Send + 'static>>,
54
+ /// HTTP status code for the response
55
+ status: StatusCode,
56
+ /// Response headers to send
57
+ headers: HeaderMap,
58
+ },
59
+ }
60
+
61
+ impl From<AxumResponse<Body>> for HandlerResponse {
62
+ fn from(response: AxumResponse<Body>) -> Self {
63
+ HandlerResponse::Response(response)
64
+ }
65
+ }
66
+
67
+ impl HandlerResponse {
68
+ /// Convert the handler response into an Axum response.
69
+ ///
70
+ /// Consumes the `HandlerResponse` and produces an `AxumResponse<Body>` ready
71
+ /// to be sent to the client. For streaming responses, wraps the stream in an
72
+ /// Axum Body.
73
+ ///
74
+ /// # Returns
75
+ /// An `AxumResponse<Body>` ready to be returned from an Axum handler
76
+ pub fn into_response(self) -> AxumResponse<Body> {
77
+ match self {
78
+ HandlerResponse::Response(response) => response,
79
+ HandlerResponse::Stream {
80
+ stream,
81
+ status,
82
+ mut headers,
83
+ } => {
84
+ let body = Body::from_stream(stream);
85
+ let mut response = AxumResponse::new(body);
86
+ *response.status_mut() = status;
87
+ response.headers_mut().extend(headers.drain());
88
+ response
89
+ }
90
+ }
91
+ }
92
+
93
+ /// Create a streaming response from any async stream of byte chunks.
94
+ ///
95
+ /// Wraps an async stream of byte chunks into a `HandlerResponse::Stream`.
96
+ /// This is useful for large files, real-time data, or any unbounded response.
97
+ ///
98
+ /// # Type Parameters
99
+ /// * `S` - The stream type implementing `Stream<Item = Result<Bytes, E>>`
100
+ /// * `E` - The error type that can be converted to `BoxError`
101
+ ///
102
+ /// # Arguments
103
+ /// * `stream` - An async stream that yields byte chunks or errors
104
+ ///
105
+ /// # Returns
106
+ /// A `HandlerResponse` with 200 OK status and empty headers (customize with
107
+ /// `with_status()` and `with_header()`)
108
+ ///
109
+ /// # Example
110
+ ///
111
+ /// ```ignore
112
+ /// use futures::stream;
113
+ /// use spikard_http::HandlerResponse;
114
+ /// use bytes::Bytes;
115
+ ///
116
+ /// let stream = stream::iter(vec![
117
+ /// Ok::<_, Box<dyn std::error::Error>>(Bytes::from("Hello ")),
118
+ /// Ok(Bytes::from("World")),
119
+ /// ]);
120
+ /// let response = HandlerResponse::stream(stream)
121
+ /// .with_status(StatusCode::OK);
122
+ /// ```
123
+ pub fn stream<S, E>(stream: S) -> Self
124
+ where
125
+ S: Stream<Item = Result<Bytes, E>> + Send + 'static,
126
+ E: Into<BoxError>,
127
+ {
128
+ let mapped = stream.map(|chunk| chunk.map_err(Into::into));
129
+ HandlerResponse::Stream {
130
+ stream: Box::pin(mapped),
131
+ status: StatusCode::OK,
132
+ headers: HeaderMap::new(),
133
+ }
134
+ }
135
+
136
+ /// Override the HTTP status code for the streaming response.
137
+ ///
138
+ /// Sets the HTTP status code to be used in the response. This only affects
139
+ /// `Stream` variants; regular responses already have their status set.
140
+ ///
141
+ /// # Arguments
142
+ /// * `status` - The HTTP status code to use (e.g., `StatusCode::OK`)
143
+ ///
144
+ /// # Returns
145
+ /// Self for method chaining
146
+ ///
147
+ /// # Example
148
+ ///
149
+ /// ```ignore
150
+ /// let response = HandlerResponse::stream(my_stream)
151
+ /// .with_status(StatusCode::PARTIAL_CONTENT);
152
+ /// ```
153
+ pub fn with_status(mut self, status: StatusCode) -> Self {
154
+ if let HandlerResponse::Stream { status: s, .. } = &mut self {
155
+ *s = status;
156
+ }
157
+ self
158
+ }
159
+
160
+ /// Insert or replace a header on the streaming response.
161
+ ///
162
+ /// Adds an HTTP header to the response. This only affects `Stream` variants;
163
+ /// regular responses already have their headers set. If a header with the same
164
+ /// name already exists, it will be replaced.
165
+ ///
166
+ /// # Arguments
167
+ /// * `name` - The header name (e.g., `HeaderName::from_static("content-type")`)
168
+ /// * `value` - The header value
169
+ ///
170
+ /// # Returns
171
+ /// Self for method chaining
172
+ ///
173
+ /// # Example
174
+ ///
175
+ /// ```ignore
176
+ /// use axum::http::{HeaderName, HeaderValue};
177
+ ///
178
+ /// let response = HandlerResponse::stream(my_stream)
179
+ /// .with_header(
180
+ /// HeaderName::from_static("content-type"),
181
+ /// HeaderValue::from_static("application/octet-stream")
182
+ /// );
183
+ /// ```
184
+ pub fn with_header(mut self, name: HeaderName, value: HeaderValue) -> Self {
185
+ if let HandlerResponse::Stream { headers, .. } = &mut self {
186
+ headers.insert(name, value);
187
+ }
188
+ self
189
+ }
190
+ }