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
@@ -128,11 +128,11 @@ pub fn parse_query_string(qs: &[u8], separator: char) -> Vec<(String, String)> {
128
128
  /// - Numbers (if parse_numbers is true)
129
129
  /// - Strings (fallback)
130
130
  #[inline]
131
- fn decode_value(json_str: String, parse_numbers: bool) -> Value {
132
- if PARENTHESES_RE.is_match(json_str.as_str()) {
133
- let result: Value = match from_str(json_str.as_str()) {
131
+ fn decode_value(raw: &str, parse_numbers: bool) -> Value {
132
+ if PARENTHESES_RE.is_match(raw) {
133
+ let result: Value = match from_str(raw) {
134
134
  Ok(value) => value,
135
- Err(_) => match from_str(json_str.replace('\'', "\"").as_str()) {
135
+ Err(_) => match from_str(raw.replace('\'', "\"").as_str()) {
136
136
  Ok(normalized) => normalized,
137
137
  Err(_) => Value::Null,
138
138
  },
@@ -140,27 +140,31 @@ fn decode_value(json_str: String, parse_numbers: bool) -> Value {
140
140
  return result;
141
141
  }
142
142
 
143
- let normalized = json_str.replace('"', "");
143
+ let normalized = if raw.as_bytes().contains(&b'"') {
144
+ Cow::Owned(raw.replace('"', ""))
145
+ } else {
146
+ Cow::Borrowed(raw)
147
+ };
144
148
 
145
149
  let json_boolean = parse_boolean(&normalized);
146
- let json_null = Ok::<_, Infallible>(normalized == "null");
150
+ let json_null = Ok::<_, Infallible>(normalized.as_ref() == "null");
147
151
 
148
152
  if parse_numbers {
149
- let json_integer = normalized.parse::<i64>();
150
- let json_float = normalized.parse::<f64>();
153
+ let json_integer = normalized.as_ref().parse::<i64>();
154
+ let json_float = normalized.as_ref().parse::<f64>();
151
155
  return match (json_integer, json_float, json_boolean, json_null) {
152
156
  (Ok(json_integer), _, _, _) => Value::from(json_integer),
153
157
  (_, Ok(json_float), _, _) => Value::from(json_float),
154
158
  (_, _, Ok(json_boolean), _) => Value::from(json_boolean),
155
159
  (_, _, _, Ok(true)) => Value::Null,
156
- _ => Value::from(normalized),
160
+ _ => Value::from(normalized.as_ref()),
157
161
  };
158
162
  }
159
163
 
160
164
  match (json_boolean, json_null) {
161
165
  (Ok(json_boolean), _) => Value::from(json_boolean),
162
166
  (_, Ok(true)) => Value::Null,
163
- _ => Value::from(normalized),
167
+ _ => Value::from(normalized.as_ref()),
164
168
  }
165
169
  }
166
170
 
@@ -174,43 +178,33 @@ fn decode_value(json_str: String, parse_numbers: bool) -> Value {
174
178
  /// - "" (empty string) → Err (don't coerce, preserve as empty string)
175
179
  #[inline]
176
180
  fn parse_boolean(s: &str) -> Result<bool, ()> {
177
- let lower = s.to_lowercase();
178
- if lower == "true" || s == "1" {
181
+ if s.eq_ignore_ascii_case("true") || s == "1" {
179
182
  Ok(true)
180
- } else if lower == "false" || s == "0" {
183
+ } else if s.eq_ignore_ascii_case("false") || s == "0" {
181
184
  Ok(false)
182
185
  } else {
183
186
  Err(())
184
187
  }
185
188
  }
186
189
 
187
- /// Parse a query string into a JSON Value.
190
+ /// Convert already-decoded query pairs into a JSON Value.
188
191
  ///
189
- /// This function:
190
- /// - Handles multiple values for the same key (creates arrays)
191
- /// - Auto-converts types (numbers, booleans, null, objects, arrays)
192
- /// - Collapses single-item arrays into single values
192
+ /// This is useful when callers need both:
193
+ /// - the raw decoded pairs (for error messages / multi-value handling), and
194
+ /// - a JSON object with type coercion (for downstream consumers),
193
195
  ///
194
- /// # Arguments
195
- /// * `qs` - The query string bytes
196
- /// * `parse_numbers` - Whether to parse numeric strings into numbers
197
- ///
198
- /// # Example
199
- /// ```ignore
200
- /// let result = parse_query_string_to_json(b"foo=1&foo=2&bar=test&active=true", true);
201
- /// // {"foo": [1, 2], "bar": "test", "active": true}
202
- /// ```
196
+ /// while avoiding a second URL-decoding pass.
203
197
  #[inline]
204
- pub fn parse_query_string_to_json(qs: &[u8], parse_numbers: bool) -> Value {
198
+ pub fn parse_query_pairs_to_json(pairs: &[(String, String)], parse_numbers: bool) -> Value {
205
199
  let mut array_map: FxHashMap<String, Vec<Value>> = FxHashMap::default();
206
200
 
207
- for (key, value) in parse_query_string(qs, '&') {
208
- match array_map.get_mut(&key) {
201
+ for (key, value) in pairs {
202
+ match array_map.get_mut(key) {
209
203
  Some(entry) => {
210
204
  entry.push(decode_value(value, parse_numbers));
211
205
  }
212
206
  None => {
213
- array_map.insert(key, vec![decode_value(value, parse_numbers)]);
207
+ array_map.insert(key.clone(), vec![decode_value(value, parse_numbers)]);
214
208
  }
215
209
  }
216
210
  }
@@ -227,6 +221,28 @@ pub fn parse_query_string_to_json(qs: &[u8], parse_numbers: bool) -> Value {
227
221
  .collect::<Value>()
228
222
  }
229
223
 
224
+ /// Parse a query string into a JSON Value.
225
+ ///
226
+ /// This function:
227
+ /// - Handles multiple values for the same key (creates arrays)
228
+ /// - Auto-converts types (numbers, booleans, null, objects, arrays)
229
+ /// - Collapses single-item arrays into single values
230
+ ///
231
+ /// # Arguments
232
+ /// * `qs` - The query string bytes
233
+ /// * `parse_numbers` - Whether to parse numeric strings into numbers
234
+ ///
235
+ /// # Example
236
+ /// ```ignore
237
+ /// let result = parse_query_string_to_json(b"foo=1&foo=2&bar=test&active=true", true);
238
+ /// // {"foo": [1, 2], "bar": "test", "active": true}
239
+ /// ```
240
+ #[inline]
241
+ pub fn parse_query_string_to_json(qs: &[u8], parse_numbers: bool) -> Value {
242
+ let pairs = parse_query_string(qs, '&');
243
+ parse_query_pairs_to_json(&pairs, parse_numbers)
244
+ }
245
+
230
246
  #[cfg(test)]
231
247
  mod tests {
232
248
  use super::*;
@@ -366,4 +382,412 @@ mod tests {
366
382
  let result = parse_query_string_to_json(b"name=test%26value%3D123", false);
367
383
  assert_eq!(result, json!({ "name": "test&value=123" }));
368
384
  }
385
+
386
+ #[test]
387
+ fn test_malformed_percent_single_char() {
388
+ let result = parse_query_string(b"key=%", '&');
389
+ assert_eq!(result, vec![(String::from("key"), String::from("%"))]);
390
+ }
391
+
392
+ #[test]
393
+ fn test_malformed_percent_single_hex_only() {
394
+ let result = parse_query_string(b"key=%2", '&');
395
+ assert_eq!(result, vec![(String::from("key"), String::from("%2"))]);
396
+ }
397
+
398
+ #[test]
399
+ fn test_malformed_percent_invalid_hex_chars() {
400
+ let result = parse_query_string(b"key=%GG&other=value", '&');
401
+ assert_eq!(
402
+ result,
403
+ vec![
404
+ (String::from("key"), String::from("%GG")),
405
+ (String::from("other"), String::from("value")),
406
+ ]
407
+ );
408
+ }
409
+
410
+ #[test]
411
+ fn test_malformed_percent_mixed_invalid_hex() {
412
+ let result = parse_query_string(b"key=%2G&other=value", '&');
413
+ assert_eq!(
414
+ result,
415
+ vec![
416
+ (String::from("key"), String::from("%2G")),
417
+ (String::from("other"), String::from("value")),
418
+ ]
419
+ );
420
+ }
421
+
422
+ #[test]
423
+ fn test_percent_encoding_lowercase_hex() {
424
+ let result = parse_query_string(b"key=%2f&other=test", '&');
425
+ assert_eq!(
426
+ result,
427
+ vec![
428
+ (String::from("key"), String::from("/")),
429
+ (String::from("other"), String::from("test")),
430
+ ]
431
+ );
432
+ }
433
+
434
+ #[test]
435
+ fn test_percent_encoding_uppercase_hex() {
436
+ let result = parse_query_string(b"key=%2F&other=test", '&');
437
+ assert_eq!(
438
+ result,
439
+ vec![
440
+ (String::from("key"), String::from("/")),
441
+ (String::from("other"), String::from("test")),
442
+ ]
443
+ );
444
+ }
445
+
446
+ #[test]
447
+ fn test_percent_encoding_mixed_case_hex() {
448
+ let result = parse_query_string(b"key=%2f%3D%4A", '&');
449
+ assert_eq!(result, vec![(String::from("key"), String::from("/=J"))]);
450
+ }
451
+
452
+ #[test]
453
+ fn test_plus_as_space_in_value() {
454
+ let result = parse_query_string(b"message=hello+world", '&');
455
+ assert_eq!(result, vec![(String::from("message"), String::from("hello world"))]);
456
+ }
457
+
458
+ #[test]
459
+ fn test_plus_as_space_multiple_plus() {
460
+ let result = parse_query_string(b"message=a+b+c+d", '&');
461
+ assert_eq!(result, vec![(String::from("message"), String::from("a b c d"))]);
462
+ }
463
+
464
+ #[test]
465
+ fn test_percent_encoded_space_vs_plus() {
466
+ let result = parse_query_string(b"a=%20space&b=+plus", '&');
467
+ assert_eq!(
468
+ result,
469
+ vec![
470
+ (String::from("a"), String::from(" space")),
471
+ (String::from("b"), String::from(" plus")),
472
+ ]
473
+ );
474
+ }
475
+
476
+ #[test]
477
+ fn test_mixed_plus_and_percent_encoded_space() {
478
+ let result = parse_query_string(b"text=hello+%20world", '&');
479
+ assert_eq!(result, vec![(String::from("text"), String::from("hello world"))]);
480
+ }
481
+
482
+ #[test]
483
+ fn test_ampersand_in_value_encoded() {
484
+ let result = parse_query_string(b"text=foo%26bar", '&');
485
+ assert_eq!(result, vec![(String::from("text"), String::from("foo&bar"))]);
486
+ }
487
+
488
+ #[test]
489
+ fn test_equals_in_value_encoded() {
490
+ let result = parse_query_string(b"text=a%3Db", '&');
491
+ assert_eq!(result, vec![(String::from("text"), String::from("a=b"))]);
492
+ }
493
+
494
+ #[test]
495
+ fn test_question_mark_in_value_encoded() {
496
+ let result = parse_query_string(b"text=what%3F", '&');
497
+ assert_eq!(result, vec![(String::from("text"), String::from("what?"))]);
498
+ }
499
+
500
+ #[test]
501
+ fn test_hash_in_value_encoded() {
502
+ let result = parse_query_string(b"text=anchor%23top", '&');
503
+ assert_eq!(result, vec![(String::from("text"), String::from("anchor#top"))]);
504
+ }
505
+
506
+ #[test]
507
+ fn test_multiple_encoded_special_chars() {
508
+ let result = parse_query_string(b"text=%26%3D%3F%23", '&');
509
+ assert_eq!(result, vec![(String::from("text"), String::from("&=?#"))]);
510
+ }
511
+
512
+ #[test]
513
+ fn test_empty_query_string() {
514
+ let result = parse_query_string(b"", '&');
515
+ assert_eq!(result, vec![]);
516
+ }
517
+
518
+ #[test]
519
+ fn test_multiple_consecutive_separators() {
520
+ let result = parse_query_string(b"a=1&&&b=2", '&');
521
+ assert_eq!(
522
+ result,
523
+ vec![
524
+ (String::from("a"), String::from("1")),
525
+ (String::from("b"), String::from("2")),
526
+ ]
527
+ );
528
+ }
529
+
530
+ #[test]
531
+ fn test_key_without_value() {
532
+ let result = parse_query_string(b"key=", '&');
533
+ assert_eq!(result, vec![(String::from("key"), String::from(""))]);
534
+ }
535
+
536
+ #[test]
537
+ fn test_key_without_equals() {
538
+ let result = parse_query_string(b"key", '&');
539
+ assert_eq!(result, vec![(String::from("key"), String::from(""))]);
540
+ }
541
+
542
+ #[test]
543
+ fn test_value_without_key() {
544
+ let result = parse_query_string(b"=value", '&');
545
+ assert_eq!(result, vec![(String::from(""), String::from("value"))]);
546
+ }
547
+
548
+ #[test]
549
+ fn test_multiple_equals_in_pair() {
550
+ let result = parse_query_string(b"key=val=more", '&');
551
+ assert_eq!(result, vec![(String::from("key"), String::from("val=more"))]);
552
+ }
553
+
554
+ #[test]
555
+ fn test_separator_at_start() {
556
+ let result = parse_query_string(b"&key=value", '&');
557
+ assert_eq!(result, vec![(String::from("key"), String::from("value"))]);
558
+ }
559
+
560
+ #[test]
561
+ fn test_separator_at_end() {
562
+ let result = parse_query_string(b"key=value&", '&');
563
+ assert_eq!(result, vec![(String::from("key"), String::from("value"))]);
564
+ }
565
+
566
+ #[test]
567
+ fn test_separator_at_both_ends() {
568
+ let result = parse_query_string(b"&key=value&", '&');
569
+ assert_eq!(result, vec![(String::from("key"), String::from("value"))]);
570
+ }
571
+
572
+ #[test]
573
+ fn test_multiple_values_same_key() {
574
+ let result = parse_query_string(b"tag=foo&tag=bar&tag=baz", '&');
575
+ assert_eq!(
576
+ result,
577
+ vec![
578
+ (String::from("tag"), String::from("foo")),
579
+ (String::from("tag"), String::from("bar")),
580
+ (String::from("tag"), String::from("baz")),
581
+ ]
582
+ );
583
+ }
584
+
585
+ #[test]
586
+ fn test_multiple_values_mixed_keys() {
587
+ let result = parse_query_string(b"tag=foo&id=1&tag=bar&id=2", '&');
588
+ assert_eq!(
589
+ result,
590
+ vec![
591
+ (String::from("tag"), String::from("foo")),
592
+ (String::from("id"), String::from("1")),
593
+ (String::from("tag"), String::from("bar")),
594
+ (String::from("id"), String::from("2")),
595
+ ]
596
+ );
597
+ }
598
+
599
+ #[test]
600
+ fn test_json_conversion_empty_key() {
601
+ let result = parse_query_string_to_json(b"=value", false);
602
+ assert_eq!(result, json!({ "": "value" }));
603
+ }
604
+
605
+ #[test]
606
+ fn test_json_conversion_all_empty_values() {
607
+ let result = parse_query_string_to_json(b"a=&b=&c=", false);
608
+ assert_eq!(result, json!({ "a": "", "b": "", "c": "" }));
609
+ }
610
+
611
+ #[test]
612
+ fn test_json_conversion_malformed_json_object() {
613
+ let result = parse_query_string_to_json(b"data={invalid", false);
614
+ assert_eq!(result, json!({ "data": "{invalid" }));
615
+ }
616
+
617
+ #[test]
618
+ fn test_json_conversion_malformed_json_array() {
619
+ let result = parse_query_string_to_json(b"items=[1,2,", false);
620
+ assert_eq!(result, json!({ "items": "[1,2," }));
621
+ }
622
+
623
+ #[test]
624
+ fn test_json_conversion_with_quotes_in_value() {
625
+ let result = parse_query_string_to_json(b"text=\"hello\"", false);
626
+ assert_eq!(result, json!({ "text": "hello" }));
627
+ }
628
+
629
+ #[test]
630
+ fn test_json_conversion_single_quotes_in_object() {
631
+ let result = parse_query_string_to_json(b"obj={'key':'value'}", false);
632
+ let value = result.get("obj");
633
+ assert!(value.is_some());
634
+ }
635
+
636
+ #[test]
637
+ fn test_boolean_case_insensitive_variations() {
638
+ assert_eq!(parse_query_string_to_json(b"a=tRuE", false), json!({"a": true}));
639
+ assert_eq!(parse_query_string_to_json(b"a=FaLsE", false), json!({"a": false}));
640
+ assert_eq!(
641
+ parse_query_string_to_json(b"a=tRuE&b=FaLsE", false),
642
+ json!({"a": true, "b": false})
643
+ );
644
+ }
645
+
646
+ #[test]
647
+ fn test_boolean_with_numbers_no_parse() {
648
+ assert_eq!(parse_query_string_to_json(b"a=1", false), json!({"a": true}));
649
+ assert_eq!(parse_query_string_to_json(b"a=0", false), json!({"a": false}));
650
+ }
651
+
652
+ #[test]
653
+ fn test_number_parsing_negative() {
654
+ assert_eq!(parse_query_string_to_json(b"a=-123", true), json!({"a": -123}));
655
+ assert_eq!(parse_query_string_to_json(b"a=-1.5", true), json!({"a": -1.5}));
656
+ }
657
+
658
+ #[test]
659
+ fn test_number_parsing_zero() {
660
+ assert_eq!(parse_query_string_to_json(b"a=0", true), json!({"a": 0}));
661
+ assert_eq!(parse_query_string_to_json(b"a=0.0", true), json!({"a": 0.0}));
662
+ }
663
+
664
+ #[test]
665
+ fn test_number_parsing_scientific_notation() {
666
+ assert_eq!(parse_query_string_to_json(b"a=1e10", true), json!({"a": 1e10}));
667
+ assert_eq!(parse_query_string_to_json(b"a=1.5e-3", true), json!({"a": 1.5e-3}));
668
+ }
669
+
670
+ #[test]
671
+ fn test_array_mixed_types_with_number_parsing() {
672
+ assert_eq!(
673
+ parse_query_string_to_json(b"vals=1&vals=2.5&vals=true&vals=test", true),
674
+ json!({"vals": [1, 2.5, true, "test"]})
675
+ );
676
+ }
677
+
678
+ #[test]
679
+ fn test_array_mixed_types_without_number_parsing() {
680
+ assert_eq!(
681
+ parse_query_string_to_json(b"vals=1&vals=2.5&vals=true&vals=test", false),
682
+ json!({"vals": [true, "2.5", true, "test"]})
683
+ );
684
+ }
685
+
686
+ #[test]
687
+ fn test_utf8_chinese_characters() {
688
+ let result = parse_query_string("name=中文".as_bytes(), '&');
689
+ assert_eq!(result, vec![(String::from("name"), String::from("中文"))]);
690
+ }
691
+
692
+ #[test]
693
+ fn test_utf8_emoji() {
694
+ let result = parse_query_string("emoji=🚀".as_bytes(), '&');
695
+ assert_eq!(result, vec![(String::from("emoji"), String::from("🚀"))]);
696
+ }
697
+
698
+ #[test]
699
+ fn test_utf8_mixed_with_encoding() {
700
+ let result = parse_query_string("text=hello%20中文".as_bytes(), '&');
701
+ assert_eq!(result, vec![(String::from("text"), String::from("hello 中文"))]);
702
+ }
703
+
704
+ #[test]
705
+ fn test_custom_separator_semicolon() {
706
+ let result = parse_query_string(b"a=1;b=2;c=3", ';');
707
+ assert_eq!(
708
+ result,
709
+ vec![
710
+ (String::from("a"), String::from("1")),
711
+ (String::from("b"), String::from("2")),
712
+ (String::from("c"), String::from("3")),
713
+ ]
714
+ );
715
+ }
716
+
717
+ #[test]
718
+ fn test_custom_separator_comma() {
719
+ let result = parse_query_string(b"a=1,b=2,c=3", ',');
720
+ assert_eq!(
721
+ result,
722
+ vec![
723
+ (String::from("a"), String::from("1")),
724
+ (String::from("b"), String::from("2")),
725
+ (String::from("c"), String::from("3")),
726
+ ]
727
+ );
728
+ }
729
+
730
+ #[test]
731
+ fn test_percent_encoding_all_byte_values() {
732
+ let result = parse_query_string(b"space=%20&at=%40&hash=%23&dollar=%24", '&');
733
+ assert_eq!(
734
+ result,
735
+ vec![
736
+ (String::from("space"), String::from(" ")),
737
+ (String::from("at"), String::from("@")),
738
+ (String::from("hash"), String::from("#")),
739
+ (String::from("dollar"), String::from("$")),
740
+ ]
741
+ );
742
+ }
743
+
744
+ #[test]
745
+ fn test_high_byte_values_in_percent_encoding() {
746
+ let result = parse_query_string(b"high=%ff%fe%fd", '&');
747
+ assert_eq!(result.len(), 1);
748
+ assert_eq!(result[0].0, "high");
749
+ }
750
+
751
+ #[test]
752
+ fn test_very_long_query_string() {
753
+ let mut long_query = String::from("key=");
754
+ long_query.push_str(&"a".repeat(10000));
755
+ let result = parse_query_string(long_query.as_bytes(), '&');
756
+ assert_eq!(result.len(), 1);
757
+ assert_eq!(result[0].0, "key");
758
+ assert_eq!(result[0].1.len(), 10000);
759
+ }
760
+
761
+ #[test]
762
+ fn test_very_large_number_of_parameters() {
763
+ let mut query = String::new();
764
+ for i in 0..100 {
765
+ if i > 0 {
766
+ query.push('&');
767
+ }
768
+ query.push_str(&format!("param{}=value{}", i, i));
769
+ }
770
+ let result = parse_query_string(query.as_bytes(), '&');
771
+ assert_eq!(result.len(), 100);
772
+ assert_eq!(result[0].0, "param0");
773
+ assert_eq!(result[99].0, "param99");
774
+ }
775
+
776
+ #[test]
777
+ fn test_literal_space_in_value() {
778
+ let result = parse_query_string(b"name=hello world", '&');
779
+ assert_eq!(result, vec![(String::from("name"), String::from("hello world"))]);
780
+ }
781
+
782
+ #[test]
783
+ fn test_tab_in_value() {
784
+ let result = parse_query_string(b"name=hello\tworld", '&');
785
+ assert_eq!(result, vec![(String::from("name"), String::from("hello\tworld"))]);
786
+ }
787
+
788
+ #[test]
789
+ fn test_newline_in_value() {
790
+ let result = parse_query_string(b"name=hello\nworld", '&');
791
+ assert_eq!(result, vec![(String::from("name"), String::from("hello\nworld"))]);
792
+ }
369
793
  }