spikard 0.3.6 → 0.5.0

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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -6
  3. data/ext/spikard_rb/Cargo.toml +2 -2
  4. data/lib/spikard/app.rb +33 -14
  5. data/lib/spikard/testing.rb +47 -12
  6. data/lib/spikard/version.rb +1 -1
  7. data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -0
  8. data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +132 -0
  9. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +752 -0
  10. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -0
  11. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -0
  12. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +401 -0
  13. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +238 -0
  14. data/vendor/crates/spikard-bindings-shared/src/lib.rs +24 -0
  15. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +292 -0
  16. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +616 -0
  17. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +305 -0
  18. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -0
  19. data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +351 -0
  20. data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +454 -0
  21. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +383 -0
  22. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +280 -0
  23. data/vendor/crates/spikard-core/Cargo.toml +4 -4
  24. data/vendor/crates/spikard-core/src/debug.rs +64 -0
  25. data/vendor/crates/spikard-core/src/di/container.rs +3 -27
  26. data/vendor/crates/spikard-core/src/di/factory.rs +1 -5
  27. data/vendor/crates/spikard-core/src/di/graph.rs +8 -47
  28. data/vendor/crates/spikard-core/src/di/mod.rs +1 -1
  29. data/vendor/crates/spikard-core/src/di/resolved.rs +1 -7
  30. data/vendor/crates/spikard-core/src/di/value.rs +2 -4
  31. data/vendor/crates/spikard-core/src/errors.rs +30 -0
  32. data/vendor/crates/spikard-core/src/http.rs +262 -0
  33. data/vendor/crates/spikard-core/src/lib.rs +1 -1
  34. data/vendor/crates/spikard-core/src/lifecycle.rs +764 -0
  35. data/vendor/crates/spikard-core/src/metadata.rs +389 -0
  36. data/vendor/crates/spikard-core/src/parameters.rs +1962 -159
  37. data/vendor/crates/spikard-core/src/problem.rs +34 -0
  38. data/vendor/crates/spikard-core/src/request_data.rs +966 -1
  39. data/vendor/crates/spikard-core/src/router.rs +263 -2
  40. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +688 -0
  41. data/vendor/crates/spikard-core/src/{validation.rs → validation/mod.rs} +26 -268
  42. data/vendor/crates/spikard-http/Cargo.toml +12 -16
  43. data/vendor/crates/spikard-http/examples/sse-notifications.rs +148 -0
  44. data/vendor/crates/spikard-http/examples/websocket-chat.rs +92 -0
  45. data/vendor/crates/spikard-http/src/auth.rs +65 -16
  46. data/vendor/crates/spikard-http/src/background.rs +1614 -3
  47. data/vendor/crates/spikard-http/src/cors.rs +515 -0
  48. data/vendor/crates/spikard-http/src/debug.rs +65 -0
  49. data/vendor/crates/spikard-http/src/di_handler.rs +1322 -77
  50. data/vendor/crates/spikard-http/src/handler_response.rs +711 -0
  51. data/vendor/crates/spikard-http/src/handler_trait.rs +607 -5
  52. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +6 -0
  53. data/vendor/crates/spikard-http/src/lib.rs +33 -28
  54. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +81 -0
  55. data/vendor/crates/spikard-http/src/lifecycle.rs +765 -0
  56. data/vendor/crates/spikard-http/src/middleware/mod.rs +372 -117
  57. data/vendor/crates/spikard-http/src/middleware/multipart.rs +836 -10
  58. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +409 -43
  59. data/vendor/crates/spikard-http/src/middleware/validation.rs +513 -65
  60. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +345 -0
  61. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +1055 -0
  62. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +473 -3
  63. data/vendor/crates/spikard-http/src/query_parser.rs +455 -31
  64. data/vendor/crates/spikard-http/src/response.rs +321 -0
  65. data/vendor/crates/spikard-http/src/server/handler.rs +1572 -9
  66. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +136 -0
  67. data/vendor/crates/spikard-http/src/server/mod.rs +875 -178
  68. data/vendor/crates/spikard-http/src/server/request_extraction.rs +674 -23
  69. data/vendor/crates/spikard-http/src/server/routing_factory.rs +599 -0
  70. data/vendor/crates/spikard-http/src/sse.rs +983 -21
  71. data/vendor/crates/spikard-http/src/testing/form.rs +38 -0
  72. data/vendor/crates/spikard-http/src/testing/test_client.rs +0 -2
  73. data/vendor/crates/spikard-http/src/testing.rs +7 -7
  74. data/vendor/crates/spikard-http/src/websocket.rs +1055 -4
  75. data/vendor/crates/spikard-http/tests/background_behavior.rs +832 -0
  76. data/vendor/crates/spikard-http/tests/common/handlers.rs +309 -0
  77. data/vendor/crates/spikard-http/tests/common/mod.rs +26 -0
  78. data/vendor/crates/spikard-http/tests/di_integration.rs +192 -0
  79. data/vendor/crates/spikard-http/tests/doc_snippets.rs +5 -0
  80. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1093 -0
  81. data/vendor/crates/spikard-http/tests/multipart_behavior.rs +656 -0
  82. data/vendor/crates/spikard-http/tests/server_config_builder.rs +314 -0
  83. data/vendor/crates/spikard-http/tests/sse_behavior.rs +620 -0
  84. data/vendor/crates/spikard-http/tests/websocket_behavior.rs +663 -0
  85. data/vendor/crates/spikard-rb/Cargo.toml +10 -4
  86. data/vendor/crates/spikard-rb/build.rs +196 -5
  87. data/vendor/crates/spikard-rb/src/config/mod.rs +5 -0
  88. data/vendor/crates/spikard-rb/src/{config.rs → config/server_config.rs} +100 -109
  89. data/vendor/crates/spikard-rb/src/conversion.rs +121 -20
  90. data/vendor/crates/spikard-rb/src/di/builder.rs +100 -0
  91. data/vendor/crates/spikard-rb/src/{di.rs → di/mod.rs} +12 -46
  92. data/vendor/crates/spikard-rb/src/handler.rs +100 -107
  93. data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -0
  94. data/vendor/crates/spikard-rb/src/lib.rs +467 -1428
  95. data/vendor/crates/spikard-rb/src/lifecycle.rs +1 -0
  96. data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -0
  97. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +447 -0
  98. data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -0
  99. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +324 -0
  100. data/vendor/crates/spikard-rb/src/server.rs +47 -22
  101. data/vendor/crates/spikard-rb/src/{test_client.rs → testing/client.rs} +187 -40
  102. data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -0
  103. data/vendor/crates/spikard-rb/src/testing/websocket.rs +635 -0
  104. data/vendor/crates/spikard-rb/src/websocket.rs +178 -37
  105. metadata +46 -13
  106. data/vendor/crates/spikard-http/src/parameters.rs +0 -1
  107. data/vendor/crates/spikard-http/src/problem.rs +0 -1
  108. data/vendor/crates/spikard-http/src/router.rs +0 -1
  109. data/vendor/crates/spikard-http/src/schema_registry.rs +0 -1
  110. data/vendor/crates/spikard-http/src/type_hints.rs +0 -1
  111. data/vendor/crates/spikard-http/src/validation.rs +0 -1
  112. data/vendor/crates/spikard-rb/src/test_websocket.rs +0 -221
  113. /data/vendor/crates/spikard-rb/src/{test_sse.rs → testing/sse.rs} +0 -0
