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.
Files changed (138) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +1 -0
  3. data/README.md +659 -0
  4. data/ext/spikard_rb/Cargo.toml +17 -0
  5. data/ext/spikard_rb/extconf.rb +10 -0
  6. data/ext/spikard_rb/src/lib.rs +6 -0
  7. data/lib/spikard/app.rb +405 -0
  8. data/lib/spikard/background.rb +27 -0
  9. data/lib/spikard/config.rb +396 -0
  10. data/lib/spikard/converters.rb +13 -0
  11. data/lib/spikard/handler_wrapper.rb +113 -0
  12. data/lib/spikard/provide.rb +214 -0
  13. data/lib/spikard/response.rb +173 -0
  14. data/lib/spikard/schema.rb +243 -0
  15. data/lib/spikard/sse.rb +111 -0
  16. data/lib/spikard/streaming_response.rb +44 -0
  17. data/lib/spikard/testing.rb +221 -0
  18. data/lib/spikard/upload_file.rb +131 -0
  19. data/lib/spikard/version.rb +5 -0
  20. data/lib/spikard/websocket.rb +59 -0
  21. data/lib/spikard.rb +43 -0
  22. data/sig/spikard.rbs +366 -0
  23. data/vendor/bundle/ruby/3.4.0/gems/diff-lcs-1.6.2/mise.toml +5 -0
  24. data/vendor/bundle/ruby/3.4.0/gems/rake-compiler-dock-1.10.0/build/buildkitd.toml +2 -0
  25. data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -0
  26. data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +139 -0
  27. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +561 -0
  28. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -0
  29. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -0
  30. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +403 -0
  31. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +274 -0
  32. data/vendor/crates/spikard-bindings-shared/src/lib.rs +25 -0
  33. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +298 -0
  34. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +637 -0
  35. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +309 -0
  36. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -0
  37. data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +355 -0
  38. data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +502 -0
  39. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +389 -0
  40. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +413 -0
  41. data/vendor/crates/spikard-core/Cargo.toml +40 -0
  42. data/vendor/crates/spikard-core/src/bindings/mod.rs +3 -0
  43. data/vendor/crates/spikard-core/src/bindings/response.rs +133 -0
  44. data/vendor/crates/spikard-core/src/debug.rs +63 -0
  45. data/vendor/crates/spikard-core/src/di/container.rs +726 -0
  46. data/vendor/crates/spikard-core/src/di/dependency.rs +273 -0
  47. data/vendor/crates/spikard-core/src/di/error.rs +118 -0
  48. data/vendor/crates/spikard-core/src/di/factory.rs +538 -0
  49. data/vendor/crates/spikard-core/src/di/graph.rs +545 -0
  50. data/vendor/crates/spikard-core/src/di/mod.rs +192 -0
  51. data/vendor/crates/spikard-core/src/di/resolved.rs +411 -0
  52. data/vendor/crates/spikard-core/src/di/value.rs +283 -0
  53. data/vendor/crates/spikard-core/src/errors.rs +39 -0
  54. data/vendor/crates/spikard-core/src/http.rs +153 -0
  55. data/vendor/crates/spikard-core/src/lib.rs +29 -0
  56. data/vendor/crates/spikard-core/src/lifecycle.rs +422 -0
  57. data/vendor/crates/spikard-core/src/metadata.rs +397 -0
  58. data/vendor/crates/spikard-core/src/parameters.rs +723 -0
  59. data/vendor/crates/spikard-core/src/problem.rs +310 -0
  60. data/vendor/crates/spikard-core/src/request_data.rs +189 -0
  61. data/vendor/crates/spikard-core/src/router.rs +249 -0
  62. data/vendor/crates/spikard-core/src/schema_registry.rs +183 -0
  63. data/vendor/crates/spikard-core/src/type_hints.rs +304 -0
  64. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +689 -0
  65. data/vendor/crates/spikard-core/src/validation/mod.rs +459 -0
  66. data/vendor/crates/spikard-http/Cargo.toml +58 -0
  67. data/vendor/crates/spikard-http/examples/sse-notifications.rs +147 -0
  68. data/vendor/crates/spikard-http/examples/websocket-chat.rs +91 -0
  69. data/vendor/crates/spikard-http/src/auth.rs +247 -0
  70. data/vendor/crates/spikard-http/src/background.rs +1562 -0
  71. data/vendor/crates/spikard-http/src/bindings/mod.rs +3 -0
  72. data/vendor/crates/spikard-http/src/bindings/response.rs +1 -0
  73. data/vendor/crates/spikard-http/src/body_metadata.rs +8 -0
  74. data/vendor/crates/spikard-http/src/cors.rs +490 -0
  75. data/vendor/crates/spikard-http/src/debug.rs +63 -0
  76. data/vendor/crates/spikard-http/src/di_handler.rs +1878 -0
  77. data/vendor/crates/spikard-http/src/handler_response.rs +532 -0
  78. data/vendor/crates/spikard-http/src/handler_trait.rs +861 -0
  79. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +284 -0
  80. data/vendor/crates/spikard-http/src/lib.rs +524 -0
  81. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +149 -0
  82. data/vendor/crates/spikard-http/src/lifecycle.rs +428 -0
  83. data/vendor/crates/spikard-http/src/middleware/mod.rs +285 -0
  84. data/vendor/crates/spikard-http/src/middleware/multipart.rs +930 -0
  85. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +541 -0
  86. data/vendor/crates/spikard-http/src/middleware/validation.rs +287 -0
  87. data/vendor/crates/spikard-http/src/openapi/mod.rs +309 -0
  88. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +535 -0
  89. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +867 -0
  90. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +678 -0
  91. data/vendor/crates/spikard-http/src/query_parser.rs +369 -0
  92. data/vendor/crates/spikard-http/src/response.rs +399 -0
  93. data/vendor/crates/spikard-http/src/server/handler.rs +1557 -0
  94. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +98 -0
  95. data/vendor/crates/spikard-http/src/server/mod.rs +806 -0
  96. data/vendor/crates/spikard-http/src/server/request_extraction.rs +630 -0
  97. data/vendor/crates/spikard-http/src/server/routing_factory.rs +497 -0
  98. data/vendor/crates/spikard-http/src/sse.rs +961 -0
  99. data/vendor/crates/spikard-http/src/testing/form.rs +14 -0
  100. data/vendor/crates/spikard-http/src/testing/multipart.rs +60 -0
  101. data/vendor/crates/spikard-http/src/testing/test_client.rs +285 -0
  102. data/vendor/crates/spikard-http/src/testing.rs +377 -0
  103. data/vendor/crates/spikard-http/src/websocket.rs +831 -0
  104. data/vendor/crates/spikard-http/tests/background_behavior.rs +918 -0
  105. data/vendor/crates/spikard-http/tests/common/handlers.rs +308 -0
  106. data/vendor/crates/spikard-http/tests/common/mod.rs +21 -0
  107. data/vendor/crates/spikard-http/tests/di_integration.rs +202 -0
  108. data/vendor/crates/spikard-http/tests/doc_snippets.rs +4 -0
  109. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1135 -0
  110. data/vendor/crates/spikard-http/tests/multipart_behavior.rs +688 -0
  111. data/vendor/crates/spikard-http/tests/server_config_builder.rs +324 -0
  112. data/vendor/crates/spikard-http/tests/sse_behavior.rs +728 -0
  113. data/vendor/crates/spikard-http/tests/websocket_behavior.rs +724 -0
  114. data/vendor/crates/spikard-rb/Cargo.toml +43 -0
  115. data/vendor/crates/spikard-rb/build.rs +199 -0
  116. data/vendor/crates/spikard-rb/src/background.rs +63 -0
  117. data/vendor/crates/spikard-rb/src/config/mod.rs +5 -0
  118. data/vendor/crates/spikard-rb/src/config/server_config.rs +283 -0
  119. data/vendor/crates/spikard-rb/src/conversion.rs +459 -0
  120. data/vendor/crates/spikard-rb/src/di/builder.rs +105 -0
  121. data/vendor/crates/spikard-rb/src/di/mod.rs +413 -0
  122. data/vendor/crates/spikard-rb/src/handler.rs +612 -0
  123. data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -0
  124. data/vendor/crates/spikard-rb/src/lib.rs +1857 -0
  125. data/vendor/crates/spikard-rb/src/lifecycle.rs +275 -0
  126. data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -0
  127. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +427 -0
  128. data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -0
  129. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +326 -0
  130. data/vendor/crates/spikard-rb/src/server.rs +283 -0
  131. data/vendor/crates/spikard-rb/src/sse.rs +231 -0
  132. data/vendor/crates/spikard-rb/src/testing/client.rs +404 -0
  133. data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -0
  134. data/vendor/crates/spikard-rb/src/testing/sse.rs +143 -0
  135. data/vendor/crates/spikard-rb/src/testing/websocket.rs +221 -0
  136. data/vendor/crates/spikard-rb/src/websocket.rs +233 -0
  137. data/vendor/crates/spikard-rb/tests/magnus_ffi_tests.rs +14 -0
  138. 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
+ }