spikard 0.4.0-arm64-darwin-23

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,541 @@
1
+ //! URL-encoded form data parsing
2
+
3
+ use std::collections::HashMap;
4
+
5
+ /// Parse URL-encoded form data to JSON
6
+ ///
7
+ /// This handles:
8
+ /// - Array notation: tags[]=value1&tags[]=value2 → {"tags": ["value1", "value2"]}
9
+ /// - Nested objects: profile[name]=John → {"profile": {"name": "John"}}
10
+ /// - Type conversion: age=30 → {"age": 30}, active=true → {"active": true}
11
+ /// - Multiple values: tags=a&tags=b → {"tags": ["a", "b"]}
12
+ /// - Empty strings: Preserved as empty strings (unlike query parameter parsing)
13
+ ///
14
+ /// Strategy:
15
+ /// - If brackets present → use serde_qs (handles nested objects, arrays with [])
16
+ /// - Otherwise → use custom parser that preserves empty strings and handles duplicate keys
17
+ 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('[') {
21
+ let config = serde_qs::Config::new(10, false);
22
+ let parsed: HashMap<String, serde_json::Value> = config.deserialize_str(body_str)?;
23
+ let mut json_value = serde_json::to_value(parsed)?;
24
+ convert_types_recursive(&mut json_value);
25
+ Ok(json_value)
26
+ } else {
27
+ Ok(parse_urlencoded_simple(data))
28
+ }
29
+ }
30
+
31
+ /// Parse simple URL-encoded data (no brackets) while preserving empty strings
32
+ fn parse_urlencoded_simple(data: &[u8]) -> serde_json::Value {
33
+ use rustc_hash::FxHashMap;
34
+ use urlencoding::decode;
35
+
36
+ let mut array_map: FxHashMap<String, Vec<serde_json::Value>> = FxHashMap::default();
37
+
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
+
55
+ let json_value = convert_string_to_json_value(&value);
56
+
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
+ }
64
+ }
65
+ }
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>()
77
+ }
78
+
79
+ /// Try to parse a string as an integer
80
+ fn try_parse_integer(s: &str) -> Option<serde_json::Value> {
81
+ s.parse::<i64>().ok().map(|i| serde_json::Value::Number(i.into()))
82
+ }
83
+
84
+ /// Try to parse a string as a float
85
+ fn try_parse_float(s: &str) -> Option<serde_json::Value> {
86
+ s.parse::<f64>()
87
+ .ok()
88
+ .and_then(|f| serde_json::Number::from_f64(f).map(serde_json::Value::Number))
89
+ }
90
+
91
+ /// Try to parse a string as a boolean (true/false, case-insensitive)
92
+ 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,
97
+ }
98
+ }
99
+
100
+ /// Convert a string value to appropriate JSON type while preserving empty strings
101
+ pub fn convert_string_to_json_value(s: &str) -> serde_json::Value {
102
+ if s.is_empty() {
103
+ return serde_json::Value::String(String::new());
104
+ }
105
+
106
+ try_parse_integer(s)
107
+ .or_else(|| try_parse_float(s))
108
+ .or_else(|| try_parse_boolean(s))
109
+ .or_else(|| {
110
+ if s == "null" {
111
+ Some(serde_json::Value::Null)
112
+ } else {
113
+ None
114
+ }
115
+ })
116
+ .unwrap_or_else(|| serde_json::Value::String(s.to_string()))
117
+ }
118
+
119
+ /// Recursively convert string values to appropriate types (numbers, booleans)
120
+ /// while preserving empty strings
121
+ fn convert_types_recursive(value: &mut serde_json::Value) {
122
+ match value {
123
+ serde_json::Value::String(s) => {
124
+ if s.is_empty() {
125
+ return;
126
+ }
127
+
128
+ if let Some(parsed) = try_parse_integer(s)
129
+ .or_else(|| try_parse_float(s))
130
+ .or_else(|| try_parse_boolean(s))
131
+ {
132
+ *value = parsed;
133
+ }
134
+ }
135
+ serde_json::Value::Array(arr) => {
136
+ for item in arr.iter_mut() {
137
+ convert_types_recursive(item);
138
+ }
139
+ }
140
+ serde_json::Value::Object(obj) => {
141
+ for (_, v) in obj.iter_mut() {
142
+ convert_types_recursive(v);
143
+ }
144
+ }
145
+ _ => {}
146
+ }
147
+ }
148
+
149
+ #[cfg(test)]
150
+ mod tests {
151
+ use super::*;
152
+
153
+ #[test]
154
+ fn test_simple_key_value() {
155
+ let data = b"name=value";
156
+ let result = parse_urlencoded_to_json(data).unwrap();
157
+
158
+ assert!(result.is_object());
159
+ assert_eq!(result["name"], "value");
160
+ }
161
+
162
+ #[test]
163
+ fn test_array_notation() {
164
+ let data = b"tags[]=a&tags[]=b&tags[]=c";
165
+ let result = parse_urlencoded_to_json(data).unwrap();
166
+
167
+ assert!(result["tags"].is_array());
168
+ let tags = result["tags"].as_array().unwrap();
169
+ assert_eq!(tags.len(), 3);
170
+ assert_eq!(tags[0], "a");
171
+ assert_eq!(tags[1], "b");
172
+ assert_eq!(tags[2], "c");
173
+ }
174
+
175
+ #[test]
176
+ fn test_nested_objects() {
177
+ let data = b"profile[name]=John&profile[age]=30";
178
+ let result = parse_urlencoded_to_json(data).unwrap();
179
+
180
+ assert!(result["profile"].is_object());
181
+ assert_eq!(result["profile"]["name"], "John");
182
+ assert_eq!(result["profile"]["age"], 30);
183
+ }
184
+
185
+ #[test]
186
+ fn test_type_conversion_integers() {
187
+ let data = b"age=30&count=1000&id=12345";
188
+ let result = parse_urlencoded_to_json(data).unwrap();
189
+
190
+ assert!(result["age"].is_number());
191
+ assert_eq!(result["age"], 30);
192
+ assert!(result["count"].is_number());
193
+ assert_eq!(result["count"], 1000);
194
+ assert!(result["id"].is_number());
195
+ assert_eq!(result["id"], 12345);
196
+ }
197
+
198
+ #[test]
199
+ fn test_type_conversion_booleans() {
200
+ let data = b"active=true&enabled=false&visible=True&disabled=False";
201
+ let result = parse_urlencoded_to_json(data).unwrap();
202
+
203
+ assert!(result["active"].is_boolean());
204
+ assert_eq!(result["active"], true);
205
+ assert!(result["enabled"].is_boolean());
206
+ assert_eq!(result["enabled"], false);
207
+ assert!(result["visible"].is_boolean());
208
+ assert_eq!(result["visible"], true);
209
+ assert!(result["disabled"].is_boolean());
210
+ assert_eq!(result["disabled"], false);
211
+ }
212
+
213
+ #[test]
214
+ fn test_type_conversion_floats() {
215
+ let data = b"price=19.99&rating=4.5&discount=0.25";
216
+ let result = parse_urlencoded_to_json(data).unwrap();
217
+
218
+ assert!(result["price"].is_number());
219
+ assert_eq!(result["price"], 19.99);
220
+ assert!(result["rating"].is_number());
221
+ assert_eq!(result["rating"], 4.5);
222
+ assert!(result["discount"].is_number());
223
+ assert_eq!(result["discount"], 0.25);
224
+ }
225
+
226
+ #[test]
227
+ fn test_multiple_values_same_key() {
228
+ let data = b"tags=a&tags=b&tags=c";
229
+ let result = parse_urlencoded_to_json(data).unwrap();
230
+
231
+ assert!(result["tags"].is_array());
232
+ let tags = result["tags"].as_array().unwrap();
233
+ assert_eq!(tags.len(), 3);
234
+ assert_eq!(tags[0], "a");
235
+ assert_eq!(tags[1], "b");
236
+ assert_eq!(tags[2], "c");
237
+ }
238
+
239
+ #[test]
240
+ fn test_empty_strings() {
241
+ let data = b"name=&description=&value=";
242
+ let result = parse_urlencoded_to_json(data).unwrap();
243
+
244
+ assert_eq!(result["name"], "");
245
+ assert_eq!(result["description"], "");
246
+ assert_eq!(result["value"], "");
247
+ }
248
+
249
+ #[test]
250
+ fn test_url_encoded_spaces() {
251
+ let data = b"name=John+Doe&message=Hello%20World";
252
+ let result = parse_urlencoded_to_json(data).unwrap();
253
+
254
+ assert_eq!(result["name"], "John Doe");
255
+ assert_eq!(result["message"], "Hello World");
256
+ }
257
+
258
+ #[test]
259
+ fn test_url_encoded_special_chars() {
260
+ let data = b"email=user%40example.com&url=https%3A%2F%2Fexample.com";
261
+ let result = parse_urlencoded_to_json(data).unwrap();
262
+
263
+ assert_eq!(result["email"], "user@example.com");
264
+ assert_eq!(result["url"], "https://example.com");
265
+ }
266
+
267
+ #[test]
268
+ fn test_null_value() {
269
+ let data = b"value=null&other=something";
270
+ let result = parse_urlencoded_to_json(data).unwrap();
271
+
272
+ assert!(result["value"].is_null());
273
+ assert_eq!(result["other"], "something");
274
+ }
275
+
276
+ #[test]
277
+ fn test_multiple_fields() {
278
+ let data = b"username=john&password=secret123&remember=true&age=28";
279
+ let result = parse_urlencoded_to_json(data).unwrap();
280
+
281
+ assert_eq!(result["username"], "john");
282
+ assert_eq!(result["password"], "secret123");
283
+ assert_eq!(result["remember"], true);
284
+ assert_eq!(result["age"], 28);
285
+ }
286
+
287
+ #[test]
288
+ fn test_convert_string_to_json_value_integer() {
289
+ let value = convert_string_to_json_value("42");
290
+ assert_eq!(value, 42);
291
+ }
292
+
293
+ #[test]
294
+ fn test_convert_string_to_json_value_float() {
295
+ let value = convert_string_to_json_value("3.14");
296
+ assert_eq!(value, 3.14);
297
+ }
298
+
299
+ #[test]
300
+ fn test_convert_string_to_json_value_boolean_true() {
301
+ let value = convert_string_to_json_value("true");
302
+ assert_eq!(value, true);
303
+ }
304
+
305
+ #[test]
306
+ fn test_convert_string_to_json_value_boolean_false() {
307
+ let value = convert_string_to_json_value("false");
308
+ assert_eq!(value, false);
309
+ }
310
+
311
+ #[test]
312
+ fn test_convert_string_to_json_value_null() {
313
+ let value = convert_string_to_json_value("null");
314
+ assert!(value.is_null());
315
+ }
316
+
317
+ #[test]
318
+ fn test_convert_string_to_json_value_empty_string() {
319
+ let value = convert_string_to_json_value("");
320
+ assert_eq!(value, "");
321
+ }
322
+
323
+ #[test]
324
+ fn test_convert_string_to_json_value_regular_string() {
325
+ let value = convert_string_to_json_value("hello");
326
+ assert_eq!(value, "hello");
327
+ }
328
+
329
+ #[test]
330
+ fn test_try_parse_integer() {
331
+ assert!(try_parse_integer("123").is_some());
332
+ assert!(try_parse_integer("-456").is_some());
333
+ assert!(try_parse_integer("0").is_some());
334
+ assert!(try_parse_integer("abc").is_none());
335
+ assert!(try_parse_integer("12.34").is_none());
336
+ }
337
+
338
+ #[test]
339
+ fn test_try_parse_float() {
340
+ assert!(try_parse_float("3.14").is_some());
341
+ assert!(try_parse_float("10.0").is_some());
342
+ assert!(try_parse_float("-2.5").is_some());
343
+ assert!(try_parse_float("abc").is_none());
344
+ // Note: integers parse as floats too (f64::parse accepts "123")
345
+ assert!(try_parse_float("123").is_some());
346
+ }
347
+
348
+ #[test]
349
+ fn test_try_parse_boolean() {
350
+ assert_eq!(try_parse_boolean("true"), Some(serde_json::Value::Bool(true)));
351
+ assert_eq!(try_parse_boolean("false"), Some(serde_json::Value::Bool(false)));
352
+ assert_eq!(try_parse_boolean("True"), Some(serde_json::Value::Bool(true)));
353
+ assert_eq!(try_parse_boolean("FALSE"), Some(serde_json::Value::Bool(false)));
354
+ assert_eq!(try_parse_boolean("1"), None);
355
+ assert_eq!(try_parse_boolean("yes"), None);
356
+ }
357
+
358
+ #[test]
359
+ fn test_mixed_types_in_array() {
360
+ let data = b"values=1&values=hello&values=2.5&values=true";
361
+ let result = parse_urlencoded_to_json(data).unwrap();
362
+
363
+ assert!(result["values"].is_array());
364
+ let values = result["values"].as_array().unwrap();
365
+ assert_eq!(values.len(), 4);
366
+ assert_eq!(values[0], 1);
367
+ assert_eq!(values[1], "hello");
368
+ assert_eq!(values[2], 2.5);
369
+ assert_eq!(values[3], true);
370
+ }
371
+
372
+ #[test]
373
+ fn test_deeply_nested_objects() {
374
+ // Test single level nesting - serde_qs supports profile[name] style
375
+ let data = b"user[profile]=test&user[id]=123";
376
+ let result = parse_urlencoded_to_json(data).unwrap();
377
+
378
+ assert!(result["user"].is_object());
379
+ assert_eq!(result["user"]["profile"], "test");
380
+ assert_eq!(result["user"]["id"], 123);
381
+ }
382
+
383
+ #[test]
384
+ fn test_single_key_without_value() {
385
+ let data = b"key";
386
+ let result = parse_urlencoded_to_json(data).unwrap();
387
+
388
+ assert_eq!(result["key"], "");
389
+ }
390
+
391
+ #[test]
392
+ fn test_empty_form_data() {
393
+ let data = b"";
394
+ let result = parse_urlencoded_to_json(data).unwrap();
395
+
396
+ assert!(result.is_object());
397
+ assert!(result.as_object().unwrap().is_empty());
398
+ }
399
+
400
+ #[test]
401
+ fn test_negative_numbers() {
402
+ let data = b"temp=-15&balance=-1000.50";
403
+ let result = parse_urlencoded_to_json(data).unwrap();
404
+
405
+ assert_eq!(result["temp"], -15);
406
+ assert_eq!(result["balance"], -1000.50);
407
+ }
408
+
409
+ #[test]
410
+ fn test_large_numbers() {
411
+ let data = b"big=9223372036854775807&decimal=999999.99";
412
+ let result = parse_urlencoded_to_json(data).unwrap();
413
+
414
+ assert!(result["big"].is_number());
415
+ assert!(result["decimal"].is_number());
416
+ }
417
+
418
+ #[test]
419
+ fn test_unicode_values() {
420
+ let data = "name=José&city=São+Paulo".as_bytes();
421
+ let result = parse_urlencoded_to_json(data).unwrap();
422
+
423
+ assert_eq!(result["name"], "José");
424
+ assert_eq!(result["city"], "São Paulo");
425
+ }
426
+
427
+ #[test]
428
+ fn test_ampersand_escaping() {
429
+ let data = b"text=A%26B&code=1%262";
430
+ let result = parse_urlencoded_to_json(data).unwrap();
431
+
432
+ assert_eq!(result["text"], "A&B");
433
+ assert_eq!(result["code"], "1&2");
434
+ }
435
+
436
+ #[test]
437
+ fn test_equals_sign_escaping() {
438
+ let data = b"expression=1%3D1&formula=2%3D2";
439
+ let result = parse_urlencoded_to_json(data).unwrap();
440
+
441
+ assert_eq!(result["expression"], "1=1");
442
+ assert_eq!(result["formula"], "2=2");
443
+ }
444
+
445
+ #[test]
446
+ fn test_leading_zeros_in_numbers() {
447
+ let data = b"code=00123&id=007";
448
+ let result = parse_urlencoded_to_json(data).unwrap();
449
+
450
+ // These should parse as integers, stripping leading zeros
451
+ assert!(result["code"].is_number());
452
+ assert!(result["id"].is_number());
453
+ }
454
+
455
+ #[test]
456
+ fn test_boolean_case_insensitivity() {
457
+ let data = b"a=true&b=TRUE&c=True&d=false&e=FALSE&f=False";
458
+ let result = parse_urlencoded_to_json(data).unwrap();
459
+
460
+ assert_eq!(result["a"], true);
461
+ assert_eq!(result["b"], true);
462
+ assert_eq!(result["c"], true);
463
+ assert_eq!(result["d"], false);
464
+ assert_eq!(result["e"], false);
465
+ assert_eq!(result["f"], false);
466
+ }
467
+
468
+ #[test]
469
+ fn test_array_with_mixed_content_and_objects() {
470
+ let data = b"items[]=1&items[]=text&categories[]=a&categories[]=b";
471
+ let result = parse_urlencoded_to_json(data).unwrap();
472
+
473
+ assert!(result["items"].is_array());
474
+ assert!(result["categories"].is_array());
475
+ let items = result["items"].as_array().unwrap();
476
+ assert_eq!(items[0], 1);
477
+ assert_eq!(items[1], "text");
478
+ let categories = result["categories"].as_array().unwrap();
479
+ assert_eq!(categories[0], "a");
480
+ assert_eq!(categories[1], "b");
481
+ }
482
+
483
+ #[test]
484
+ fn test_scientific_notation() {
485
+ let data = b"value=1e5&small=1e-3";
486
+ let result = parse_urlencoded_to_json(data).unwrap();
487
+
488
+ assert!(result["value"].is_number());
489
+ assert!(result["small"].is_number());
490
+ }
491
+
492
+ #[test]
493
+ fn test_plus_vs_space_encoding() {
494
+ // Plus signs should be converted to spaces
495
+ let data = b"message=Hello+World&greeting=Hi%20There";
496
+ let result = parse_urlencoded_to_json(data).unwrap();
497
+
498
+ assert_eq!(result["message"], "Hello World");
499
+ assert_eq!(result["greeting"], "Hi There");
500
+ }
501
+
502
+ #[test]
503
+ fn test_no_value_key_in_middle() {
504
+ let data = b"a=1&empty&c=3";
505
+ let result = parse_urlencoded_to_json(data).unwrap();
506
+
507
+ assert_eq!(result["a"], 1);
508
+ assert_eq!(result["empty"], "");
509
+ assert_eq!(result["c"], 3);
510
+ }
511
+
512
+ #[test]
513
+ fn test_consecutive_ampersands() {
514
+ let data = b"a=1&&b=2";
515
+ let result = parse_urlencoded_to_json(data).unwrap();
516
+
517
+ assert_eq!(result["a"], 1);
518
+ assert_eq!(result["b"], 2);
519
+ assert_eq!(result.as_object().unwrap().len(), 2);
520
+ }
521
+
522
+ #[test]
523
+ fn test_trailing_ampersand() {
524
+ let data = b"a=1&b=2&";
525
+ let result = parse_urlencoded_to_json(data).unwrap();
526
+
527
+ assert_eq!(result["a"], 1);
528
+ assert_eq!(result["b"], 2);
529
+ assert_eq!(result.as_object().unwrap().len(), 2);
530
+ }
531
+
532
+ #[test]
533
+ fn test_leading_ampersand() {
534
+ let data = b"&a=1&b=2";
535
+ let result = parse_urlencoded_to_json(data).unwrap();
536
+
537
+ assert_eq!(result["a"], 1);
538
+ assert_eq!(result["b"], 2);
539
+ assert_eq!(result.as_object().unwrap().len(), 2);
540
+ }
541
+ }