spikard 0.5.0 → 0.6.1
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 +674 -674
- data/ext/spikard_rb/Cargo.toml +17 -17
- data/ext/spikard_rb/extconf.rb +13 -10
- data/ext/spikard_rb/src/lib.rs +6 -6
- data/lib/spikard/app.rb +405 -405
- 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 +256 -256
- 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 -366
- data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -63
- data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +132 -132
- data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +752 -752
- data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -194
- data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -246
- data/vendor/crates/spikard-bindings-shared/src/error_response.rs +401 -401
- data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +238 -238
- data/vendor/crates/spikard-bindings-shared/src/lib.rs +24 -24
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +292 -292
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +616 -616
- data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +305 -305
- data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -248
- data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +351 -351
- data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +454 -454
- data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +383 -383
- data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +280 -280
- 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 +127 -127
- data/vendor/crates/spikard-core/src/di/container.rs +702 -702
- 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 +534 -534
- data/vendor/crates/spikard-core/src/di/graph.rs +506 -506
- data/vendor/crates/spikard-core/src/di/mod.rs +192 -192
- data/vendor/crates/spikard-core/src/di/resolved.rs +405 -405
- data/vendor/crates/spikard-core/src/di/value.rs +281 -281
- data/vendor/crates/spikard-core/src/errors.rs +69 -69
- data/vendor/crates/spikard-core/src/http.rs +415 -415
- data/vendor/crates/spikard-core/src/lib.rs +29 -29
- data/vendor/crates/spikard-core/src/lifecycle.rs +1186 -1186
- data/vendor/crates/spikard-core/src/metadata.rs +389 -389
- data/vendor/crates/spikard-core/src/parameters.rs +2525 -2525
- data/vendor/crates/spikard-core/src/problem.rs +344 -344
- data/vendor/crates/spikard-core/src/request_data.rs +1154 -1154
- data/vendor/crates/spikard-core/src/router.rs +510 -510
- 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/error_mapper.rs +696 -688
- data/vendor/crates/spikard-core/src/validation/mod.rs +457 -457
- data/vendor/crates/spikard-http/Cargo.toml +62 -64
- data/vendor/crates/spikard-http/examples/sse-notifications.rs +148 -148
- data/vendor/crates/spikard-http/examples/websocket-chat.rs +92 -92
- data/vendor/crates/spikard-http/src/auth.rs +296 -296
- data/vendor/crates/spikard-http/src/background.rs +1860 -1860
- 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 +1005 -1005
- data/vendor/crates/spikard-http/src/debug.rs +128 -128
- data/vendor/crates/spikard-http/src/di_handler.rs +1668 -1668
- data/vendor/crates/spikard-http/src/handler_response.rs +901 -901
- data/vendor/crates/spikard-http/src/handler_trait.rs +838 -830
- data/vendor/crates/spikard-http/src/handler_trait_tests.rs +290 -290
- data/vendor/crates/spikard-http/src/lib.rs +534 -534
- data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +230 -230
- data/vendor/crates/spikard-http/src/lifecycle.rs +1193 -1193
- data/vendor/crates/spikard-http/src/middleware/mod.rs +560 -540
- data/vendor/crates/spikard-http/src/middleware/multipart.rs +912 -912
- data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +513 -513
- data/vendor/crates/spikard-http/src/middleware/validation.rs +768 -735
- data/vendor/crates/spikard-http/src/openapi/mod.rs +309 -309
- data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +535 -535
- data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +1363 -1363
- data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +665 -665
- data/vendor/crates/spikard-http/src/query_parser.rs +793 -793
- data/vendor/crates/spikard-http/src/response.rs +720 -720
- data/vendor/crates/spikard-http/src/server/handler.rs +1650 -1650
- data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +234 -234
- data/vendor/crates/spikard-http/src/server/mod.rs +1593 -1502
- data/vendor/crates/spikard-http/src/server/request_extraction.rs +789 -770
- data/vendor/crates/spikard-http/src/server/routing_factory.rs +629 -599
- data/vendor/crates/spikard-http/src/sse.rs +1409 -1409
- data/vendor/crates/spikard-http/src/testing/form.rs +52 -52
- data/vendor/crates/spikard-http/src/testing/multipart.rs +64 -60
- data/vendor/crates/spikard-http/src/testing/test_client.rs +311 -283
- data/vendor/crates/spikard-http/src/testing.rs +406 -377
- data/vendor/crates/spikard-http/src/websocket.rs +1404 -1375
- data/vendor/crates/spikard-http/tests/background_behavior.rs +832 -832
- data/vendor/crates/spikard-http/tests/common/handlers.rs +309 -309
- data/vendor/crates/spikard-http/tests/common/mod.rs +26 -26
- data/vendor/crates/spikard-http/tests/di_integration.rs +192 -192
- data/vendor/crates/spikard-http/tests/doc_snippets.rs +5 -5
- data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1093 -1093
- data/vendor/crates/spikard-http/tests/multipart_behavior.rs +656 -656
- data/vendor/crates/spikard-http/tests/server_config_builder.rs +314 -314
- data/vendor/crates/spikard-http/tests/sse_behavior.rs +620 -620
- data/vendor/crates/spikard-http/tests/websocket_behavior.rs +663 -663
- data/vendor/crates/spikard-rb/Cargo.toml +48 -48
- data/vendor/crates/spikard-rb/build.rs +199 -199
- data/vendor/crates/spikard-rb/src/background.rs +63 -63
- data/vendor/crates/spikard-rb/src/config/mod.rs +5 -5
- data/vendor/crates/spikard-rb/src/config/server_config.rs +285 -285
- data/vendor/crates/spikard-rb/src/conversion.rs +554 -554
- data/vendor/crates/spikard-rb/src/di/builder.rs +100 -100
- data/vendor/crates/spikard-rb/src/di/mod.rs +375 -375
- data/vendor/crates/spikard-rb/src/handler.rs +618 -618
- data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -3
- data/vendor/crates/spikard-rb/src/lib.rs +1806 -1810
- data/vendor/crates/spikard-rb/src/lifecycle.rs +275 -275
- data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -5
- data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +442 -447
- data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -5
- data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +324 -324
- data/vendor/crates/spikard-rb/src/server.rs +305 -308
- data/vendor/crates/spikard-rb/src/sse.rs +231 -231
- data/vendor/crates/spikard-rb/src/testing/client.rs +538 -551
- data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -7
- data/vendor/crates/spikard-rb/src/testing/sse.rs +143 -143
- data/vendor/crates/spikard-rb/src/testing/websocket.rs +608 -635
- data/vendor/crates/spikard-rb/src/websocket.rs +377 -374
- metadata +15 -1
|
@@ -1,234 +1,234 @@
|
|
|
1
|
-
//! Lifecycle hooks execution logic
|
|
2
|
-
|
|
3
|
-
use crate::handler_trait::Handler;
|
|
4
|
-
use axum::body::Body;
|
|
5
|
-
use axum::http::StatusCode;
|
|
6
|
-
use std::sync::Arc;
|
|
7
|
-
|
|
8
|
-
/// Execute a handler with lifecycle hooks
|
|
9
|
-
///
|
|
10
|
-
/// This wraps the handler execution with lifecycle hooks at appropriate points:
|
|
11
|
-
/// 1. preValidation hooks (before handler, which does validation)
|
|
12
|
-
/// 2. preHandler hooks (after validation, before handler)
|
|
13
|
-
/// 3. Handler execution
|
|
14
|
-
/// 4. onResponse hooks (after successful handler execution)
|
|
15
|
-
/// 5. onError hooks (if handler or any hook fails)
|
|
16
|
-
pub async fn execute_with_lifecycle_hooks(
|
|
17
|
-
req: axum::http::Request<Body>,
|
|
18
|
-
request_data: crate::handler_trait::RequestData,
|
|
19
|
-
handler: Arc<dyn Handler>,
|
|
20
|
-
hooks: Option<Arc<crate::LifecycleHooks>>,
|
|
21
|
-
) -> Result<axum::http::Response<Body>, (axum::http::StatusCode, String)> {
|
|
22
|
-
use crate::lifecycle::HookResult;
|
|
23
|
-
|
|
24
|
-
let Some(hooks) = hooks else {
|
|
25
|
-
return handler.call(req, request_data).await;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
if hooks.is_empty() {
|
|
29
|
-
return handler.call(req, request_data).await;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
let req = match hooks.execute_pre_validation(req).await {
|
|
33
|
-
Ok(HookResult::Continue(r)) => r,
|
|
34
|
-
Ok(HookResult::ShortCircuit(response)) => return Ok(response),
|
|
35
|
-
Err(e) => {
|
|
36
|
-
let error_response = axum::http::Response::builder()
|
|
37
|
-
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
38
|
-
.body(Body::from(format!(
|
|
39
|
-
"{{\"error\":\"preValidation hook failed: {}\"}}",
|
|
40
|
-
e
|
|
41
|
-
)))
|
|
42
|
-
.unwrap();
|
|
43
|
-
|
|
44
|
-
return match hooks.execute_on_error(error_response).await {
|
|
45
|
-
Ok(resp) => Ok(resp),
|
|
46
|
-
Err(_) => Ok(axum::http::Response::builder()
|
|
47
|
-
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
48
|
-
.body(Body::from("{\"error\":\"Hook execution failed\"}"))
|
|
49
|
-
.unwrap()),
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
let req = match hooks.execute_pre_handler(req).await {
|
|
55
|
-
Ok(HookResult::Continue(r)) => r,
|
|
56
|
-
Ok(HookResult::ShortCircuit(response)) => return Ok(response),
|
|
57
|
-
Err(e) => {
|
|
58
|
-
let error_response = axum::http::Response::builder()
|
|
59
|
-
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
60
|
-
.body(Body::from(format!("{{\"error\":\"preHandler hook failed: {}\"}}", e)))
|
|
61
|
-
.unwrap();
|
|
62
|
-
|
|
63
|
-
return match hooks.execute_on_error(error_response).await {
|
|
64
|
-
Ok(resp) => Ok(resp),
|
|
65
|
-
Err(_) => Ok(axum::http::Response::builder()
|
|
66
|
-
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
67
|
-
.body(Body::from("{\"error\":\"Hook execution failed\"}"))
|
|
68
|
-
.unwrap()),
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
let response = match handler.call(req, request_data).await {
|
|
74
|
-
Ok(resp) => resp,
|
|
75
|
-
Err((status, message)) => {
|
|
76
|
-
let error_response = axum::http::Response::builder()
|
|
77
|
-
.status(status)
|
|
78
|
-
.body(Body::from(message))
|
|
79
|
-
.unwrap();
|
|
80
|
-
|
|
81
|
-
return match hooks.execute_on_error(error_response).await {
|
|
82
|
-
Ok(resp) => Ok(resp),
|
|
83
|
-
Err(e) => Ok(axum::http::Response::builder()
|
|
84
|
-
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
85
|
-
.body(Body::from(format!("{{\"error\":\"onError hook failed: {}\"}}", e)))
|
|
86
|
-
.unwrap()),
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
match hooks.execute_on_response(response).await {
|
|
92
|
-
Ok(resp) => Ok(resp),
|
|
93
|
-
Err(e) => Ok(axum::http::Response::builder()
|
|
94
|
-
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
95
|
-
.body(Body::from(format!("{{\"error\":\"onResponse hook failed: {}\"}}", e)))
|
|
96
|
-
.unwrap()),
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
#[cfg(test)]
|
|
101
|
-
mod tests {
|
|
102
|
-
use super::*;
|
|
103
|
-
use crate::lifecycle::{HookResult, request_hook, response_hook};
|
|
104
|
-
use axum::http::{Request, Response, StatusCode};
|
|
105
|
-
use http_body_util::BodyExt;
|
|
106
|
-
use serde_json::json;
|
|
107
|
-
use std::collections::HashMap;
|
|
108
|
-
|
|
109
|
-
struct OkHandler;
|
|
110
|
-
|
|
111
|
-
impl Handler for OkHandler {
|
|
112
|
-
fn call(
|
|
113
|
-
&self,
|
|
114
|
-
_request: Request<Body>,
|
|
115
|
-
_request_data: crate::handler_trait::RequestData,
|
|
116
|
-
) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::handler_trait::HandlerResult> + Send + '_>>
|
|
117
|
-
{
|
|
118
|
-
Box::pin(async move {
|
|
119
|
-
Ok(Response::builder()
|
|
120
|
-
.status(StatusCode::OK)
|
|
121
|
-
.body(Body::from("ok"))
|
|
122
|
-
.unwrap())
|
|
123
|
-
})
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
struct ErrHandler;
|
|
128
|
-
|
|
129
|
-
impl Handler for ErrHandler {
|
|
130
|
-
fn call(
|
|
131
|
-
&self,
|
|
132
|
-
_request: Request<Body>,
|
|
133
|
-
_request_data: crate::handler_trait::RequestData,
|
|
134
|
-
) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::handler_trait::HandlerResult> + Send + '_>>
|
|
135
|
-
{
|
|
136
|
-
Box::pin(async move { Err((StatusCode::BAD_REQUEST, "bad".to_string())) })
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
fn empty_request_data() -> crate::handler_trait::RequestData {
|
|
141
|
-
crate::handler_trait::RequestData {
|
|
142
|
-
path_params: std::sync::Arc::new(HashMap::new()),
|
|
143
|
-
query_params: json!({}),
|
|
144
|
-
validated_params: None,
|
|
145
|
-
raw_query_params: std::sync::Arc::new(HashMap::new()),
|
|
146
|
-
body: json!(null),
|
|
147
|
-
raw_body: None,
|
|
148
|
-
headers: std::sync::Arc::new(HashMap::new()),
|
|
149
|
-
cookies: std::sync::Arc::new(HashMap::new()),
|
|
150
|
-
method: "GET".to_string(),
|
|
151
|
-
path: "/".to_string(),
|
|
152
|
-
#[cfg(feature = "di")]
|
|
153
|
-
dependencies: None,
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
#[tokio::test]
|
|
158
|
-
async fn pre_validation_error_with_failing_on_error_hook_returns_fallback() {
|
|
159
|
-
let mut hooks = crate::LifecycleHooks::new();
|
|
160
|
-
hooks.add_pre_validation(request_hook("boom", |_req| async move { Err("boom".to_string()) }));
|
|
161
|
-
hooks.add_on_error(response_hook("fail-on-error", |_resp| async move {
|
|
162
|
-
Err("fail".to_string())
|
|
163
|
-
}));
|
|
164
|
-
|
|
165
|
-
let req = Request::builder().uri("/").body(Body::empty()).unwrap();
|
|
166
|
-
let resp = execute_with_lifecycle_hooks(req, empty_request_data(), Arc::new(OkHandler), Some(Arc::new(hooks)))
|
|
167
|
-
.await
|
|
168
|
-
.unwrap();
|
|
169
|
-
|
|
170
|
-
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
|
171
|
-
let body = resp.into_body().collect().await.unwrap().to_bytes();
|
|
172
|
-
assert_eq!(body.as_ref(), b"{\"error\":\"Hook execution failed\"}");
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
#[tokio::test]
|
|
176
|
-
async fn handler_error_with_failing_on_error_hook_returns_on_error_hook_failed_response() {
|
|
177
|
-
let mut hooks = crate::LifecycleHooks::new();
|
|
178
|
-
hooks.add_on_error(response_hook("fail-on-error", |_resp| async move {
|
|
179
|
-
Err("boom".to_string())
|
|
180
|
-
}));
|
|
181
|
-
|
|
182
|
-
let req = Request::builder().uri("/").body(Body::empty()).unwrap();
|
|
183
|
-
let resp = execute_with_lifecycle_hooks(req, empty_request_data(), Arc::new(ErrHandler), Some(Arc::new(hooks)))
|
|
184
|
-
.await
|
|
185
|
-
.unwrap();
|
|
186
|
-
|
|
187
|
-
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
|
188
|
-
let body = resp.into_body().collect().await.unwrap().to_bytes();
|
|
189
|
-
let body_str = std::str::from_utf8(body.as_ref()).unwrap();
|
|
190
|
-
assert!(body_str.contains("\"error\":\"onError hook failed:"));
|
|
191
|
-
assert!(body_str.contains("boom"));
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
#[tokio::test]
|
|
195
|
-
async fn on_response_hook_error_returns_on_response_hook_failed_response() {
|
|
196
|
-
let mut hooks = crate::LifecycleHooks::new();
|
|
197
|
-
hooks.add_on_response(response_hook("fail-on-response", |_resp| async move {
|
|
198
|
-
Err("boom".to_string())
|
|
199
|
-
}));
|
|
200
|
-
|
|
201
|
-
let req = Request::builder().uri("/").body(Body::empty()).unwrap();
|
|
202
|
-
let resp = execute_with_lifecycle_hooks(req, empty_request_data(), Arc::new(OkHandler), Some(Arc::new(hooks)))
|
|
203
|
-
.await
|
|
204
|
-
.unwrap();
|
|
205
|
-
|
|
206
|
-
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
|
207
|
-
let body = resp.into_body().collect().await.unwrap().to_bytes();
|
|
208
|
-
let body_str = std::str::from_utf8(body.as_ref()).unwrap();
|
|
209
|
-
assert!(body_str.contains("\"error\":\"onResponse hook failed:"));
|
|
210
|
-
assert!(body_str.contains("boom"));
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
#[tokio::test]
|
|
214
|
-
async fn pre_validation_short_circuit_skips_handler_and_returns_response() {
|
|
215
|
-
let mut hooks = crate::LifecycleHooks::new();
|
|
216
|
-
hooks.add_pre_validation(request_hook("short-circuit", |_req| async move {
|
|
217
|
-
Ok(HookResult::ShortCircuit(
|
|
218
|
-
Response::builder()
|
|
219
|
-
.status(StatusCode::UNAUTHORIZED)
|
|
220
|
-
.body(Body::from("nope"))
|
|
221
|
-
.unwrap(),
|
|
222
|
-
))
|
|
223
|
-
}));
|
|
224
|
-
|
|
225
|
-
let req = Request::builder().uri("/").body(Body::empty()).unwrap();
|
|
226
|
-
let resp = execute_with_lifecycle_hooks(req, empty_request_data(), Arc::new(ErrHandler), Some(Arc::new(hooks)))
|
|
227
|
-
.await
|
|
228
|
-
.unwrap();
|
|
229
|
-
|
|
230
|
-
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
|
|
231
|
-
let body = resp.into_body().collect().await.unwrap().to_bytes();
|
|
232
|
-
assert_eq!(body.as_ref(), b"nope");
|
|
233
|
-
}
|
|
234
|
-
}
|
|
1
|
+
//! Lifecycle hooks execution logic
|
|
2
|
+
|
|
3
|
+
use crate::handler_trait::Handler;
|
|
4
|
+
use axum::body::Body;
|
|
5
|
+
use axum::http::StatusCode;
|
|
6
|
+
use std::sync::Arc;
|
|
7
|
+
|
|
8
|
+
/// Execute a handler with lifecycle hooks
|
|
9
|
+
///
|
|
10
|
+
/// This wraps the handler execution with lifecycle hooks at appropriate points:
|
|
11
|
+
/// 1. preValidation hooks (before handler, which does validation)
|
|
12
|
+
/// 2. preHandler hooks (after validation, before handler)
|
|
13
|
+
/// 3. Handler execution
|
|
14
|
+
/// 4. onResponse hooks (after successful handler execution)
|
|
15
|
+
/// 5. onError hooks (if handler or any hook fails)
|
|
16
|
+
pub async fn execute_with_lifecycle_hooks(
|
|
17
|
+
req: axum::http::Request<Body>,
|
|
18
|
+
request_data: crate::handler_trait::RequestData,
|
|
19
|
+
handler: Arc<dyn Handler>,
|
|
20
|
+
hooks: Option<Arc<crate::LifecycleHooks>>,
|
|
21
|
+
) -> Result<axum::http::Response<Body>, (axum::http::StatusCode, String)> {
|
|
22
|
+
use crate::lifecycle::HookResult;
|
|
23
|
+
|
|
24
|
+
let Some(hooks) = hooks else {
|
|
25
|
+
return handler.call(req, request_data).await;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
if hooks.is_empty() {
|
|
29
|
+
return handler.call(req, request_data).await;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let req = match hooks.execute_pre_validation(req).await {
|
|
33
|
+
Ok(HookResult::Continue(r)) => r,
|
|
34
|
+
Ok(HookResult::ShortCircuit(response)) => return Ok(response),
|
|
35
|
+
Err(e) => {
|
|
36
|
+
let error_response = axum::http::Response::builder()
|
|
37
|
+
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
38
|
+
.body(Body::from(format!(
|
|
39
|
+
"{{\"error\":\"preValidation hook failed: {}\"}}",
|
|
40
|
+
e
|
|
41
|
+
)))
|
|
42
|
+
.unwrap();
|
|
43
|
+
|
|
44
|
+
return match hooks.execute_on_error(error_response).await {
|
|
45
|
+
Ok(resp) => Ok(resp),
|
|
46
|
+
Err(_) => Ok(axum::http::Response::builder()
|
|
47
|
+
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
48
|
+
.body(Body::from("{\"error\":\"Hook execution failed\"}"))
|
|
49
|
+
.unwrap()),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
let req = match hooks.execute_pre_handler(req).await {
|
|
55
|
+
Ok(HookResult::Continue(r)) => r,
|
|
56
|
+
Ok(HookResult::ShortCircuit(response)) => return Ok(response),
|
|
57
|
+
Err(e) => {
|
|
58
|
+
let error_response = axum::http::Response::builder()
|
|
59
|
+
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
60
|
+
.body(Body::from(format!("{{\"error\":\"preHandler hook failed: {}\"}}", e)))
|
|
61
|
+
.unwrap();
|
|
62
|
+
|
|
63
|
+
return match hooks.execute_on_error(error_response).await {
|
|
64
|
+
Ok(resp) => Ok(resp),
|
|
65
|
+
Err(_) => Ok(axum::http::Response::builder()
|
|
66
|
+
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
67
|
+
.body(Body::from("{\"error\":\"Hook execution failed\"}"))
|
|
68
|
+
.unwrap()),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
let response = match handler.call(req, request_data).await {
|
|
74
|
+
Ok(resp) => resp,
|
|
75
|
+
Err((status, message)) => {
|
|
76
|
+
let error_response = axum::http::Response::builder()
|
|
77
|
+
.status(status)
|
|
78
|
+
.body(Body::from(message))
|
|
79
|
+
.unwrap();
|
|
80
|
+
|
|
81
|
+
return match hooks.execute_on_error(error_response).await {
|
|
82
|
+
Ok(resp) => Ok(resp),
|
|
83
|
+
Err(e) => Ok(axum::http::Response::builder()
|
|
84
|
+
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
85
|
+
.body(Body::from(format!("{{\"error\":\"onError hook failed: {}\"}}", e)))
|
|
86
|
+
.unwrap()),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
match hooks.execute_on_response(response).await {
|
|
92
|
+
Ok(resp) => Ok(resp),
|
|
93
|
+
Err(e) => Ok(axum::http::Response::builder()
|
|
94
|
+
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
95
|
+
.body(Body::from(format!("{{\"error\":\"onResponse hook failed: {}\"}}", e)))
|
|
96
|
+
.unwrap()),
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#[cfg(test)]
|
|
101
|
+
mod tests {
|
|
102
|
+
use super::*;
|
|
103
|
+
use crate::lifecycle::{HookResult, request_hook, response_hook};
|
|
104
|
+
use axum::http::{Request, Response, StatusCode};
|
|
105
|
+
use http_body_util::BodyExt;
|
|
106
|
+
use serde_json::json;
|
|
107
|
+
use std::collections::HashMap;
|
|
108
|
+
|
|
109
|
+
struct OkHandler;
|
|
110
|
+
|
|
111
|
+
impl Handler for OkHandler {
|
|
112
|
+
fn call(
|
|
113
|
+
&self,
|
|
114
|
+
_request: Request<Body>,
|
|
115
|
+
_request_data: crate::handler_trait::RequestData,
|
|
116
|
+
) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::handler_trait::HandlerResult> + Send + '_>>
|
|
117
|
+
{
|
|
118
|
+
Box::pin(async move {
|
|
119
|
+
Ok(Response::builder()
|
|
120
|
+
.status(StatusCode::OK)
|
|
121
|
+
.body(Body::from("ok"))
|
|
122
|
+
.unwrap())
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
struct ErrHandler;
|
|
128
|
+
|
|
129
|
+
impl Handler for ErrHandler {
|
|
130
|
+
fn call(
|
|
131
|
+
&self,
|
|
132
|
+
_request: Request<Body>,
|
|
133
|
+
_request_data: crate::handler_trait::RequestData,
|
|
134
|
+
) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::handler_trait::HandlerResult> + Send + '_>>
|
|
135
|
+
{
|
|
136
|
+
Box::pin(async move { Err((StatusCode::BAD_REQUEST, "bad".to_string())) })
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fn empty_request_data() -> crate::handler_trait::RequestData {
|
|
141
|
+
crate::handler_trait::RequestData {
|
|
142
|
+
path_params: std::sync::Arc::new(HashMap::new()),
|
|
143
|
+
query_params: json!({}),
|
|
144
|
+
validated_params: None,
|
|
145
|
+
raw_query_params: std::sync::Arc::new(HashMap::new()),
|
|
146
|
+
body: json!(null),
|
|
147
|
+
raw_body: None,
|
|
148
|
+
headers: std::sync::Arc::new(HashMap::new()),
|
|
149
|
+
cookies: std::sync::Arc::new(HashMap::new()),
|
|
150
|
+
method: "GET".to_string(),
|
|
151
|
+
path: "/".to_string(),
|
|
152
|
+
#[cfg(feature = "di")]
|
|
153
|
+
dependencies: None,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#[tokio::test]
|
|
158
|
+
async fn pre_validation_error_with_failing_on_error_hook_returns_fallback() {
|
|
159
|
+
let mut hooks = crate::LifecycleHooks::new();
|
|
160
|
+
hooks.add_pre_validation(request_hook("boom", |_req| async move { Err("boom".to_string()) }));
|
|
161
|
+
hooks.add_on_error(response_hook("fail-on-error", |_resp| async move {
|
|
162
|
+
Err("fail".to_string())
|
|
163
|
+
}));
|
|
164
|
+
|
|
165
|
+
let req = Request::builder().uri("/").body(Body::empty()).unwrap();
|
|
166
|
+
let resp = execute_with_lifecycle_hooks(req, empty_request_data(), Arc::new(OkHandler), Some(Arc::new(hooks)))
|
|
167
|
+
.await
|
|
168
|
+
.unwrap();
|
|
169
|
+
|
|
170
|
+
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
|
171
|
+
let body = resp.into_body().collect().await.unwrap().to_bytes();
|
|
172
|
+
assert_eq!(body.as_ref(), b"{\"error\":\"Hook execution failed\"}");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#[tokio::test]
|
|
176
|
+
async fn handler_error_with_failing_on_error_hook_returns_on_error_hook_failed_response() {
|
|
177
|
+
let mut hooks = crate::LifecycleHooks::new();
|
|
178
|
+
hooks.add_on_error(response_hook("fail-on-error", |_resp| async move {
|
|
179
|
+
Err("boom".to_string())
|
|
180
|
+
}));
|
|
181
|
+
|
|
182
|
+
let req = Request::builder().uri("/").body(Body::empty()).unwrap();
|
|
183
|
+
let resp = execute_with_lifecycle_hooks(req, empty_request_data(), Arc::new(ErrHandler), Some(Arc::new(hooks)))
|
|
184
|
+
.await
|
|
185
|
+
.unwrap();
|
|
186
|
+
|
|
187
|
+
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
|
188
|
+
let body = resp.into_body().collect().await.unwrap().to_bytes();
|
|
189
|
+
let body_str = std::str::from_utf8(body.as_ref()).unwrap();
|
|
190
|
+
assert!(body_str.contains("\"error\":\"onError hook failed:"));
|
|
191
|
+
assert!(body_str.contains("boom"));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
#[tokio::test]
|
|
195
|
+
async fn on_response_hook_error_returns_on_response_hook_failed_response() {
|
|
196
|
+
let mut hooks = crate::LifecycleHooks::new();
|
|
197
|
+
hooks.add_on_response(response_hook("fail-on-response", |_resp| async move {
|
|
198
|
+
Err("boom".to_string())
|
|
199
|
+
}));
|
|
200
|
+
|
|
201
|
+
let req = Request::builder().uri("/").body(Body::empty()).unwrap();
|
|
202
|
+
let resp = execute_with_lifecycle_hooks(req, empty_request_data(), Arc::new(OkHandler), Some(Arc::new(hooks)))
|
|
203
|
+
.await
|
|
204
|
+
.unwrap();
|
|
205
|
+
|
|
206
|
+
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
|
207
|
+
let body = resp.into_body().collect().await.unwrap().to_bytes();
|
|
208
|
+
let body_str = std::str::from_utf8(body.as_ref()).unwrap();
|
|
209
|
+
assert!(body_str.contains("\"error\":\"onResponse hook failed:"));
|
|
210
|
+
assert!(body_str.contains("boom"));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
#[tokio::test]
|
|
214
|
+
async fn pre_validation_short_circuit_skips_handler_and_returns_response() {
|
|
215
|
+
let mut hooks = crate::LifecycleHooks::new();
|
|
216
|
+
hooks.add_pre_validation(request_hook("short-circuit", |_req| async move {
|
|
217
|
+
Ok(HookResult::ShortCircuit(
|
|
218
|
+
Response::builder()
|
|
219
|
+
.status(StatusCode::UNAUTHORIZED)
|
|
220
|
+
.body(Body::from("nope"))
|
|
221
|
+
.unwrap(),
|
|
222
|
+
))
|
|
223
|
+
}));
|
|
224
|
+
|
|
225
|
+
let req = Request::builder().uri("/").body(Body::empty()).unwrap();
|
|
226
|
+
let resp = execute_with_lifecycle_hooks(req, empty_request_data(), Arc::new(ErrHandler), Some(Arc::new(hooks)))
|
|
227
|
+
.await
|
|
228
|
+
.unwrap();
|
|
229
|
+
|
|
230
|
+
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
|
|
231
|
+
let body = resp.into_body().collect().await.unwrap().to_bytes();
|
|
232
|
+
assert_eq!(body.as_ref(), b"nope");
|
|
233
|
+
}
|
|
234
|
+
}
|