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
@@ -1,6 +1,7 @@
1
1
  //! URL-encoded form data parsing
2
2
 
3
3
  use std::collections::HashMap;
4
+ use url::form_urlencoded;
4
5
 
5
6
  /// Parse URL-encoded form data to JSON
6
7
  ///
@@ -15,9 +16,8 @@ use std::collections::HashMap;
15
16
  /// - If brackets present → use serde_qs (handles nested objects, arrays with [])
16
17
  /// - Otherwise → use custom parser that preserves empty strings and handles duplicate keys
17
18
  pub fn parse_urlencoded_to_json(data: &[u8]) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
18
- let body_str = std::str::from_utf8(data)?;
19
-
20
- if body_str.contains('[') {
19
+ if data.contains(&b'[') {
20
+ let body_str = std::str::from_utf8(data)?;
21
21
  let config = serde_qs::Config::new(10, false);
22
22
  let parsed: HashMap<String, serde_json::Value> = config.deserialize_str(body_str)?;
23
23
  let mut json_value = serde_json::to_value(parsed)?;
@@ -31,49 +31,23 @@ pub fn parse_urlencoded_to_json(data: &[u8]) -> Result<serde_json::Value, Box<dy
31
31
  /// Parse simple URL-encoded data (no brackets) while preserving empty strings
32
32
  fn parse_urlencoded_simple(data: &[u8]) -> serde_json::Value {
33
33
  use rustc_hash::FxHashMap;
34
- use urlencoding::decode;
35
34
 
36
35
  let mut array_map: FxHashMap<String, Vec<serde_json::Value>> = FxHashMap::default();
37
36
 
38
- let body_str = String::from_utf8_lossy(data);
39
- let body_str = body_str.replace('+', " ");
40
-
41
- for pair in body_str.split('&') {
42
- if pair.is_empty() {
43
- continue;
44
- }
45
-
46
- let (key, value) = if let Some((k, v)) = pair.split_once('=') {
47
- (
48
- decode(k).unwrap_or_default().to_string(),
49
- decode(v).unwrap_or_default().to_string(),
50
- )
51
- } else {
52
- (pair.to_string(), String::new())
53
- };
54
-
37
+ for (key, value) in form_urlencoded::parse(data) {
55
38
  let json_value = convert_string_to_json_value(&value);
39
+ array_map.entry(key.into_owned()).or_default().push(json_value);
40
+ }
56
41
 
57
- match array_map.get_mut(&key) {
58
- Some(entry) => {
59
- entry.push(json_value);
60
- }
61
- None => {
62
- array_map.insert(key, vec![json_value]);
63
- }
42
+ let mut obj = serde_json::Map::with_capacity(array_map.len());
43
+ for (key, mut values) in array_map {
44
+ if values.len() == 1 {
45
+ obj.insert(key, values.pop().unwrap_or(serde_json::Value::Null));
46
+ } else {
47
+ obj.insert(key, serde_json::Value::Array(values));
64
48
  }
65
49
  }
66
-
67
- array_map
68
- .iter()
69
- .map(|(key, value)| {
70
- if value.len() == 1 {
71
- (key, value[0].clone())
72
- } else {
73
- (key, serde_json::Value::Array(value.clone()))
74
- }
75
- })
76
- .collect::<serde_json::Value>()
50
+ serde_json::Value::Object(obj)
77
51
  }
78
52
 
79
53
  /// Try to parse a string as an integer
@@ -90,10 +64,12 @@ fn try_parse_float(s: &str) -> Option<serde_json::Value> {
90
64
 
91
65
  /// Try to parse a string as a boolean (true/false, case-insensitive)
92
66
  fn try_parse_boolean(s: &str) -> Option<serde_json::Value> {
93
- match s.to_lowercase().as_str() {
94
- "true" => Some(serde_json::Value::Bool(true)),
95
- "false" => Some(serde_json::Value::Bool(false)),
96
- _ => None,
67
+ if s.eq_ignore_ascii_case("true") {
68
+ Some(serde_json::Value::Bool(true))
69
+ } else if s.eq_ignore_ascii_case("false") {
70
+ Some(serde_json::Value::Bool(false))
71
+ } else {
72
+ None
97
73
  }
98
74
  }
99
75
 
@@ -145,3 +121,393 @@ fn convert_types_recursive(value: &mut serde_json::Value) {
145
121
  _ => {}
146
122
  }
147
123
  }
124
+
125
+ #[cfg(test)]
126
+ mod tests {
127
+ use super::*;
128
+
129
+ #[test]
130
+ fn test_simple_key_value() {
131
+ let data = b"name=value";
132
+ let result = parse_urlencoded_to_json(data).unwrap();
133
+
134
+ assert!(result.is_object());
135
+ assert_eq!(result["name"], "value");
136
+ }
137
+
138
+ #[test]
139
+ fn test_array_notation() {
140
+ let data = b"tags[]=a&tags[]=b&tags[]=c";
141
+ let result = parse_urlencoded_to_json(data).unwrap();
142
+
143
+ assert!(result["tags"].is_array());
144
+ let tags = result["tags"].as_array().unwrap();
145
+ assert_eq!(tags.len(), 3);
146
+ assert_eq!(tags[0], "a");
147
+ assert_eq!(tags[1], "b");
148
+ assert_eq!(tags[2], "c");
149
+ }
150
+
151
+ #[test]
152
+ fn test_nested_objects() {
153
+ let data = b"profile[name]=John&profile[age]=30";
154
+ let result = parse_urlencoded_to_json(data).unwrap();
155
+
156
+ assert!(result["profile"].is_object());
157
+ assert_eq!(result["profile"]["name"], "John");
158
+ assert_eq!(result["profile"]["age"], 30);
159
+ }
160
+
161
+ #[test]
162
+ fn test_type_conversion_integers() {
163
+ let data = b"age=30&count=1000&id=12345";
164
+ let result = parse_urlencoded_to_json(data).unwrap();
165
+
166
+ assert!(result["age"].is_number());
167
+ assert_eq!(result["age"], 30);
168
+ assert!(result["count"].is_number());
169
+ assert_eq!(result["count"], 1000);
170
+ assert!(result["id"].is_number());
171
+ assert_eq!(result["id"], 12345);
172
+ }
173
+
174
+ #[test]
175
+ fn test_type_conversion_booleans() {
176
+ let data = b"active=true&enabled=false&visible=True&disabled=False";
177
+ let result = parse_urlencoded_to_json(data).unwrap();
178
+
179
+ assert!(result["active"].is_boolean());
180
+ assert_eq!(result["active"], true);
181
+ assert!(result["enabled"].is_boolean());
182
+ assert_eq!(result["enabled"], false);
183
+ assert!(result["visible"].is_boolean());
184
+ assert_eq!(result["visible"], true);
185
+ assert!(result["disabled"].is_boolean());
186
+ assert_eq!(result["disabled"], false);
187
+ }
188
+
189
+ #[test]
190
+ fn test_type_conversion_floats() {
191
+ let data = b"price=19.99&rating=4.5&discount=0.25";
192
+ let result = parse_urlencoded_to_json(data).unwrap();
193
+
194
+ assert!(result["price"].is_number());
195
+ assert_eq!(result["price"], 19.99);
196
+ assert!(result["rating"].is_number());
197
+ assert_eq!(result["rating"], 4.5);
198
+ assert!(result["discount"].is_number());
199
+ assert_eq!(result["discount"], 0.25);
200
+ }
201
+
202
+ #[test]
203
+ fn test_multiple_values_same_key() {
204
+ let data = b"tags=a&tags=b&tags=c";
205
+ let result = parse_urlencoded_to_json(data).unwrap();
206
+
207
+ assert!(result["tags"].is_array());
208
+ let tags = result["tags"].as_array().unwrap();
209
+ assert_eq!(tags.len(), 3);
210
+ assert_eq!(tags[0], "a");
211
+ assert_eq!(tags[1], "b");
212
+ assert_eq!(tags[2], "c");
213
+ }
214
+
215
+ #[test]
216
+ fn test_empty_strings() {
217
+ let data = b"name=&description=&value=";
218
+ let result = parse_urlencoded_to_json(data).unwrap();
219
+
220
+ assert_eq!(result["name"], "");
221
+ assert_eq!(result["description"], "");
222
+ assert_eq!(result["value"], "");
223
+ }
224
+
225
+ #[test]
226
+ fn test_url_encoded_spaces() {
227
+ let data = b"name=John+Doe&message=Hello%20World";
228
+ let result = parse_urlencoded_to_json(data).unwrap();
229
+
230
+ assert_eq!(result["name"], "John Doe");
231
+ assert_eq!(result["message"], "Hello World");
232
+ }
233
+
234
+ #[test]
235
+ fn test_url_encoded_special_chars() {
236
+ let data = b"email=user%40example.com&url=https%3A%2F%2Fexample.com";
237
+ let result = parse_urlencoded_to_json(data).unwrap();
238
+
239
+ assert_eq!(result["email"], "user@example.com");
240
+ assert_eq!(result["url"], "https://example.com");
241
+ }
242
+
243
+ #[test]
244
+ fn test_null_value() {
245
+ let data = b"value=null&other=something";
246
+ let result = parse_urlencoded_to_json(data).unwrap();
247
+
248
+ assert!(result["value"].is_null());
249
+ assert_eq!(result["other"], "something");
250
+ }
251
+
252
+ #[test]
253
+ fn test_multiple_fields() {
254
+ let data = b"username=john&password=secret123&remember=true&age=28";
255
+ let result = parse_urlencoded_to_json(data).unwrap();
256
+
257
+ assert_eq!(result["username"], "john");
258
+ assert_eq!(result["password"], "secret123");
259
+ assert_eq!(result["remember"], true);
260
+ assert_eq!(result["age"], 28);
261
+ }
262
+
263
+ #[test]
264
+ fn test_convert_string_to_json_value_integer() {
265
+ let value = convert_string_to_json_value("42");
266
+ assert_eq!(value, 42);
267
+ }
268
+
269
+ #[test]
270
+ fn test_convert_string_to_json_value_float() {
271
+ let value = convert_string_to_json_value("3.14");
272
+ assert_eq!(value, 3.14);
273
+ }
274
+
275
+ #[test]
276
+ fn test_convert_string_to_json_value_boolean_true() {
277
+ let value = convert_string_to_json_value("true");
278
+ assert_eq!(value, true);
279
+ }
280
+
281
+ #[test]
282
+ fn test_convert_string_to_json_value_boolean_false() {
283
+ let value = convert_string_to_json_value("false");
284
+ assert_eq!(value, false);
285
+ }
286
+
287
+ #[test]
288
+ fn test_convert_string_to_json_value_null() {
289
+ let value = convert_string_to_json_value("null");
290
+ assert!(value.is_null());
291
+ }
292
+
293
+ #[test]
294
+ fn test_convert_string_to_json_value_empty_string() {
295
+ let value = convert_string_to_json_value("");
296
+ assert_eq!(value, "");
297
+ }
298
+
299
+ #[test]
300
+ fn test_convert_string_to_json_value_regular_string() {
301
+ let value = convert_string_to_json_value("hello");
302
+ assert_eq!(value, "hello");
303
+ }
304
+
305
+ #[test]
306
+ fn test_try_parse_integer() {
307
+ assert!(try_parse_integer("123").is_some());
308
+ assert!(try_parse_integer("-456").is_some());
309
+ assert!(try_parse_integer("0").is_some());
310
+ assert!(try_parse_integer("abc").is_none());
311
+ assert!(try_parse_integer("12.34").is_none());
312
+ }
313
+
314
+ #[test]
315
+ fn test_try_parse_float() {
316
+ assert!(try_parse_float("3.14").is_some());
317
+ assert!(try_parse_float("10.0").is_some());
318
+ assert!(try_parse_float("-2.5").is_some());
319
+ assert!(try_parse_float("abc").is_none());
320
+ assert!(try_parse_float("123").is_some());
321
+ }
322
+
323
+ #[test]
324
+ fn test_try_parse_boolean() {
325
+ assert_eq!(try_parse_boolean("true"), Some(serde_json::Value::Bool(true)));
326
+ assert_eq!(try_parse_boolean("false"), Some(serde_json::Value::Bool(false)));
327
+ assert_eq!(try_parse_boolean("True"), Some(serde_json::Value::Bool(true)));
328
+ assert_eq!(try_parse_boolean("FALSE"), Some(serde_json::Value::Bool(false)));
329
+ assert_eq!(try_parse_boolean("1"), None);
330
+ assert_eq!(try_parse_boolean("yes"), None);
331
+ }
332
+
333
+ #[test]
334
+ fn test_mixed_types_in_array() {
335
+ let data = b"values=1&values=hello&values=2.5&values=true";
336
+ let result = parse_urlencoded_to_json(data).unwrap();
337
+
338
+ assert!(result["values"].is_array());
339
+ let values = result["values"].as_array().unwrap();
340
+ assert_eq!(values.len(), 4);
341
+ assert_eq!(values[0], 1);
342
+ assert_eq!(values[1], "hello");
343
+ assert_eq!(values[2], 2.5);
344
+ assert_eq!(values[3], true);
345
+ }
346
+
347
+ #[test]
348
+ fn test_deeply_nested_objects() {
349
+ let data = b"user[profile]=test&user[id]=123";
350
+ let result = parse_urlencoded_to_json(data).unwrap();
351
+
352
+ assert!(result["user"].is_object());
353
+ assert_eq!(result["user"]["profile"], "test");
354
+ assert_eq!(result["user"]["id"], 123);
355
+ }
356
+
357
+ #[test]
358
+ fn test_single_key_without_value() {
359
+ let data = b"key";
360
+ let result = parse_urlencoded_to_json(data).unwrap();
361
+
362
+ assert_eq!(result["key"], "");
363
+ }
364
+
365
+ #[test]
366
+ fn test_empty_form_data() {
367
+ let data = b"";
368
+ let result = parse_urlencoded_to_json(data).unwrap();
369
+
370
+ assert!(result.is_object());
371
+ assert!(result.as_object().unwrap().is_empty());
372
+ }
373
+
374
+ #[test]
375
+ fn test_negative_numbers() {
376
+ let data = b"temp=-15&balance=-1000.50";
377
+ let result = parse_urlencoded_to_json(data).unwrap();
378
+
379
+ assert_eq!(result["temp"], -15);
380
+ assert_eq!(result["balance"], -1000.50);
381
+ }
382
+
383
+ #[test]
384
+ fn test_large_numbers() {
385
+ let data = b"big=9223372036854775807&decimal=999999.99";
386
+ let result = parse_urlencoded_to_json(data).unwrap();
387
+
388
+ assert!(result["big"].is_number());
389
+ assert!(result["decimal"].is_number());
390
+ }
391
+
392
+ #[test]
393
+ fn test_unicode_values() {
394
+ let data = "name=José&city=São+Paulo".as_bytes();
395
+ let result = parse_urlencoded_to_json(data).unwrap();
396
+
397
+ assert_eq!(result["name"], "José");
398
+ assert_eq!(result["city"], "São Paulo");
399
+ }
400
+
401
+ #[test]
402
+ fn test_ampersand_escaping() {
403
+ let data = b"text=A%26B&code=1%262";
404
+ let result = parse_urlencoded_to_json(data).unwrap();
405
+
406
+ assert_eq!(result["text"], "A&B");
407
+ assert_eq!(result["code"], "1&2");
408
+ }
409
+
410
+ #[test]
411
+ fn test_equals_sign_escaping() {
412
+ let data = b"expression=1%3D1&formula=2%3D2";
413
+ let result = parse_urlencoded_to_json(data).unwrap();
414
+
415
+ assert_eq!(result["expression"], "1=1");
416
+ assert_eq!(result["formula"], "2=2");
417
+ }
418
+
419
+ #[test]
420
+ fn test_leading_zeros_in_numbers() {
421
+ let data = b"code=00123&id=007";
422
+ let result = parse_urlencoded_to_json(data).unwrap();
423
+
424
+ assert!(result["code"].is_number());
425
+ assert!(result["id"].is_number());
426
+ }
427
+
428
+ #[test]
429
+ fn test_boolean_case_insensitivity() {
430
+ let data = b"a=true&b=TRUE&c=True&d=false&e=FALSE&f=False";
431
+ let result = parse_urlencoded_to_json(data).unwrap();
432
+
433
+ assert_eq!(result["a"], true);
434
+ assert_eq!(result["b"], true);
435
+ assert_eq!(result["c"], true);
436
+ assert_eq!(result["d"], false);
437
+ assert_eq!(result["e"], false);
438
+ assert_eq!(result["f"], false);
439
+ }
440
+
441
+ #[test]
442
+ fn test_array_with_mixed_content_and_objects() {
443
+ let data = b"items[]=1&items[]=text&categories[]=a&categories[]=b";
444
+ let result = parse_urlencoded_to_json(data).unwrap();
445
+
446
+ assert!(result["items"].is_array());
447
+ assert!(result["categories"].is_array());
448
+ let items = result["items"].as_array().unwrap();
449
+ assert_eq!(items[0], 1);
450
+ assert_eq!(items[1], "text");
451
+ let categories = result["categories"].as_array().unwrap();
452
+ assert_eq!(categories[0], "a");
453
+ assert_eq!(categories[1], "b");
454
+ }
455
+
456
+ #[test]
457
+ fn test_scientific_notation() {
458
+ let data = b"value=1e5&small=1e-3";
459
+ let result = parse_urlencoded_to_json(data).unwrap();
460
+
461
+ assert!(result["value"].is_number());
462
+ assert!(result["small"].is_number());
463
+ }
464
+
465
+ #[test]
466
+ fn test_plus_vs_space_encoding() {
467
+ let data = b"message=Hello+World&greeting=Hi%20There";
468
+ let result = parse_urlencoded_to_json(data).unwrap();
469
+
470
+ assert_eq!(result["message"], "Hello World");
471
+ assert_eq!(result["greeting"], "Hi There");
472
+ }
473
+
474
+ #[test]
475
+ fn test_no_value_key_in_middle() {
476
+ let data = b"a=1&empty&c=3";
477
+ let result = parse_urlencoded_to_json(data).unwrap();
478
+
479
+ assert_eq!(result["a"], 1);
480
+ assert_eq!(result["empty"], "");
481
+ assert_eq!(result["c"], 3);
482
+ }
483
+
484
+ #[test]
485
+ fn test_consecutive_ampersands() {
486
+ let data = b"a=1&&b=2";
487
+ let result = parse_urlencoded_to_json(data).unwrap();
488
+
489
+ assert_eq!(result["a"], 1);
490
+ assert_eq!(result["b"], 2);
491
+ assert_eq!(result.as_object().unwrap().len(), 2);
492
+ }
493
+
494
+ #[test]
495
+ fn test_trailing_ampersand() {
496
+ let data = b"a=1&b=2&";
497
+ let result = parse_urlencoded_to_json(data).unwrap();
498
+
499
+ assert_eq!(result["a"], 1);
500
+ assert_eq!(result["b"], 2);
501
+ assert_eq!(result.as_object().unwrap().len(), 2);
502
+ }
503
+
504
+ #[test]
505
+ fn test_leading_ampersand() {
506
+ let data = b"&a=1&b=2";
507
+ let result = parse_urlencoded_to_json(data).unwrap();
508
+
509
+ assert_eq!(result["a"], 1);
510
+ assert_eq!(result["b"], 2);
511
+ assert_eq!(result.as_object().unwrap().len(), 2);
512
+ }
513
+ }