@@ -26,6 +26,8 @@ use std::pin::Pin;
26
26
  pub struct RequestData {
27
27
  pub path_params: std::sync::Arc<HashMap<String, String>>,
28
28
  pub query_params: Value,
29
+ /// Validated parameters produced by ParameterValidator (query/path/header/cookie combined).
30
+ pub validated_params: Option<Value>,
29
31
  pub raw_query_params: std::sync::Arc<HashMap<String, Vec<String>>>,
30
32
  pub body: Value,
31
33
  pub raw_body: Option<bytes::Bytes>,
@@ -45,13 +47,14 @@ impl Serialize for RequestData {
45
47
  {
46
48
  use serde::ser::SerializeStruct;
47
49
  #[cfg(feature = "di")]
48
- let field_count = 10;
50
+ let field_count = 11;
49
51
  #[cfg(not(feature = "di"))]
50
- let field_count = 9;
52
+ let field_count = 10;
51
53
 
52
54
  let mut state = serializer.serialize_struct("RequestData", field_count)?;
53
55
  state.serialize_field("path_params", &*self.path_params)?;
54
56
  state.serialize_field("query_params", &self.query_params)?;
57
+ state.serialize_field("validated_params", &self.validated_params)?;
55
58
  state.serialize_field("raw_query_params", &*self.raw_query_params)?;
56
59
  state.serialize_field("body", &self.body)?;
57
60
  state.serialize_field("raw_body", &self.raw_body.as_ref().map(|b| b.as_ref()))?;
@@ -62,8 +65,6 @@ impl Serialize for RequestData {
62
65
 
63
66
  #[cfg(feature = "di")]
64
67
  {
65
- // Dependencies field is not serialized (contains Arc<dyn Any>)
66
- // We just serialize a marker indicating whether dependencies exist
67
68
  state.serialize_field("has_dependencies", &self.dependencies.is_some())?;
68
69
  }
69
70
 
@@ -82,6 +83,7 @@ impl<'de> Deserialize<'de> for RequestData {
82
83
  PathParams,
83
84
  QueryParams,
84
85
  RawQueryParams,
86
+ ValidatedParams,
85
87
  Body,
86
88
  RawBody,
87
89
  Headers,
@@ -108,6 +110,7 @@ impl<'de> Deserialize<'de> for RequestData {
108
110
  let mut path_params = None;
109
111
  let mut query_params = None;
110
112
  let mut raw_query_params = None;
113
+ let mut validated_params = None;
111
114
  let mut body = None;
112
115
  let mut raw_body = None;
113
116
  let mut headers = None;
@@ -126,6 +129,9 @@ impl<'de> Deserialize<'de> for RequestData {
126
129
  Field::RawQueryParams => {
127
130
  raw_query_params = Some(std::sync::Arc::new(map.next_value()?));
128
131
  }
132
+ Field::ValidatedParams => {
133
+ validated_params = Some(map.next_value()?);
134
+ }
129
135
  Field::Body => {
130
136
  body = Some(map.next_value()?);
131
137
  }
@@ -147,7 +153,6 @@ impl<'de> Deserialize<'de> for RequestData {
147
153
  }
148
154
  #[cfg(feature = "di")]
149
155
  Field::HasDependencies => {
150
- // We skip this field as dependencies can't be deserialized
151
156
  let _: bool = map.next_value()?;
152
157
  }
153
158
  }
@@ -158,6 +163,7 @@ impl<'de> Deserialize<'de> for RequestData {
158
163
  query_params: query_params.ok_or_else(|| serde::de::Error::missing_field("query_params"))?,
159
164
  raw_query_params: raw_query_params
160
165
  .ok_or_else(|| serde::de::Error::missing_field("raw_query_params"))?,
166
+ validated_params,
161
167
  body: body.ok_or_else(|| serde::de::Error::missing_field("body"))?,
162
168
  raw_body,
163
169
  headers: headers.ok_or_else(|| serde::de::Error::missing_field("headers"))?,
@@ -174,6 +180,7 @@ impl<'de> Deserialize<'de> for RequestData {
174
180
  const FIELDS: &[&str] = &[
175
181
  "path_params",
176
182
  "query_params",
183
+ "validated_params",
177
184
  "raw_query_params",
178
185
  "body",
179
186
  "raw_body",
@@ -188,6 +195,7 @@ impl<'de> Deserialize<'de> for RequestData {
188
195
  const FIELDS: &[&str] = &[
189
196
  "path_params",
190
197
  "query_params",
198
+ "validated_params",
191
199
  "raw_query_params",
192
200
  "body",
193
201
  "raw_body",
@@ -219,6 +227,40 @@ pub trait Handler: Send + Sync {
219
227
  request: Request<Body>,
220
228
  request_data: RequestData,
221
229
  ) -> Pin<Box<dyn Future<Output = HandlerResult> + Send + '_>>;
230
+
231
+ /// Whether this handler prefers consuming `RequestData::raw_body` over the parsed
232
+ /// `RequestData::body` for JSON requests.
233
+ ///
234
+ /// When `true`, the server may skip eager JSON parsing when there is no request-body
235
+ /// schema validator attached to the route.
236
+ fn prefers_raw_json_body(&self) -> bool {
237
+ false
238
+ }
239
+
240
+ /// Whether this handler wants to perform its own parameter validation/extraction (path/query/header/cookie).
241
+ ///
242
+ /// When `true`, the server will skip `ParameterValidator::validate_and_extract` in `ValidatingHandler`.
243
+ /// This is useful for language bindings which need to transform validated parameters into
244
+ /// language-specific values (e.g., Python kwargs) without duplicating work. When `false`,
245
+ /// the server stores validated output in `RequestData::validated_params`.
246
+ fn prefers_parameter_extraction(&self) -> bool {
247
+ false
248
+ }
249
+
250
+ /// Whether this handler needs the parsed headers map in `RequestData`.
251
+ ///
252
+ /// When `false`, the server may skip building `RequestData::headers` for requests without a body.
253
+ /// (Requests with bodies still typically need `Content-Type` decisions.)
254
+ fn wants_headers(&self) -> bool {
255
+ true
256
+ }
257
+
258
+ /// Whether this handler needs the parsed cookies map in `RequestData`.
259
+ ///
260
+ /// When `false`, the server may skip parsing cookies for requests without a body.
261
+ fn wants_cookies(&self) -> bool {
262
+ true
263
+ }
222
264
  }
223
265
 
224
266
  /// Validated parameters from request (path, query, headers, cookies)
@@ -226,3 +268,563 @@ pub trait Handler: Send + Sync {
226
268
  pub struct ValidatedParams {
227
269
  pub params: HashMap<String, Value>,
228
270
  }
271
+
272
+ #[cfg(test)]
273
+ mod tests {
274
+ use super::*;
275
+ use std::collections::HashMap;
276
+
277
+ fn minimal_request_data() -> RequestData {
278
+ RequestData {
279
+ path_params: std::sync::Arc::new(HashMap::new()),
280
+ query_params: Value::Object(serde_json::Map::new()),
281
+ validated_params: None,
282
+ raw_query_params: std::sync::Arc::new(HashMap::new()),
283
+ body: Value::Null,
284
+ raw_body: None,
285
+ headers: std::sync::Arc::new(HashMap::new()),
286
+ cookies: std::sync::Arc::new(HashMap::new()),
287
+ method: "GET".to_string(),
288
+ path: "/".to_string(),
289
+ #[cfg(feature = "di")]
290
+ dependencies: None,
291
+ }
292
+ }
293
+
294
+ #[test]
295
+ fn test_request_data_serialization_minimal() {
296
+ let data = minimal_request_data();
297
+
298
+ let json = serde_json::to_value(&data).expect("serialization failed");
299
+
300
+ assert!(json["path_params"].is_object());
301
+ assert!(json["query_params"].is_object());
302
+ assert!(json["raw_query_params"].is_object());
303
+ assert!(json["body"].is_null());
304
+ assert!(json["headers"].is_object());
305
+ assert!(json["cookies"].is_object());
306
+ assert_eq!(json["method"], "GET");
307
+ assert_eq!(json["path"], "/");
308
+ }
309
+
310
+ #[test]
311
+ fn test_request_data_serialization_with_path_params() {
312
+ let mut path_params = HashMap::new();
313
+ path_params.insert("id".to_string(), "123".to_string());
314
+ path_params.insert("username".to_string(), "alice".to_string());
315
+
316
+ let data = RequestData {
317
+ path_params: std::sync::Arc::new(path_params),
318
+ ..minimal_request_data()
319
+ };
320
+
321
+ let json = serde_json::to_value(&data).expect("serialization failed");
322
+
323
+ assert_eq!(json["path_params"]["id"], "123");
324
+ assert_eq!(json["path_params"]["username"], "alice");
325
+ }
326
+
327
+ #[test]
328
+ fn test_request_data_serialization_with_query_params() {
329
+ let query_params = serde_json::json!({
330
+ "filter": "active",
331
+ "limit": 10,
332
+ "sort": "name"
333
+ });
334
+
335
+ let data = RequestData {
336
+ query_params,
337
+ ..minimal_request_data()
338
+ };
339
+
340
+ let json = serde_json::to_value(&data).expect("serialization failed");
341
+
342
+ assert_eq!(json["query_params"]["filter"], "active");
343
+ assert_eq!(json["query_params"]["limit"], 10);
344
+ assert_eq!(json["query_params"]["sort"], "name");
345
+ }
346
+
347
+ #[test]
348
+ fn test_request_data_serialization_with_raw_query_params() {
349
+ let mut raw_query_params = HashMap::new();
350
+ raw_query_params.insert("tags".to_string(), vec!["rust".to_string(), "web".to_string()]);
351
+ raw_query_params.insert("category".to_string(), vec!["backend".to_string()]);
352
+
353
+ let data = RequestData {
354
+ raw_query_params: std::sync::Arc::new(raw_query_params),
355
+ ..minimal_request_data()
356
+ };
357
+
358
+ let json = serde_json::to_value(&data).expect("serialization failed");
359
+
360
+ assert!(json["raw_query_params"]["tags"].is_array());
361
+ assert_eq!(json["raw_query_params"]["tags"][0], "rust");
362
+ assert_eq!(json["raw_query_params"]["tags"][1], "web");
363
+ }
364
+
365
+ #[test]
366
+ fn test_request_data_serialization_with_headers() {
367
+ let mut headers = HashMap::new();
368
+ headers.insert("content-type".to_string(), "application/json".to_string());
369
+ headers.insert("authorization".to_string(), "Bearer token123".to_string());
370
+ headers.insert("user-agent".to_string(), "test-client/1.0".to_string());
371
+
372
+ let data = RequestData {
373
+ headers: std::sync::Arc::new(headers),
374
+ ..minimal_request_data()
375
+ };
376
+
377
+ let json = serde_json::to_value(&data).expect("serialization failed");
378
+
379
+ assert_eq!(json["headers"]["content-type"], "application/json");
380
+ assert_eq!(json["headers"]["authorization"], "Bearer token123");
381
+ assert_eq!(json["headers"]["user-agent"], "test-client/1.0");
382
+ }
383
+
384
+ #[test]
385
+ fn test_request_data_serialization_with_cookies() {
386
+ let mut cookies = HashMap::new();
387
+ cookies.insert("session_id".to_string(), "abc123def456".to_string());
388
+ cookies.insert("preferences".to_string(), "dark_mode=true".to_string());
389
+
390
+ let data = RequestData {
391
+ cookies: std::sync::Arc::new(cookies),
392
+ ..minimal_request_data()
393
+ };
394
+
395
+ let json = serde_json::to_value(&data).expect("serialization failed");
396
+
397
+ assert_eq!(json["cookies"]["session_id"], "abc123def456");
398
+ assert_eq!(json["cookies"]["preferences"], "dark_mode=true");
399
+ }
400
+
401
+ #[test]
402
+ fn test_request_data_serialization_with_json_body() {
403
+ let body = serde_json::json!({
404
+ "name": "test",
405
+ "age": 30,
406
+ "active": true,
407
+ "tags": ["a", "b"]
408
+ });
409
+
410
+ let data = RequestData {
411
+ body,
412
+ ..minimal_request_data()
413
+ };
414
+
415
+ let json = serde_json::to_value(&data).expect("serialization failed");
416
+
417
+ assert_eq!(json["body"]["name"], "test");
418
+ assert_eq!(json["body"]["age"], 30);
419
+ assert_eq!(json["body"]["active"], true);
420
+ assert!(json["body"]["tags"].is_array());
421
+ }
422
+
423
+ #[test]
424
+ fn test_request_data_serialization_with_raw_body() {
425
+ let raw_body = bytes::Bytes::from("raw body content");
426
+ let data = RequestData {
427
+ raw_body: Some(raw_body),
428
+ ..minimal_request_data()
429
+ };
430
+
431
+ let json = serde_json::to_value(&data).expect("serialization failed");
432
+
433
+ assert!(json["raw_body"].is_array());
434
+ let serialized_bytes: Vec<u8> =
435
+ serde_json::from_value(json["raw_body"].clone()).expect("failed to deserialize bytes");
436
+ assert_eq!(serialized_bytes, b"raw body content".to_vec());
437
+ }
438
+
439
+ #[test]
440
+ fn test_request_data_serialization_with_empty_strings() {
441
+ let mut headers = HashMap::new();
442
+ headers.insert("x-empty".to_string(), "".to_string());
443
+
444
+ let data = RequestData {
445
+ method: "".to_string(),
446
+ path: "".to_string(),
447
+ headers: std::sync::Arc::new(headers),
448
+ ..minimal_request_data()
449
+ };
450
+
451
+ let json = serde_json::to_value(&data).expect("serialization failed");
452
+
453
+ assert_eq!(json["method"], "");
454
+ assert_eq!(json["path"], "");
455
+ assert_eq!(json["headers"]["x-empty"], "");
456
+ }
457
+
458
+ #[test]
459
+ fn test_request_data_serialization_with_nested_json_body() {
460
+ let body = serde_json::json!({
461
+ "user": {
462
+ "profile": {
463
+ "name": "Alice",
464
+ "contact": {
465
+ "email": "alice@example.com",
466
+ "phone": null
467
+ }
468
+ }
469
+ },
470
+ "settings": {
471
+ "notifications": [true, false, true]
472
+ }
473
+ });
474
+
475
+ let data = RequestData {
476
+ body,
477
+ ..minimal_request_data()
478
+ };
479
+
480
+ let json = serde_json::to_value(&data).expect("serialization failed");
481
+
482
+ assert_eq!(json["body"]["user"]["profile"]["name"], "Alice");
483
+ assert_eq!(json["body"]["user"]["profile"]["contact"]["email"], "alice@example.com");
484
+ assert!(json["body"]["user"]["profile"]["contact"]["phone"].is_null());
485
+ assert_eq!(json["body"]["settings"]["notifications"][0], true);
486
+ }
487
+
488
+ #[test]
489
+ fn test_request_data_serialization_all_fields_complete() {
490
+ let mut path_params = HashMap::new();
491
+ path_params.insert("id".to_string(), "42".to_string());
492
+
493
+ let mut raw_query_params = HashMap::new();
494
+ raw_query_params.insert("filter".to_string(), vec!["active".to_string()]);
495
+
496
+ let mut headers = HashMap::new();
497
+ headers.insert("content-type".to_string(), "application/json".to_string());
498
+
499
+ let mut cookies = HashMap::new();
500
+ cookies.insert("session".to_string(), "xyz789".to_string());
501
+
502
+ let body = serde_json::json!({"action": "create"});
503
+ let raw_body = bytes::Bytes::from("body bytes");
504
+
505
+ let data = RequestData {
506
+ path_params: std::sync::Arc::new(path_params),
507
+ query_params: serde_json::json!({"page": 1}),
508
+ validated_params: None,
509
+ raw_query_params: std::sync::Arc::new(raw_query_params),
510
+ body,
511
+ raw_body: Some(raw_body),
512
+ headers: std::sync::Arc::new(headers),
513
+ cookies: std::sync::Arc::new(cookies),
514
+ method: "POST".to_string(),
515
+ path: "/api/users".to_string(),
516
+ #[cfg(feature = "di")]
517
+ dependencies: None,
518
+ };
519
+
520
+ let json = serde_json::to_value(&data).expect("serialization failed");
521
+
522
+ assert_eq!(json["path_params"]["id"], "42");
523
+ assert_eq!(json["query_params"]["page"], 1);
524
+ assert_eq!(json["raw_query_params"]["filter"][0], "active");
525
+ assert_eq!(json["body"]["action"], "create");
526
+ assert!(json["raw_body"].is_array());
527
+ assert_eq!(json["headers"]["content-type"], "application/json");
528
+ assert_eq!(json["cookies"]["session"], "xyz789");
529
+ assert_eq!(json["method"], "POST");
530
+ assert_eq!(json["path"], "/api/users");
531
+ }
532
+
533
+ #[test]
534
+ fn test_request_data_clone_shares_arc_data() {
535
+ let mut path_params = HashMap::new();
536
+ path_params.insert("id".to_string(), "original".to_string());
537
+
538
+ let data1 = RequestData {
539
+ path_params: std::sync::Arc::new(path_params),
540
+ ..minimal_request_data()
541
+ };
542
+
543
+ let data2 = data1.clone();
544
+
545
+ assert!(std::sync::Arc::ptr_eq(&data1.path_params, &data2.path_params));
546
+ }
547
+
548
+ #[test]
549
+ fn test_request_data_deserialization_complete() {
550
+ let json = serde_json::json!({
551
+ "path_params": {"id": "123"},
552
+ "query_params": {"filter": "active"},
553
+ "raw_query_params": {"tags": ["rust", "web"]},
554
+ "body": {"name": "test"},
555
+ "raw_body": null,
556
+ "headers": {"content-type": "application/json"},
557
+ "cookies": {"session": "abc"},
558
+ "method": "POST",
559
+ "path": "/api/test"
560
+ });
561
+
562
+ let data: RequestData = serde_json::from_value(json).expect("deserialization failed");
563
+
564
+ assert_eq!(data.path_params.get("id").unwrap(), "123");
565
+ assert_eq!(data.query_params["filter"], "active");
566
+ assert_eq!(data.raw_query_params.get("tags").unwrap()[0], "rust");
567
+ assert_eq!(data.body["name"], "test");
568
+ assert!(data.raw_body.is_none());
569
+ assert_eq!(data.headers.get("content-type").unwrap(), "application/json");
570
+ assert_eq!(data.cookies.get("session").unwrap(), "abc");
571
+ assert_eq!(data.method, "POST");
572
+ assert_eq!(data.path, "/api/test");
573
+ }
574
+
575
+ #[test]
576
+ fn test_request_data_deserialization_with_raw_body_bytes() {
577
+ let json = serde_json::json!({
578
+ "path_params": {},
579
+ "query_params": {},
580
+ "raw_query_params": {},
581
+ "body": null,
582
+ "raw_body": [72, 101, 108, 108, 111],
583
+ "headers": {},
584
+ "cookies": {},
585
+ "method": "GET",
586
+ "path": "/"
587
+ });
588
+
589
+ let data: RequestData = serde_json::from_value(json).expect("deserialization failed");
590
+
591
+ assert!(data.raw_body.is_some());
592
+ assert_eq!(data.raw_body.unwrap().as_ref(), b"Hello");
593
+ }
594
+
595
+ #[test]
596
+ fn test_request_data_deserialization_missing_required_field_path_params() {
597
+ let json = serde_json::json!({
598
+ "query_params": {},
599
+ "raw_query_params": {},
600
+ "body": null,
601
+ "headers": {},
602
+ "cookies": {},
603
+ "method": "GET",
604
+ "path": "/"
605
+ });
606
+
607
+ let result: Result<RequestData, _> = serde_json::from_value(json);
608
+ assert!(result.is_err());
609
+ assert!(result.unwrap_err().to_string().contains("path_params"));
610
+ }
611
+
612
+ #[test]
613
+ fn test_request_data_deserialization_missing_required_field_method() {
614
+ let json = serde_json::json!({
615
+ "path_params": {},
616
+ "query_params": {},
617
+ "raw_query_params": {},
618
+ "body": null,
619
+ "headers": {},
620
+ "cookies": {},
621
+ "path": "/"
622
+ });
623
+
624
+ let result: Result<RequestData, _> = serde_json::from_value(json);
625
+ assert!(result.is_err());
626
+ assert!(result.unwrap_err().to_string().contains("method"));
627
+ }
628
+
629
+ #[test]
630
+ fn test_request_data_serialization_roundtrip() {
631
+ let original = RequestData {
632
+ path_params: std::sync::Arc::new({
633
+ let mut map = HashMap::new();
634
+ map.insert("id".to_string(), "999".to_string());
635
+ map
636
+ }),
637
+ query_params: serde_json::json!({"limit": 50, "offset": 10}),
638
+ validated_params: None,
639
+ raw_query_params: std::sync::Arc::new({
640
+ let mut map = HashMap::new();
641
+ map.insert("sort".to_string(), vec!["name".to_string(), "date".to_string()]);
642
+ map
643
+ }),
644
+ body: serde_json::json!({"title": "New Post", "content": "Hello World"}),
645
+ raw_body: None,
646
+ headers: std::sync::Arc::new({
647
+ let mut map = HashMap::new();
648
+ map.insert("accept".to_string(), "application/json".to_string());
649
+ map
650
+ }),
651
+ cookies: std::sync::Arc::new({
652
+ let mut map = HashMap::new();
653
+ map.insert("user_id".to_string(), "user42".to_string());
654
+ map
655
+ }),
656
+ method: "PUT".to_string(),
657
+ path: "/blog/posts/999".to_string(),
658
+ #[cfg(feature = "di")]
659
+ dependencies: None,
660
+ };
661
+
662
+ let json = serde_json::to_value(&original).expect("serialization failed");
663
+ let restored: RequestData = serde_json::from_value(json).expect("deserialization failed");
664
+
665
+ assert_eq!(*original.path_params, *restored.path_params);
666
+ assert_eq!(original.query_params, restored.query_params);
667
+ assert_eq!(*original.raw_query_params, *restored.raw_query_params);
668
+ assert_eq!(original.body, restored.body);
669
+ assert_eq!(original.raw_body, restored.raw_body);
670
+ assert_eq!(*original.headers, *restored.headers);
671
+ assert_eq!(*original.cookies, *restored.cookies);
672
+ assert_eq!(original.method, restored.method);
673
+ assert_eq!(original.path, restored.path);
674
+ }
675
+
676
+ #[test]
677
+ fn test_request_data_serialization_large_body() {
678
+ let mut large_object = serde_json::Map::new();
679
+ for i in 0..100 {
680
+ large_object.insert(format!("key_{}", i), serde_json::Value::String(format!("value_{}", i)));
681
+ }
682
+
683
+ let data = RequestData {
684
+ body: Value::Object(large_object),
685
+ ..minimal_request_data()
686
+ };
687
+
688
+ let json = serde_json::to_value(&data).expect("serialization failed");
689
+
690
+ assert!(json["body"].is_object());
691
+ assert_eq!(json["body"].get("key_0").unwrap(), "value_0");
692
+ assert_eq!(json["body"].get("key_99").unwrap(), "value_99");
693
+ }
694
+
695
+ #[test]
696
+ fn test_request_data_empty_collections() {
697
+ let data = RequestData {
698
+ path_params: std::sync::Arc::new(HashMap::new()),
699
+ query_params: Value::Object(serde_json::Map::new()),
700
+ validated_params: None,
701
+ raw_query_params: std::sync::Arc::new(HashMap::new()),
702
+ body: Value::Object(serde_json::Map::new()),
703
+ raw_body: None,
704
+ headers: std::sync::Arc::new(HashMap::new()),
705
+ cookies: std::sync::Arc::new(HashMap::new()),
706
+ method: "GET".to_string(),
707
+ path: "/".to_string(),
708
+ #[cfg(feature = "di")]
709
+ dependencies: None,
710
+ };
711
+
712
+ let json = serde_json::to_value(&data).expect("serialization failed");
713
+
714
+ assert_eq!(json["path_params"].as_object().unwrap().len(), 0);
715
+ assert_eq!(json["query_params"].as_object().unwrap().len(), 0);
716
+ assert_eq!(json["raw_query_params"].as_object().unwrap().len(), 0);
717
+ assert_eq!(json["body"].as_object().unwrap().len(), 0);
718
+ assert!(json["raw_body"].is_null());
719
+ assert_eq!(json["headers"].as_object().unwrap().len(), 0);
720
+ assert_eq!(json["cookies"].as_object().unwrap().len(), 0);
721
+ }
722
+
723
+ #[test]
724
+ fn test_request_data_special_characters_in_strings() {
725
+ let mut headers = HashMap::new();
726
+ headers.insert("x-custom".to_string(), "value with \"quotes\"".to_string());
727
+ headers.insert("x-unicode".to_string(), "Café ☕ 🚀".to_string());
728
+
729
+ let data = RequestData {
730
+ method: "POST".to_string(),
731
+ path: "/api/v1/users\\test".to_string(),
732
+ headers: std::sync::Arc::new(headers),
733
+ body: serde_json::json!({"note": "Contains\nnewline"}),
734
+ ..minimal_request_data()
735
+ };
736
+
737
+ let json = serde_json::to_value(&data).expect("serialization failed");
738
+
739
+ assert_eq!(json["headers"]["x-custom"], "value with \"quotes\"");
740
+ assert_eq!(json["headers"]["x-unicode"], "Café ☕ 🚀");
741
+ assert_eq!(json["path"], "/api/v1/users\\test");
742
+ assert_eq!(json["body"]["note"], "Contains\nnewline");
743
+ }
744
+
745
+ #[test]
746
+ #[cfg(feature = "di")]
747
+ fn test_request_data_serialization_with_di_feature_no_dependencies() {
748
+ let data = minimal_request_data();
749
+
750
+ let json = serde_json::to_value(&data).expect("serialization failed");
751
+
752
+ assert_eq!(json["has_dependencies"], false);
753
+ }
754
+
755
+ #[test]
756
+ fn test_request_data_method_variants() {
757
+ let methods = vec!["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"];
758
+
759
+ for method in methods {
760
+ let data = RequestData {
761
+ method: method.to_string(),
762
+ ..minimal_request_data()
763
+ };
764
+
765
+ let json = serde_json::to_value(&data).expect("serialization failed");
766
+
767
+ assert_eq!(json["method"], method);
768
+ }
769
+ }
770
+
771
+ #[test]
772
+ fn test_request_data_serialization_null_body() {
773
+ let data = RequestData {
774
+ body: Value::Null,
775
+ ..minimal_request_data()
776
+ };
777
+
778
+ let json = serde_json::to_value(&data).expect("serialization failed");
779
+
780
+ assert!(json["body"].is_null());
781
+ }
782
+
783
+ #[test]
784
+ fn test_request_data_serialization_array_body() {
785
+ let data = RequestData {
786
+ body: serde_json::json!([1, 2, 3, "four", {"five": 5}]),
787
+ ..minimal_request_data()
788
+ };
789
+
790
+ let json = serde_json::to_value(&data).expect("serialization failed");
791
+
792
+ assert!(json["body"].is_array());
793
+ assert_eq!(json["body"][0], 1);
794
+ assert_eq!(json["body"][1], 2);
795
+ assert_eq!(json["body"][3], "four");
796
+ assert_eq!(json["body"][4]["five"], 5);
797
+ }
798
+
799
+ #[test]
800
+ fn test_request_data_serialization_numeric_edge_cases() {
801
+ let data = RequestData {
802
+ body: serde_json::json!({
803
+ "zero": 0,
804
+ "negative": -42,
805
+ "large": 9223372036854775807i64,
806
+ "float": 3.14159
807
+ }),
808
+ ..minimal_request_data()
809
+ };
810
+
811
+ let json = serde_json::to_value(&data).expect("serialization failed");
812
+
813
+ assert_eq!(json["body"]["zero"], 0);
814
+ assert_eq!(json["body"]["negative"], -42);
815
+ assert_eq!(json["body"]["large"], 9223372036854775807i64);
816
+ assert_eq!(json["body"]["float"], 3.14159);
817
+ }
818
+
819
+ #[test]
820
+ fn test_validated_params_basic_creation() {
821
+ let mut params = HashMap::new();
822
+ params.insert("id".to_string(), Value::String("123".to_string()));
823
+ params.insert("active".to_string(), Value::Bool(true));
824
+
825
+ let validated = ValidatedParams { params };
826
+
827
+ assert_eq!(validated.params.get("id").unwrap(), &Value::String("123".to_string()));
828
+ assert_eq!(validated.params.get("active").unwrap(), &Value::Bool(true));
829
+ }
830
+ }