spikard 0.4.0-x64-mingw-ucrt
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 +7 -0
- data/LICENSE +1 -0
- data/README.md +659 -0
- data/ext/spikard_rb/Cargo.toml +17 -0
- data/ext/spikard_rb/extconf.rb +10 -0
- data/ext/spikard_rb/src/lib.rs +6 -0
- data/lib/spikard/app.rb +405 -0
- data/lib/spikard/background.rb +27 -0
- data/lib/spikard/config.rb +396 -0
- data/lib/spikard/converters.rb +13 -0
- data/lib/spikard/handler_wrapper.rb +113 -0
- data/lib/spikard/provide.rb +214 -0
- data/lib/spikard/response.rb +173 -0
- data/lib/spikard/schema.rb +243 -0
- data/lib/spikard/sse.rb +111 -0
- data/lib/spikard/streaming_response.rb +44 -0
- data/lib/spikard/testing.rb +221 -0
- data/lib/spikard/upload_file.rb +131 -0
- data/lib/spikard/version.rb +5 -0
- data/lib/spikard/websocket.rb +59 -0
- data/lib/spikard.rb +43 -0
- data/sig/spikard.rbs +366 -0
- data/vendor/bundle/ruby/3.4.0/gems/diff-lcs-1.6.2/mise.toml +5 -0
- data/vendor/bundle/ruby/3.4.0/gems/rake-compiler-dock-1.10.0/build/buildkitd.toml +2 -0
- data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -0
- data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +139 -0
- data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +561 -0
- data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -0
- data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -0
- data/vendor/crates/spikard-bindings-shared/src/error_response.rs +403 -0
- data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +274 -0
- data/vendor/crates/spikard-bindings-shared/src/lib.rs +25 -0
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +298 -0
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +637 -0
- data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +309 -0
- data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -0
- data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +355 -0
- data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +502 -0
- data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +389 -0
- data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +413 -0
- data/vendor/crates/spikard-core/Cargo.toml +40 -0
- data/vendor/crates/spikard-core/src/bindings/mod.rs +3 -0
- data/vendor/crates/spikard-core/src/bindings/response.rs +133 -0
- data/vendor/crates/spikard-core/src/debug.rs +63 -0
- data/vendor/crates/spikard-core/src/di/container.rs +726 -0
- data/vendor/crates/spikard-core/src/di/dependency.rs +273 -0
- data/vendor/crates/spikard-core/src/di/error.rs +118 -0
- data/vendor/crates/spikard-core/src/di/factory.rs +538 -0
- data/vendor/crates/spikard-core/src/di/graph.rs +545 -0
- data/vendor/crates/spikard-core/src/di/mod.rs +192 -0
- data/vendor/crates/spikard-core/src/di/resolved.rs +411 -0
- data/vendor/crates/spikard-core/src/di/value.rs +283 -0
- data/vendor/crates/spikard-core/src/errors.rs +39 -0
- data/vendor/crates/spikard-core/src/http.rs +153 -0
- data/vendor/crates/spikard-core/src/lib.rs +29 -0
- data/vendor/crates/spikard-core/src/lifecycle.rs +422 -0
- data/vendor/crates/spikard-core/src/metadata.rs +397 -0
- data/vendor/crates/spikard-core/src/parameters.rs +723 -0
- data/vendor/crates/spikard-core/src/problem.rs +310 -0
- data/vendor/crates/spikard-core/src/request_data.rs +189 -0
- data/vendor/crates/spikard-core/src/router.rs +249 -0
- data/vendor/crates/spikard-core/src/schema_registry.rs +183 -0
- data/vendor/crates/spikard-core/src/type_hints.rs +304 -0
- data/vendor/crates/spikard-core/src/validation/error_mapper.rs +689 -0
- data/vendor/crates/spikard-core/src/validation/mod.rs +459 -0
- data/vendor/crates/spikard-http/Cargo.toml +58 -0
- data/vendor/crates/spikard-http/examples/sse-notifications.rs +147 -0
- data/vendor/crates/spikard-http/examples/websocket-chat.rs +91 -0
- data/vendor/crates/spikard-http/src/auth.rs +247 -0
- data/vendor/crates/spikard-http/src/background.rs +1562 -0
- data/vendor/crates/spikard-http/src/bindings/mod.rs +3 -0
- data/vendor/crates/spikard-http/src/bindings/response.rs +1 -0
- data/vendor/crates/spikard-http/src/body_metadata.rs +8 -0
- data/vendor/crates/spikard-http/src/cors.rs +490 -0
- data/vendor/crates/spikard-http/src/debug.rs +63 -0
- data/vendor/crates/spikard-http/src/di_handler.rs +1878 -0
- data/vendor/crates/spikard-http/src/handler_response.rs +532 -0
- data/vendor/crates/spikard-http/src/handler_trait.rs +861 -0
- data/vendor/crates/spikard-http/src/handler_trait_tests.rs +284 -0
- data/vendor/crates/spikard-http/src/lib.rs +524 -0
- data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +149 -0
- data/vendor/crates/spikard-http/src/lifecycle.rs +428 -0
- data/vendor/crates/spikard-http/src/middleware/mod.rs +285 -0
- data/vendor/crates/spikard-http/src/middleware/multipart.rs +930 -0
- data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +541 -0
- data/vendor/crates/spikard-http/src/middleware/validation.rs +287 -0
- data/vendor/crates/spikard-http/src/openapi/mod.rs +309 -0
- data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +535 -0
- data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +867 -0
- data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +678 -0
- data/vendor/crates/spikard-http/src/query_parser.rs +369 -0
- data/vendor/crates/spikard-http/src/response.rs +399 -0
- data/vendor/crates/spikard-http/src/server/handler.rs +1557 -0
- data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +98 -0
- data/vendor/crates/spikard-http/src/server/mod.rs +806 -0
- data/vendor/crates/spikard-http/src/server/request_extraction.rs +630 -0
- data/vendor/crates/spikard-http/src/server/routing_factory.rs +497 -0
- data/vendor/crates/spikard-http/src/sse.rs +961 -0
- data/vendor/crates/spikard-http/src/testing/form.rs +14 -0
- data/vendor/crates/spikard-http/src/testing/multipart.rs +60 -0
- data/vendor/crates/spikard-http/src/testing/test_client.rs +285 -0
- data/vendor/crates/spikard-http/src/testing.rs +377 -0
- data/vendor/crates/spikard-http/src/websocket.rs +831 -0
- data/vendor/crates/spikard-http/tests/background_behavior.rs +918 -0
- data/vendor/crates/spikard-http/tests/common/handlers.rs +308 -0
- data/vendor/crates/spikard-http/tests/common/mod.rs +21 -0
- data/vendor/crates/spikard-http/tests/di_integration.rs +202 -0
- data/vendor/crates/spikard-http/tests/doc_snippets.rs +4 -0
- data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1135 -0
- data/vendor/crates/spikard-http/tests/multipart_behavior.rs +688 -0
- data/vendor/crates/spikard-http/tests/server_config_builder.rs +324 -0
- data/vendor/crates/spikard-http/tests/sse_behavior.rs +728 -0
- data/vendor/crates/spikard-http/tests/websocket_behavior.rs +724 -0
- data/vendor/crates/spikard-rb/Cargo.toml +43 -0
- data/vendor/crates/spikard-rb/build.rs +199 -0
- data/vendor/crates/spikard-rb/src/background.rs +63 -0
- data/vendor/crates/spikard-rb/src/config/mod.rs +5 -0
- data/vendor/crates/spikard-rb/src/config/server_config.rs +283 -0
- data/vendor/crates/spikard-rb/src/conversion.rs +459 -0
- data/vendor/crates/spikard-rb/src/di/builder.rs +105 -0
- data/vendor/crates/spikard-rb/src/di/mod.rs +413 -0
- data/vendor/crates/spikard-rb/src/handler.rs +612 -0
- data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -0
- data/vendor/crates/spikard-rb/src/lib.rs +1857 -0
- data/vendor/crates/spikard-rb/src/lifecycle.rs +275 -0
- data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -0
- data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +427 -0
- data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -0
- data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +326 -0
- data/vendor/crates/spikard-rb/src/server.rs +283 -0
- data/vendor/crates/spikard-rb/src/sse.rs +231 -0
- data/vendor/crates/spikard-rb/src/testing/client.rs +404 -0
- data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -0
- data/vendor/crates/spikard-rb/src/testing/sse.rs +143 -0
- data/vendor/crates/spikard-rb/src/testing/websocket.rs +221 -0
- data/vendor/crates/spikard-rb/src/websocket.rs +233 -0
- data/vendor/crates/spikard-rb/tests/magnus_ffi_tests.rs +14 -0
- metadata +213 -0
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
//! Shared lifecycle hook executor infrastructure
|
|
2
|
+
//!
|
|
3
|
+
//! This module provides a language-agnostic abstraction for executing lifecycle hooks.
|
|
4
|
+
//! It extracts ~960 lines of duplicated logic from Python, Node.js, Ruby, and PHP bindings
|
|
5
|
+
//! into reusable trait-based components.
|
|
6
|
+
//!
|
|
7
|
+
//! # Design
|
|
8
|
+
//!
|
|
9
|
+
//! The executor uses a trait-based pattern where language bindings implement
|
|
10
|
+
//! `LanguageLifecycleHook` to provide language-specific hook invocation, while
|
|
11
|
+
//! `LifecycleExecutor` handles the common logic:
|
|
12
|
+
//!
|
|
13
|
+
//! - Hook result type handling (Continue vs ShortCircuit)
|
|
14
|
+
//! - Response/Request building from hook results
|
|
15
|
+
//! - Error handling and conversion
|
|
16
|
+
//!
|
|
17
|
+
//! # Example
|
|
18
|
+
//!
|
|
19
|
+
//! ```ignore
|
|
20
|
+
//! struct MyLanguageHook { ... }
|
|
21
|
+
//!
|
|
22
|
+
//! impl LanguageLifecycleHook for MyLanguageHook {
|
|
23
|
+
//! fn prepare_hook_data(&self, req: &Request<Body>) -> Result<Self::HookData, String> {
|
|
24
|
+
//! // Convert Request<Body> to language-specific representation
|
|
25
|
+
//! }
|
|
26
|
+
//!
|
|
27
|
+
//! async fn invoke_hook(&self, data: Self::HookData) -> Result<HookResultData, String> {
|
|
28
|
+
//! // Call language function and return structured result
|
|
29
|
+
//! }
|
|
30
|
+
//! }
|
|
31
|
+
//! ```
|
|
32
|
+
|
|
33
|
+
use axum::{
|
|
34
|
+
body::Body,
|
|
35
|
+
http::{Request, Response, StatusCode},
|
|
36
|
+
};
|
|
37
|
+
use std::collections::HashMap;
|
|
38
|
+
use std::future::Future;
|
|
39
|
+
use std::pin::Pin;
|
|
40
|
+
use std::sync::Arc;
|
|
41
|
+
|
|
42
|
+
/// Data returned from language-specific hook invocation
|
|
43
|
+
///
|
|
44
|
+
/// This is a normalized representation of hook results that abstracts away
|
|
45
|
+
/// language-specific details. Bindings convert their native hook results
|
|
46
|
+
/// into this common format.
|
|
47
|
+
#[derive(Debug, Clone)]
|
|
48
|
+
pub struct HookResultData {
|
|
49
|
+
/// Whether to continue execution (true) or short-circuit (false)
|
|
50
|
+
pub continue_execution: bool,
|
|
51
|
+
/// Optional status code for short-circuit responses
|
|
52
|
+
pub status_code: Option<u16>,
|
|
53
|
+
/// Optional headers to include in response
|
|
54
|
+
pub headers: Option<HashMap<String, String>>,
|
|
55
|
+
/// Optional body bytes for response
|
|
56
|
+
pub body: Option<Vec<u8>>,
|
|
57
|
+
/// Optional request modifications (method, path, headers, body)
|
|
58
|
+
pub request_modifications: Option<RequestModifications>,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Modifications to apply to a request
|
|
62
|
+
#[derive(Debug, Clone)]
|
|
63
|
+
pub struct RequestModifications {
|
|
64
|
+
/// New HTTP method (e.g., "GET", "POST")
|
|
65
|
+
pub method: Option<String>,
|
|
66
|
+
/// New request path
|
|
67
|
+
pub path: Option<String>,
|
|
68
|
+
/// New or updated headers
|
|
69
|
+
pub headers: Option<HashMap<String, String>>,
|
|
70
|
+
/// New request body
|
|
71
|
+
pub body: Option<Vec<u8>>,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
impl HookResultData {
|
|
75
|
+
/// Create a Continue result (pass through)
|
|
76
|
+
pub fn continue_execution() -> Self {
|
|
77
|
+
Self {
|
|
78
|
+
continue_execution: true,
|
|
79
|
+
status_code: None,
|
|
80
|
+
headers: None,
|
|
81
|
+
body: None,
|
|
82
|
+
request_modifications: None,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Create a short-circuit response result
|
|
87
|
+
pub fn short_circuit(status_code: u16, body: Vec<u8>, headers: Option<HashMap<String, String>>) -> Self {
|
|
88
|
+
Self {
|
|
89
|
+
continue_execution: false,
|
|
90
|
+
status_code: Some(status_code),
|
|
91
|
+
headers,
|
|
92
|
+
body: Some(body),
|
|
93
|
+
request_modifications: None,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Create a request modification result
|
|
98
|
+
pub fn modify_request(modifications: RequestModifications) -> Self {
|
|
99
|
+
Self {
|
|
100
|
+
continue_execution: true,
|
|
101
|
+
status_code: None,
|
|
102
|
+
headers: None,
|
|
103
|
+
body: None,
|
|
104
|
+
request_modifications: Some(modifications),
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// Trait for language-specific lifecycle hook implementations
|
|
110
|
+
///
|
|
111
|
+
/// Each language binding implements this trait to provide language-specific
|
|
112
|
+
/// hook invocation while delegating common logic to `LifecycleExecutor`.
|
|
113
|
+
pub trait LanguageLifecycleHook: Send + Sync {
|
|
114
|
+
/// Language-specific hook data type
|
|
115
|
+
type HookData: Send;
|
|
116
|
+
|
|
117
|
+
/// Prepare hook data from the incoming request/response
|
|
118
|
+
///
|
|
119
|
+
/// This should convert axum HTTP types to language-specific representations.
|
|
120
|
+
fn prepare_hook_data(&self, req: &Request<Body>) -> Result<Self::HookData, String>;
|
|
121
|
+
|
|
122
|
+
/// Invoke the language hook and return normalized result data
|
|
123
|
+
///
|
|
124
|
+
/// This should call the language function and convert its result to `HookResultData`.
|
|
125
|
+
fn invoke_hook(&self, data: Self::HookData)
|
|
126
|
+
-> Pin<Box<dyn Future<Output = Result<HookResultData, String>> + Send>>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// Executor that handles common lifecycle hook logic
|
|
130
|
+
///
|
|
131
|
+
/// This executor is generic over any language binding that implements
|
|
132
|
+
/// `LanguageLifecycleHook`. It provides common logic for:
|
|
133
|
+
/// - Executing request hooks and handling short-circuits
|
|
134
|
+
/// - Executing response hooks and building modified responses
|
|
135
|
+
/// - Converting hook results to axum Request/Response types
|
|
136
|
+
pub struct LifecycleExecutor<L: LanguageLifecycleHook> {
|
|
137
|
+
hook: Arc<L>,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
impl<L: LanguageLifecycleHook> LifecycleExecutor<L> {
|
|
141
|
+
/// Create a new executor for the given hook
|
|
142
|
+
pub fn new(hook: Arc<L>) -> Self {
|
|
143
|
+
Self { hook }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// Execute a request hook, handling Continue/ShortCircuit semantics
|
|
147
|
+
///
|
|
148
|
+
/// Returns either the modified request or a short-circuit response.
|
|
149
|
+
pub async fn execute_request_hook(
|
|
150
|
+
&self,
|
|
151
|
+
req: Request<Body>,
|
|
152
|
+
) -> Result<Result<Request<Body>, Response<Body>>, String> {
|
|
153
|
+
let hook_data = self.hook.prepare_hook_data(&req)?;
|
|
154
|
+
let result = self.hook.invoke_hook(hook_data).await?;
|
|
155
|
+
|
|
156
|
+
if !result.continue_execution {
|
|
157
|
+
// Short-circuit: build response and return it
|
|
158
|
+
let response = self.build_response_from_hook_result(&result)?;
|
|
159
|
+
return Ok(Err(response));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Continue: optionally modify request
|
|
163
|
+
if let Some(modifications) = result.request_modifications {
|
|
164
|
+
let modified_req = self.apply_request_modifications(req, modifications)?;
|
|
165
|
+
Ok(Ok(modified_req))
|
|
166
|
+
} else {
|
|
167
|
+
Ok(Ok(req))
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// Execute a response hook, handling response modification
|
|
172
|
+
///
|
|
173
|
+
/// Response hooks can only continue or modify the response,
|
|
174
|
+
/// never short-circuit.
|
|
175
|
+
pub async fn execute_response_hook(&self, resp: Response<Body>) -> Result<Response<Body>, String> {
|
|
176
|
+
// For response hooks, we typically don't have hook_data to prepare
|
|
177
|
+
// since response hooks operate on already-generated responses.
|
|
178
|
+
// This is a simplified version; language bindings may override.
|
|
179
|
+
let (parts, body) = resp.into_parts();
|
|
180
|
+
let body_bytes = extract_body(body).await?;
|
|
181
|
+
|
|
182
|
+
// Rebuild request as a dummy for prepare_hook_data
|
|
183
|
+
// (Language bindings override this behavior as needed)
|
|
184
|
+
let dummy_req = Request::builder()
|
|
185
|
+
.method("GET")
|
|
186
|
+
.uri("/")
|
|
187
|
+
.body(Body::empty())
|
|
188
|
+
.map_err(|e| format!("Failed to build dummy request: {}", e))?;
|
|
189
|
+
|
|
190
|
+
let hook_data = self.hook.prepare_hook_data(&dummy_req)?;
|
|
191
|
+
let result = self.hook.invoke_hook(hook_data).await?;
|
|
192
|
+
|
|
193
|
+
// Apply modifications to response if provided
|
|
194
|
+
if let Some(modifications) = result.request_modifications {
|
|
195
|
+
// For response hooks, interpret modifications as response body/headers changes
|
|
196
|
+
let mut builder = Response::builder().status(parts.status);
|
|
197
|
+
|
|
198
|
+
// Collect header mod keys for later comparison
|
|
199
|
+
let header_mod_keys: Vec<String> = modifications
|
|
200
|
+
.headers
|
|
201
|
+
.as_ref()
|
|
202
|
+
.map(|mods| mods.keys().map(|k| k.to_lowercase()).collect())
|
|
203
|
+
.unwrap_or_default();
|
|
204
|
+
|
|
205
|
+
// Apply header modifications
|
|
206
|
+
if let Some(header_mods) = modifications.headers {
|
|
207
|
+
for (key, value) in header_mods {
|
|
208
|
+
builder = builder.header(&key, &value);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Copy original headers not overridden
|
|
213
|
+
for (name, value) in parts.headers.iter() {
|
|
214
|
+
let key_str = name.as_str().to_lowercase();
|
|
215
|
+
// Skip if this header was in modifications
|
|
216
|
+
if !header_mod_keys.contains(&key_str) {
|
|
217
|
+
builder = builder.header(name, value);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let body = modifications.body.unwrap_or(body_bytes);
|
|
222
|
+
return builder
|
|
223
|
+
.body(Body::from(body))
|
|
224
|
+
.map_err(|e| format!("Failed to build modified response: {}", e));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Rebuild original response
|
|
228
|
+
let mut builder = Response::builder().status(parts.status);
|
|
229
|
+
for (name, value) in parts.headers {
|
|
230
|
+
if let Some(name) = name {
|
|
231
|
+
builder = builder.header(name, value);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
builder
|
|
235
|
+
.body(Body::from(body_bytes))
|
|
236
|
+
.map_err(|e| format!("Failed to rebuild response: {}", e))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/// Build an axum Response from hook result data
|
|
240
|
+
fn build_response_from_hook_result(&self, result: &HookResultData) -> Result<Response<Body>, String> {
|
|
241
|
+
let status_code = result.status_code.unwrap_or(200);
|
|
242
|
+
let status =
|
|
243
|
+
StatusCode::from_u16(status_code).map_err(|e| format!("Invalid status code {}: {}", status_code, e))?;
|
|
244
|
+
|
|
245
|
+
let mut builder = Response::builder().status(status);
|
|
246
|
+
|
|
247
|
+
// Add headers from hook result
|
|
248
|
+
if let Some(ref headers) = result.headers {
|
|
249
|
+
for (key, value) in headers {
|
|
250
|
+
builder = builder.header(key, value);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Ensure content-type if not specified
|
|
255
|
+
if !builder
|
|
256
|
+
.headers_ref()
|
|
257
|
+
.map(|h| h.contains_key("content-type"))
|
|
258
|
+
.unwrap_or(false)
|
|
259
|
+
{
|
|
260
|
+
builder = builder.header("content-type", "application/json");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let body = result.body.clone().unwrap_or_else(|| b"{}".to_vec());
|
|
264
|
+
|
|
265
|
+
builder
|
|
266
|
+
.body(Body::from(body))
|
|
267
|
+
.map_err(|e| format!("Failed to build response: {}", e))
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/// Apply request modifications to a request
|
|
271
|
+
fn apply_request_modifications(
|
|
272
|
+
&self,
|
|
273
|
+
req: Request<Body>,
|
|
274
|
+
mods: RequestModifications,
|
|
275
|
+
) -> Result<Request<Body>, String> {
|
|
276
|
+
let (mut parts, body) = req.into_parts();
|
|
277
|
+
|
|
278
|
+
// Update method if provided
|
|
279
|
+
if let Some(method) = &mods.method {
|
|
280
|
+
parts.method = method
|
|
281
|
+
.parse()
|
|
282
|
+
.map_err(|e| format!("Invalid method '{}': {}", method, e))?;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Update URI if provided
|
|
286
|
+
if let Some(path) = &mods.path {
|
|
287
|
+
parts.uri = path.parse().map_err(|e| format!("Invalid path '{}': {}", path, e))?;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Update/add headers
|
|
291
|
+
if let Some(new_headers) = &mods.headers {
|
|
292
|
+
for (key, value) in new_headers {
|
|
293
|
+
let header_name: http::header::HeaderName =
|
|
294
|
+
key.parse().map_err(|_| format!("Invalid header name: {}", key))?;
|
|
295
|
+
let header_value: http::header::HeaderValue = value
|
|
296
|
+
.parse()
|
|
297
|
+
.map_err(|_| format!("Invalid header value for {}: {}", key, value))?;
|
|
298
|
+
parts.headers.insert(header_name, header_value);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Update body if provided
|
|
303
|
+
let body = if let Some(new_body) = mods.body {
|
|
304
|
+
Body::from(new_body)
|
|
305
|
+
} else {
|
|
306
|
+
body
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
Ok(Request::from_parts(parts, body))
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/// Extract body bytes from an axum Body
|
|
314
|
+
///
|
|
315
|
+
/// This is a helper used by lifecycle executors to read response bodies.
|
|
316
|
+
pub async fn extract_body(body: Body) -> Result<Vec<u8>, String> {
|
|
317
|
+
use http_body_util::BodyExt;
|
|
318
|
+
|
|
319
|
+
let bytes = body
|
|
320
|
+
.collect()
|
|
321
|
+
.await
|
|
322
|
+
.map_err(|e| format!("Failed to read body: {}", e))?
|
|
323
|
+
.to_bytes();
|
|
324
|
+
Ok(bytes.to_vec())
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
#[cfg(test)]
|
|
328
|
+
mod tests {
|
|
329
|
+
use super::*;
|
|
330
|
+
|
|
331
|
+
#[test]
|
|
332
|
+
fn test_hook_result_data_continue() {
|
|
333
|
+
let result = HookResultData::continue_execution();
|
|
334
|
+
assert!(result.continue_execution);
|
|
335
|
+
assert_eq!(result.status_code, None);
|
|
336
|
+
assert_eq!(result.body, None);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
#[test]
|
|
340
|
+
fn test_hook_result_data_short_circuit() {
|
|
341
|
+
let body = b"error".to_vec();
|
|
342
|
+
let mut headers = HashMap::new();
|
|
343
|
+
headers.insert("x-error".to_string(), "true".to_string());
|
|
344
|
+
|
|
345
|
+
let result = HookResultData::short_circuit(400, body.clone(), Some(headers.clone()));
|
|
346
|
+
assert!(!result.continue_execution);
|
|
347
|
+
assert_eq!(result.status_code, Some(400));
|
|
348
|
+
assert_eq!(result.body, Some(body));
|
|
349
|
+
assert_eq!(result.headers, Some(headers));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
#[test]
|
|
353
|
+
fn test_hook_result_data_modify_request() {
|
|
354
|
+
let mods = RequestModifications {
|
|
355
|
+
method: Some("POST".to_string()),
|
|
356
|
+
path: Some("/new-path".to_string()),
|
|
357
|
+
headers: None,
|
|
358
|
+
body: None,
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
let result = HookResultData::modify_request(mods.clone());
|
|
362
|
+
assert!(result.continue_execution);
|
|
363
|
+
assert_eq!(result.status_code, None);
|
|
364
|
+
assert_eq!(
|
|
365
|
+
result.request_modifications.as_ref().unwrap().method,
|
|
366
|
+
Some("POST".to_string())
|
|
367
|
+
);
|
|
368
|
+
assert_eq!(
|
|
369
|
+
result.request_modifications.as_ref().unwrap().path,
|
|
370
|
+
Some("/new-path".to_string())
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
#[test]
|
|
375
|
+
fn test_request_modifications_creation() {
|
|
376
|
+
let mods = RequestModifications {
|
|
377
|
+
method: Some("PUT".to_string()),
|
|
378
|
+
path: Some("/api/resource".to_string()),
|
|
379
|
+
headers: None,
|
|
380
|
+
body: Some(b"data".to_vec()),
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
assert_eq!(mods.method, Some("PUT".to_string()));
|
|
384
|
+
assert_eq!(mods.path, Some("/api/resource".to_string()));
|
|
385
|
+
assert_eq!(mods.body, Some(b"data".to_vec()));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Mock hook implementation for testing
|
|
389
|
+
struct MockHook {
|
|
390
|
+
result: HookResultData,
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
impl LanguageLifecycleHook for MockHook {
|
|
394
|
+
type HookData = ();
|
|
395
|
+
|
|
396
|
+
fn prepare_hook_data(&self, _req: &Request<Body>) -> Result<Self::HookData, String> {
|
|
397
|
+
Ok(())
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
fn invoke_hook(
|
|
401
|
+
&self,
|
|
402
|
+
_data: Self::HookData,
|
|
403
|
+
) -> Pin<Box<dyn Future<Output = Result<HookResultData, String>> + Send>> {
|
|
404
|
+
let result = self.result.clone();
|
|
405
|
+
Box::pin(async move { Ok(result) })
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
#[tokio::test]
|
|
410
|
+
async fn test_execute_request_hook_continue() {
|
|
411
|
+
let hook = Arc::new(MockHook {
|
|
412
|
+
result: HookResultData::continue_execution(),
|
|
413
|
+
});
|
|
414
|
+
let executor = LifecycleExecutor::new(hook);
|
|
415
|
+
|
|
416
|
+
let req = Request::builder().body(Body::empty()).unwrap();
|
|
417
|
+
let result = executor.execute_request_hook(req).await.unwrap();
|
|
418
|
+
|
|
419
|
+
assert!(result.is_ok());
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
#[tokio::test]
|
|
423
|
+
async fn test_execute_request_hook_short_circuit() {
|
|
424
|
+
let hook = Arc::new(MockHook {
|
|
425
|
+
result: HookResultData::short_circuit(403, b"Forbidden".to_vec(), None),
|
|
426
|
+
});
|
|
427
|
+
let executor = LifecycleExecutor::new(hook);
|
|
428
|
+
|
|
429
|
+
let req = Request::builder().body(Body::empty()).unwrap();
|
|
430
|
+
let result = executor.execute_request_hook(req).await.unwrap();
|
|
431
|
+
|
|
432
|
+
assert!(result.is_err());
|
|
433
|
+
let response = result.unwrap_err();
|
|
434
|
+
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
#[tokio::test]
|
|
438
|
+
async fn test_execute_request_hook_modify_request() {
|
|
439
|
+
let mods = RequestModifications {
|
|
440
|
+
method: Some("POST".to_string()),
|
|
441
|
+
path: Some("/new-path".to_string()),
|
|
442
|
+
headers: None,
|
|
443
|
+
body: Some(b"new body".to_vec()),
|
|
444
|
+
};
|
|
445
|
+
let hook = Arc::new(MockHook {
|
|
446
|
+
result: HookResultData::modify_request(mods),
|
|
447
|
+
});
|
|
448
|
+
let executor = LifecycleExecutor::new(hook);
|
|
449
|
+
|
|
450
|
+
let req = Request::builder()
|
|
451
|
+
.method("GET")
|
|
452
|
+
.uri("/old-path")
|
|
453
|
+
.body(Body::empty())
|
|
454
|
+
.unwrap();
|
|
455
|
+
let result = executor.execute_request_hook(req).await.unwrap();
|
|
456
|
+
|
|
457
|
+
assert!(result.is_ok());
|
|
458
|
+
let modified_req = result.unwrap();
|
|
459
|
+
assert_eq!(modified_req.method(), "POST");
|
|
460
|
+
assert_eq!(modified_req.uri().path(), "/new-path");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
#[tokio::test]
|
|
464
|
+
async fn test_execute_response_hook_with_modifications() {
|
|
465
|
+
let mods = RequestModifications {
|
|
466
|
+
method: None,
|
|
467
|
+
path: None,
|
|
468
|
+
headers: Some({
|
|
469
|
+
let mut h = HashMap::new();
|
|
470
|
+
h.insert("X-Modified".to_string(), "true".to_string());
|
|
471
|
+
h
|
|
472
|
+
}),
|
|
473
|
+
body: Some(b"modified response".to_vec()),
|
|
474
|
+
};
|
|
475
|
+
let hook = Arc::new(MockHook {
|
|
476
|
+
result: HookResultData::modify_request(mods),
|
|
477
|
+
});
|
|
478
|
+
let executor = LifecycleExecutor::new(hook);
|
|
479
|
+
|
|
480
|
+
let resp = Response::builder().status(200).body(Body::from("original")).unwrap();
|
|
481
|
+
let result = executor.execute_response_hook(resp).await.unwrap();
|
|
482
|
+
|
|
483
|
+
assert_eq!(result.status(), StatusCode::OK);
|
|
484
|
+
assert_eq!(result.headers().get("X-Modified").unwrap().to_str().unwrap(), "true");
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
#[tokio::test]
|
|
488
|
+
async fn test_build_response_from_hook_result_with_headers() {
|
|
489
|
+
let mut headers = HashMap::new();
|
|
490
|
+
headers.insert("X-Custom".to_string(), "value".to_string());
|
|
491
|
+
|
|
492
|
+
let result = HookResultData::short_circuit(201, b"Created".to_vec(), Some(headers));
|
|
493
|
+
let hook = Arc::new(MockHook {
|
|
494
|
+
result: HookResultData::continue_execution(),
|
|
495
|
+
});
|
|
496
|
+
let executor = LifecycleExecutor::new(hook);
|
|
497
|
+
|
|
498
|
+
let response = executor.build_response_from_hook_result(&result).unwrap();
|
|
499
|
+
assert_eq!(response.status(), StatusCode::CREATED);
|
|
500
|
+
assert_eq!(response.headers().get("X-Custom").unwrap().to_str().unwrap(), "value");
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
#[tokio::test]
|
|
504
|
+
async fn test_build_response_from_hook_result_default_content_type() {
|
|
505
|
+
let result = HookResultData::short_circuit(200, b"{}".to_vec(), None);
|
|
506
|
+
let hook = Arc::new(MockHook {
|
|
507
|
+
result: HookResultData::continue_execution(),
|
|
508
|
+
});
|
|
509
|
+
let executor = LifecycleExecutor::new(hook);
|
|
510
|
+
|
|
511
|
+
let response = executor.build_response_from_hook_result(&result).unwrap();
|
|
512
|
+
assert_eq!(
|
|
513
|
+
response.headers().get("content-type").unwrap().to_str().unwrap(),
|
|
514
|
+
"application/json"
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
#[tokio::test]
|
|
519
|
+
async fn test_apply_request_modifications_method() {
|
|
520
|
+
let mods = RequestModifications {
|
|
521
|
+
method: Some("PATCH".to_string()),
|
|
522
|
+
path: None,
|
|
523
|
+
headers: None,
|
|
524
|
+
body: None,
|
|
525
|
+
};
|
|
526
|
+
let hook = Arc::new(MockHook {
|
|
527
|
+
result: HookResultData::continue_execution(),
|
|
528
|
+
});
|
|
529
|
+
let executor = LifecycleExecutor::new(hook);
|
|
530
|
+
|
|
531
|
+
let req = Request::builder().method("GET").body(Body::empty()).unwrap();
|
|
532
|
+
let modified = executor.apply_request_modifications(req, mods).unwrap();
|
|
533
|
+
|
|
534
|
+
assert_eq!(modified.method(), "PATCH");
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
#[tokio::test]
|
|
538
|
+
async fn test_apply_request_modifications_path() {
|
|
539
|
+
let mods = RequestModifications {
|
|
540
|
+
method: None,
|
|
541
|
+
path: Some("/api/v2/users".to_string()),
|
|
542
|
+
headers: None,
|
|
543
|
+
body: None,
|
|
544
|
+
};
|
|
545
|
+
let hook = Arc::new(MockHook {
|
|
546
|
+
result: HookResultData::continue_execution(),
|
|
547
|
+
});
|
|
548
|
+
let executor = LifecycleExecutor::new(hook);
|
|
549
|
+
|
|
550
|
+
let req = Request::builder().uri("/api/v1/users").body(Body::empty()).unwrap();
|
|
551
|
+
let modified = executor.apply_request_modifications(req, mods).unwrap();
|
|
552
|
+
|
|
553
|
+
assert_eq!(modified.uri().path(), "/api/v2/users");
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
#[tokio::test]
|
|
557
|
+
async fn test_apply_request_modifications_headers() {
|
|
558
|
+
let mut new_headers = HashMap::new();
|
|
559
|
+
new_headers.insert("Authorization".to_string(), "Bearer token".to_string());
|
|
560
|
+
|
|
561
|
+
let mods = RequestModifications {
|
|
562
|
+
method: None,
|
|
563
|
+
path: None,
|
|
564
|
+
headers: Some(new_headers),
|
|
565
|
+
body: None,
|
|
566
|
+
};
|
|
567
|
+
let hook = Arc::new(MockHook {
|
|
568
|
+
result: HookResultData::continue_execution(),
|
|
569
|
+
});
|
|
570
|
+
let executor = LifecycleExecutor::new(hook);
|
|
571
|
+
|
|
572
|
+
let req = Request::builder().body(Body::empty()).unwrap();
|
|
573
|
+
let modified = executor.apply_request_modifications(req, mods).unwrap();
|
|
574
|
+
|
|
575
|
+
assert_eq!(
|
|
576
|
+
modified.headers().get("Authorization").unwrap().to_str().unwrap(),
|
|
577
|
+
"Bearer token"
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
#[tokio::test]
|
|
582
|
+
async fn test_apply_request_modifications_body() {
|
|
583
|
+
let new_body = b"modified body".to_vec();
|
|
584
|
+
let mods = RequestModifications {
|
|
585
|
+
method: None,
|
|
586
|
+
path: None,
|
|
587
|
+
headers: None,
|
|
588
|
+
body: Some(new_body.clone()),
|
|
589
|
+
};
|
|
590
|
+
let hook = Arc::new(MockHook {
|
|
591
|
+
result: HookResultData::continue_execution(),
|
|
592
|
+
});
|
|
593
|
+
let executor = LifecycleExecutor::new(hook);
|
|
594
|
+
|
|
595
|
+
let req = Request::builder().body(Body::from("original body")).unwrap();
|
|
596
|
+
let modified = executor.apply_request_modifications(req, mods).unwrap();
|
|
597
|
+
|
|
598
|
+
let body_bytes = extract_body(modified.into_body()).await.unwrap();
|
|
599
|
+
assert_eq!(body_bytes, new_body);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
#[tokio::test]
|
|
603
|
+
async fn test_apply_request_modifications_invalid_method() {
|
|
604
|
+
let mods = RequestModifications {
|
|
605
|
+
method: Some("".to_string()),
|
|
606
|
+
path: None,
|
|
607
|
+
headers: None,
|
|
608
|
+
body: None,
|
|
609
|
+
};
|
|
610
|
+
let hook = Arc::new(MockHook {
|
|
611
|
+
result: HookResultData::continue_execution(),
|
|
612
|
+
});
|
|
613
|
+
let executor = LifecycleExecutor::new(hook);
|
|
614
|
+
|
|
615
|
+
let req = Request::builder().body(Body::empty()).unwrap();
|
|
616
|
+
let result = executor.apply_request_modifications(req, mods);
|
|
617
|
+
|
|
618
|
+
assert!(result.is_err());
|
|
619
|
+
assert!(result.unwrap_err().contains("Invalid method"));
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
#[tokio::test]
|
|
623
|
+
async fn test_extract_body_helper() {
|
|
624
|
+
let body = Body::from("test data");
|
|
625
|
+
let bytes = extract_body(body).await.unwrap();
|
|
626
|
+
|
|
627
|
+
assert_eq!(bytes, b"test data");
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
#[tokio::test]
|
|
631
|
+
async fn test_extract_body_empty() {
|
|
632
|
+
let body = Body::empty();
|
|
633
|
+
let bytes = extract_body(body).await.unwrap();
|
|
634
|
+
|
|
635
|
+
assert_eq!(bytes.len(), 0);
|
|
636
|
+
}
|
|
637
|
+
}
|