spikard 0.3.4 → 0.3.6
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.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +659 -659
- data/ext/spikard_rb/Cargo.toml +17 -17
- data/ext/spikard_rb/extconf.rb +10 -10
- data/ext/spikard_rb/src/lib.rs +6 -6
- data/lib/spikard/app.rb +386 -386
- data/lib/spikard/background.rb +27 -27
- data/lib/spikard/config.rb +396 -396
- data/lib/spikard/converters.rb +13 -13
- data/lib/spikard/handler_wrapper.rb +113 -113
- data/lib/spikard/provide.rb +214 -214
- data/lib/spikard/response.rb +173 -173
- data/lib/spikard/schema.rb +243 -243
- data/lib/spikard/sse.rb +111 -111
- data/lib/spikard/streaming_response.rb +44 -44
- data/lib/spikard/testing.rb +221 -221
- data/lib/spikard/upload_file.rb +131 -131
- data/lib/spikard/version.rb +5 -5
- data/lib/spikard/websocket.rb +59 -59
- data/lib/spikard.rb +43 -43
- data/sig/spikard.rbs +366 -360
- data/vendor/crates/spikard-core/Cargo.toml +40 -40
- data/vendor/crates/spikard-core/src/bindings/mod.rs +3 -3
- data/vendor/crates/spikard-core/src/bindings/response.rs +133 -133
- data/vendor/crates/spikard-core/src/debug.rs +63 -63
- data/vendor/crates/spikard-core/src/di/container.rs +726 -726
- data/vendor/crates/spikard-core/src/di/dependency.rs +273 -273
- data/vendor/crates/spikard-core/src/di/error.rs +118 -118
- data/vendor/crates/spikard-core/src/di/factory.rs +538 -538
- data/vendor/crates/spikard-core/src/di/graph.rs +545 -545
- data/vendor/crates/spikard-core/src/di/mod.rs +192 -192
- data/vendor/crates/spikard-core/src/di/resolved.rs +411 -411
- data/vendor/crates/spikard-core/src/di/value.rs +283 -283
- data/vendor/crates/spikard-core/src/errors.rs +39 -39
- data/vendor/crates/spikard-core/src/http.rs +153 -153
- data/vendor/crates/spikard-core/src/lib.rs +29 -29
- data/vendor/crates/spikard-core/src/lifecycle.rs +422 -422
- data/vendor/crates/spikard-core/src/parameters.rs +722 -722
- data/vendor/crates/spikard-core/src/problem.rs +310 -310
- data/vendor/crates/spikard-core/src/request_data.rs +189 -189
- data/vendor/crates/spikard-core/src/router.rs +249 -249
- data/vendor/crates/spikard-core/src/schema_registry.rs +183 -183
- data/vendor/crates/spikard-core/src/type_hints.rs +304 -304
- data/vendor/crates/spikard-core/src/validation.rs +699 -699
- data/vendor/crates/spikard-http/Cargo.toml +68 -58
- data/vendor/crates/spikard-http/src/auth.rs +247 -247
- data/vendor/crates/spikard-http/src/background.rs +249 -249
- data/vendor/crates/spikard-http/src/bindings/mod.rs +3 -3
- data/vendor/crates/spikard-http/src/bindings/response.rs +1 -1
- data/vendor/crates/spikard-http/src/body_metadata.rs +8 -8
- data/vendor/crates/spikard-http/src/cors.rs +490 -490
- data/vendor/crates/spikard-http/src/debug.rs +63 -63
- data/vendor/crates/spikard-http/src/di_handler.rs +423 -423
- data/vendor/crates/spikard-http/src/handler_response.rs +190 -190
- data/vendor/crates/spikard-http/src/handler_trait.rs +228 -228
- data/vendor/crates/spikard-http/src/handler_trait_tests.rs +284 -284
- data/vendor/crates/spikard-http/src/lib.rs +529 -529
- data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +149 -149
- data/vendor/crates/spikard-http/src/lifecycle.rs +428 -428
- data/vendor/crates/spikard-http/src/middleware/mod.rs +285 -285
- data/vendor/crates/spikard-http/src/middleware/multipart.rs +86 -86
- data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +147 -147
- data/vendor/crates/spikard-http/src/middleware/validation.rs +287 -287
- data/vendor/crates/spikard-http/src/openapi/mod.rs +309 -309
- data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +190 -190
- data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +308 -308
- data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +195 -195
- data/vendor/crates/spikard-http/src/parameters.rs +1 -1
- data/vendor/crates/spikard-http/src/problem.rs +1 -1
- data/vendor/crates/spikard-http/src/query_parser.rs +369 -369
- data/vendor/crates/spikard-http/src/response.rs +399 -399
- data/vendor/crates/spikard-http/src/router.rs +1 -1
- data/vendor/crates/spikard-http/src/schema_registry.rs +1 -1
- data/vendor/crates/spikard-http/src/server/handler.rs +87 -87
- data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +98 -98
- data/vendor/crates/spikard-http/src/server/mod.rs +805 -805
- data/vendor/crates/spikard-http/src/server/request_extraction.rs +119 -119
- data/vendor/crates/spikard-http/src/sse.rs +447 -447
- data/vendor/crates/spikard-http/src/testing/form.rs +14 -14
- data/vendor/crates/spikard-http/src/testing/multipart.rs +60 -60
- data/vendor/crates/spikard-http/src/testing/test_client.rs +285 -285
- data/vendor/crates/spikard-http/src/testing.rs +377 -377
- data/vendor/crates/spikard-http/src/type_hints.rs +1 -1
- data/vendor/crates/spikard-http/src/validation.rs +1 -1
- data/vendor/crates/spikard-http/src/websocket.rs +324 -324
- data/vendor/crates/spikard-rb/Cargo.toml +42 -42
- data/vendor/crates/spikard-rb/build.rs +8 -8
- data/vendor/crates/spikard-rb/src/background.rs +63 -63
- data/vendor/crates/spikard-rb/src/config.rs +294 -294
- data/vendor/crates/spikard-rb/src/conversion.rs +453 -453
- data/vendor/crates/spikard-rb/src/di.rs +409 -409
- data/vendor/crates/spikard-rb/src/handler.rs +625 -625
- data/vendor/crates/spikard-rb/src/lib.rs +2771 -2771
- data/vendor/crates/spikard-rb/src/lifecycle.rs +274 -274
- data/vendor/crates/spikard-rb/src/server.rs +283 -283
- data/vendor/crates/spikard-rb/src/sse.rs +231 -231
- data/vendor/crates/spikard-rb/src/test_client.rs +404 -404
- data/vendor/crates/spikard-rb/src/test_sse.rs +143 -143
- data/vendor/crates/spikard-rb/src/test_websocket.rs +221 -221
- data/vendor/crates/spikard-rb/src/websocket.rs +233 -233
- metadata +1 -79
- data/vendor/spikard-core/Cargo.toml +0 -40
- data/vendor/spikard-core/src/bindings/mod.rs +0 -3
- data/vendor/spikard-core/src/bindings/response.rs +0 -133
- data/vendor/spikard-core/src/debug.rs +0 -63
- data/vendor/spikard-core/src/di/container.rs +0 -726
- data/vendor/spikard-core/src/di/dependency.rs +0 -273
- data/vendor/spikard-core/src/di/error.rs +0 -118
- data/vendor/spikard-core/src/di/factory.rs +0 -538
- data/vendor/spikard-core/src/di/graph.rs +0 -545
- data/vendor/spikard-core/src/di/mod.rs +0 -192
- data/vendor/spikard-core/src/di/resolved.rs +0 -411
- data/vendor/spikard-core/src/di/value.rs +0 -283
- data/vendor/spikard-core/src/http.rs +0 -153
- data/vendor/spikard-core/src/lib.rs +0 -28
- data/vendor/spikard-core/src/lifecycle.rs +0 -422
- data/vendor/spikard-core/src/parameters.rs +0 -719
- data/vendor/spikard-core/src/problem.rs +0 -310
- data/vendor/spikard-core/src/request_data.rs +0 -189
- data/vendor/spikard-core/src/router.rs +0 -249
- data/vendor/spikard-core/src/schema_registry.rs +0 -183
- data/vendor/spikard-core/src/type_hints.rs +0 -304
- data/vendor/spikard-core/src/validation.rs +0 -699
- data/vendor/spikard-http/Cargo.toml +0 -58
- data/vendor/spikard-http/src/auth.rs +0 -247
- data/vendor/spikard-http/src/background.rs +0 -249
- data/vendor/spikard-http/src/bindings/mod.rs +0 -3
- data/vendor/spikard-http/src/bindings/response.rs +0 -1
- data/vendor/spikard-http/src/body_metadata.rs +0 -8
- data/vendor/spikard-http/src/cors.rs +0 -490
- data/vendor/spikard-http/src/debug.rs +0 -63
- data/vendor/spikard-http/src/di_handler.rs +0 -423
- data/vendor/spikard-http/src/handler_response.rs +0 -190
- data/vendor/spikard-http/src/handler_trait.rs +0 -228
- data/vendor/spikard-http/src/handler_trait_tests.rs +0 -284
- data/vendor/spikard-http/src/lib.rs +0 -529
- data/vendor/spikard-http/src/lifecycle/adapter.rs +0 -149
- data/vendor/spikard-http/src/lifecycle.rs +0 -428
- data/vendor/spikard-http/src/middleware/mod.rs +0 -285
- data/vendor/spikard-http/src/middleware/multipart.rs +0 -86
- data/vendor/spikard-http/src/middleware/urlencoded.rs +0 -147
- data/vendor/spikard-http/src/middleware/validation.rs +0 -287
- data/vendor/spikard-http/src/openapi/mod.rs +0 -309
- data/vendor/spikard-http/src/openapi/parameter_extraction.rs +0 -190
- data/vendor/spikard-http/src/openapi/schema_conversion.rs +0 -308
- data/vendor/spikard-http/src/openapi/spec_generation.rs +0 -195
- data/vendor/spikard-http/src/parameters.rs +0 -1
- data/vendor/spikard-http/src/problem.rs +0 -1
- data/vendor/spikard-http/src/query_parser.rs +0 -369
- data/vendor/spikard-http/src/response.rs +0 -399
- data/vendor/spikard-http/src/router.rs +0 -1
- data/vendor/spikard-http/src/schema_registry.rs +0 -1
- data/vendor/spikard-http/src/server/handler.rs +0 -80
- data/vendor/spikard-http/src/server/lifecycle_execution.rs +0 -98
- data/vendor/spikard-http/src/server/mod.rs +0 -805
- data/vendor/spikard-http/src/server/request_extraction.rs +0 -119
- data/vendor/spikard-http/src/sse.rs +0 -447
- data/vendor/spikard-http/src/testing/form.rs +0 -14
- data/vendor/spikard-http/src/testing/multipart.rs +0 -60
- data/vendor/spikard-http/src/testing/test_client.rs +0 -285
- data/vendor/spikard-http/src/testing.rs +0 -377
- data/vendor/spikard-http/src/type_hints.rs +0 -1
- data/vendor/spikard-http/src/validation.rs +0 -1
- data/vendor/spikard-http/src/websocket.rs +0 -324
- data/vendor/spikard-rb/Cargo.toml +0 -42
- data/vendor/spikard-rb/build.rs +0 -8
- data/vendor/spikard-rb/src/background.rs +0 -63
- data/vendor/spikard-rb/src/config.rs +0 -294
- data/vendor/spikard-rb/src/conversion.rs +0 -392
- data/vendor/spikard-rb/src/di.rs +0 -409
- data/vendor/spikard-rb/src/handler.rs +0 -534
- data/vendor/spikard-rb/src/lib.rs +0 -2020
- data/vendor/spikard-rb/src/lifecycle.rs +0 -267
- data/vendor/spikard-rb/src/server.rs +0 -283
- data/vendor/spikard-rb/src/sse.rs +0 -231
- data/vendor/spikard-rb/src/test_client.rs +0 -404
- data/vendor/spikard-rb/src/test_sse.rs +0 -143
- data/vendor/spikard-rb/src/test_websocket.rs +0 -221
- data/vendor/spikard-rb/src/websocket.rs +0 -233
|
@@ -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
|
+
}
|