spikard 0.10.2 → 0.11.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 98c245226b99792bbaa3b484e9a7885e54079ae2d5da49da5b6578865f425b11
4
- data.tar.gz: 7004262d2dcf5449c790cc4142212815b04c80dbeee5c14d58473f3fb5cce3fc
3
+ metadata.gz: ddca560615174394e71fc0066ee7749daa3fcc5e91c8387c3abcc6e8685c7f82
4
+ data.tar.gz: 273523e0e3aa0068e95b660744bf4278cfc6b157d61a9d935ac6953da7431fb9
5
5
  SHA512:
6
- metadata.gz: 054c194efe4bb66ec823f8215b82cd884da02b7ccfccc8c911cbcd25c27fa5916b9177e70e2ddd00cb0bb06399d26c0f5405c6d065aff99963c7b115a3cb8505
7
- data.tar.gz: 14a0d1bc7fa21d00c6a195279a0516df4b208d2566feed9e5d7a2383762804ebad8811054667d28b44eeca81285c0ac3d1cd63489ebfa0f7615f229f5c1c2f07
6
+ metadata.gz: 0b6005c4cc1e6f32e2e5b5157874b76b0d797931a792c12bf07324f04a83ba17210fd56ff8a495534056a7ac85bd07bcbc6af487c071c41ff0f6aa76dbfe6e67
7
+ data.tar.gz: 342a8ed2c1d180180701aedb5cc01467eadb3da8317310678b4b9857a002ba09231b5b1c5d6a85fc0f07c1e432a6cb45a97a92fe30c583a5abe406b625494dd2
data/README.md CHANGED
@@ -6,6 +6,7 @@
6
6
  [![npm](https://img.shields.io/npm/v/@spikard/node.svg?color=blue)](https://www.npmjs.com/package/@spikard/node)
7
7
  [![Gem](https://img.shields.io/gem/v/spikard.svg?color=blue)](https://rubygems.org/gems/spikard)
8
8
  [![Packagist](https://img.shields.io/packagist/v/spikard/spikard.svg?color=blue)](https://packagist.org/packages/spikard/spikard)
9
+ [![Hex.pm](https://img.shields.io/hexpm/v/spikard.svg?color=blue)](https://hex.pm/packages/spikard)
9
10
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](../../LICENSE)
10
11
 
11
12
  Ruby bindings for Spikard: a Rust-centric web framework with type-safe code generation from OpenAPI, GraphQL, AsyncAPI, and OpenRPC specifications. Leverage Sinatra-style routing with zero-copy FFI performance.
@@ -246,7 +247,7 @@ Spikard is **1.4x faster** than Roda with significantly lower tail latency.
246
247
  ## Learn More
247
248
 
248
249
  **Examples & Code Generation:**
249
- - [Runnable Examples](../../examples/) - Ruby, Python, TypeScript, PHP, and WASM
250
+ - [Runnable Examples](../../examples/) - Ruby, Python, TypeScript, PHP, Elixir, and Rust
250
251
  - [Code Generation Guide](../../examples/README.md) - Generate from OpenAPI, GraphQL, AsyncAPI, OpenRPC
251
252
 
252
253
  **Documentation:**
@@ -256,8 +257,9 @@ Spikard is **1.4x faster** than Roda with significantly lower tail latency.
256
257
 
257
258
  **Other Languages:**
258
259
  - [Python (PyPI)](https://pypi.org/project/spikard/)
259
- - [TypeScript (npm)](https://www.npmjs.com/package/spikard)
260
+ - [TypeScript (npm)](https://www.npmjs.com/package/@spikard/node)
260
261
  - [PHP (Packagist)](https://packagist.org/packages/spikard/spikard)
262
+ - [Elixir (Hex.pm)](https://hex.pm/packages/spikard)
261
263
  - [Rust (Crates.io)](https://crates.io/crates/spikard)
262
264
 
263
265
  ## License
@@ -63,9 +63,9 @@ dependencies = [
63
63
 
64
64
  [[package]]
65
65
  name = "anyhow"
66
- version = "1.0.100"
66
+ version = "1.0.101"
67
67
  source = "registry+https://github.com/rust-lang/crates.io-index"
68
- checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
68
+ checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
69
69
 
70
70
  [[package]]
71
71
  name = "arbitrary"
@@ -78,9 +78,9 @@ dependencies = [
78
78
 
79
79
  [[package]]
80
80
  name = "async-compression"
81
- version = "0.4.37"
81
+ version = "0.4.39"
82
82
  source = "registry+https://github.com/rust-lang/crates.io-index"
83
- checksum = "d10e4f991a553474232bc0a31799f6d24b034a84c0971d80d2e2f78b2e576e40"
83
+ checksum = "68650b7df54f0293fd061972a0fb05aaf4fc0879d3b3d21a638a182c5c543b9f"
84
84
  dependencies = [
85
85
  "compression-codecs",
86
86
  "compression-core",
@@ -277,9 +277,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
277
277
 
278
278
  [[package]]
279
279
  name = "bitflags"
280
- version = "2.10.0"
280
+ version = "2.11.0"
281
281
  source = "registry+https://github.com/rust-lang/crates.io-index"
282
- checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
282
+ checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
283
283
 
284
284
  [[package]]
285
285
  name = "block-buffer"
@@ -331,9 +331,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
331
331
 
332
332
  [[package]]
333
333
  name = "bytes"
334
- version = "1.11.0"
334
+ version = "1.11.1"
335
335
  source = "registry+https://github.com/rust-lang/crates.io-index"
336
- checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
336
+ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
337
337
 
338
338
  [[package]]
339
339
  name = "bytesize"
@@ -343,9 +343,9 @@ checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3"
343
343
 
344
344
  [[package]]
345
345
  name = "cc"
346
- version = "1.2.55"
346
+ version = "1.2.56"
347
347
  source = "registry+https://github.com/rust-lang/crates.io-index"
348
- checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
348
+ checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
349
349
  dependencies = [
350
350
  "find-msvc-tools",
351
351
  "shlex",
@@ -536,9 +536,9 @@ dependencies = [
536
536
 
537
537
  [[package]]
538
538
  name = "deranged"
539
- version = "0.5.5"
539
+ version = "0.5.6"
540
540
  source = "registry+https://github.com/rust-lang/crates.io-index"
541
- checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
541
+ checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4"
542
542
  dependencies = [
543
543
  "powerfmt",
544
544
  ]
@@ -792,6 +792,12 @@ version = "1.0.7"
792
792
  source = "registry+https://github.com/rust-lang/crates.io-index"
793
793
  checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
794
794
 
795
+ [[package]]
796
+ name = "foldhash"
797
+ version = "0.1.5"
798
+ source = "registry+https://github.com/rust-lang/crates.io-index"
799
+ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
800
+
795
801
  [[package]]
796
802
  name = "foldhash"
797
803
  version = "0.2.0"
@@ -958,6 +964,19 @@ dependencies = [
958
964
  "wasm-bindgen",
959
965
  ]
960
966
 
967
+ [[package]]
968
+ name = "getrandom"
969
+ version = "0.4.1"
970
+ source = "registry+https://github.com/rust-lang/crates.io-index"
971
+ checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec"
972
+ dependencies = [
973
+ "cfg-if",
974
+ "libc",
975
+ "r-efi",
976
+ "wasip2",
977
+ "wasip3",
978
+ ]
979
+
961
980
  [[package]]
962
981
  name = "glob"
963
982
  version = "0.3.3"
@@ -1033,6 +1052,15 @@ version = "0.14.5"
1033
1052
  source = "registry+https://github.com/rust-lang/crates.io-index"
1034
1053
  checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
1035
1054
 
1055
+ [[package]]
1056
+ name = "hashbrown"
1057
+ version = "0.15.5"
1058
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1059
+ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
1060
+ dependencies = [
1061
+ "foldhash 0.1.5",
1062
+ ]
1063
+
1036
1064
  [[package]]
1037
1065
  name = "hashbrown"
1038
1066
  version = "0.16.1"
@@ -1041,9 +1069,15 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
1041
1069
  dependencies = [
1042
1070
  "allocator-api2",
1043
1071
  "equivalent",
1044
- "foldhash",
1072
+ "foldhash 0.2.0",
1045
1073
  ]
1046
1074
 
1075
+ [[package]]
1076
+ name = "heck"
1077
+ version = "0.5.0"
1078
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1079
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
1080
+
1047
1081
  [[package]]
1048
1082
  name = "hkdf"
1049
1083
  version = "0.12.4"
@@ -1151,13 +1185,12 @@ dependencies = [
1151
1185
 
1152
1186
  [[package]]
1153
1187
  name = "hyper-util"
1154
- version = "0.1.19"
1188
+ version = "0.1.20"
1155
1189
  source = "registry+https://github.com/rust-lang/crates.io-index"
1156
- checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
1190
+ checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
1157
1191
  dependencies = [
1158
1192
  "bytes",
1159
1193
  "futures-channel",
1160
- "futures-core",
1161
1194
  "futures-util",
1162
1195
  "http",
1163
1196
  "http-body",
@@ -1275,6 +1308,12 @@ dependencies = [
1275
1308
  "zerovec",
1276
1309
  ]
1277
1310
 
1311
+ [[package]]
1312
+ name = "id-arena"
1313
+ version = "2.3.0"
1314
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1315
+ checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
1316
+
1278
1317
  [[package]]
1279
1318
  name = "idna"
1280
1319
  version = "1.1.0"
@@ -1343,9 +1382,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
1343
1382
 
1344
1383
  [[package]]
1345
1384
  name = "jiff"
1346
- version = "0.2.18"
1385
+ version = "0.2.20"
1347
1386
  source = "registry+https://github.com/rust-lang/crates.io-index"
1348
- checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50"
1387
+ checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543"
1349
1388
  dependencies = [
1350
1389
  "jiff-static",
1351
1390
  "jiff-tzdb-platform",
@@ -1358,9 +1397,9 @@ dependencies = [
1358
1397
 
1359
1398
  [[package]]
1360
1399
  name = "jiff-static"
1361
- version = "0.2.18"
1400
+ version = "0.2.20"
1362
1401
  source = "registry+https://github.com/rust-lang/crates.io-index"
1363
- checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78"
1402
+ checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5"
1364
1403
  dependencies = [
1365
1404
  "proc-macro2",
1366
1405
  "quote",
@@ -1394,9 +1433,9 @@ dependencies = [
1394
1433
 
1395
1434
  [[package]]
1396
1435
  name = "jsonschema"
1397
- version = "0.40.2"
1436
+ version = "0.41.0"
1398
1437
  source = "registry+https://github.com/rust-lang/crates.io-index"
1399
- checksum = "ba783d17473c27cfd4d1d72785dc1c26d5faba8072f50fec4ebea179bec8f33d"
1438
+ checksum = "9320368abda84a3aad44953d61e70515681fe43ae94529dff5567e0215b88438"
1400
1439
  dependencies = [
1401
1440
  "ahash",
1402
1441
  "bytecount",
@@ -1457,11 +1496,17 @@ version = "1.3.0"
1457
1496
  source = "registry+https://github.com/rust-lang/crates.io-index"
1458
1497
  checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
1459
1498
 
1499
+ [[package]]
1500
+ name = "leb128fmt"
1501
+ version = "0.1.0"
1502
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1503
+ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
1504
+
1460
1505
  [[package]]
1461
1506
  name = "libc"
1462
- version = "0.2.180"
1507
+ version = "0.2.182"
1463
1508
  source = "registry+https://github.com/rust-lang/crates.io-index"
1464
- checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
1509
+ checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
1465
1510
 
1466
1511
  [[package]]
1467
1512
  name = "libloading"
@@ -1558,9 +1603,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
1558
1603
 
1559
1604
  [[package]]
1560
1605
  name = "memchr"
1561
- version = "2.7.6"
1606
+ version = "2.8.0"
1562
1607
  source = "registry+https://github.com/rust-lang/crates.io-index"
1563
- checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
1608
+ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
1564
1609
 
1565
1610
  [[package]]
1566
1611
  name = "mime"
@@ -1947,6 +1992,16 @@ dependencies = [
1947
1992
  "yansi",
1948
1993
  ]
1949
1994
 
1995
+ [[package]]
1996
+ name = "prettyplease"
1997
+ version = "0.2.37"
1998
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1999
+ checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
2000
+ dependencies = [
2001
+ "proc-macro2",
2002
+ "syn",
2003
+ ]
2004
+
1950
2005
  [[package]]
1951
2006
  name = "primeorder"
1952
2007
  version = "0.13.6"
@@ -2156,9 +2211,9 @@ dependencies = [
2156
2211
 
2157
2212
  [[package]]
2158
2213
  name = "referencing"
2159
- version = "0.40.2"
2214
+ version = "0.41.0"
2160
2215
  source = "registry+https://github.com/rust-lang/crates.io-index"
2161
- checksum = "bef39a30a317e883d1ef4c43aa849f90f480d90bb24904fd38266e61d6be58f2"
2216
+ checksum = "d5773259506800a8c6ef38df3674b061c62c5941162507891ff8b580e93d954e"
2162
2217
  dependencies = [
2163
2218
  "ahash",
2164
2219
  "fluent-uri",
@@ -2171,9 +2226,9 @@ dependencies = [
2171
2226
 
2172
2227
  [[package]]
2173
2228
  name = "regex"
2174
- version = "1.12.2"
2229
+ version = "1.12.3"
2175
2230
  source = "registry+https://github.com/rust-lang/crates.io-index"
2176
- checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
2231
+ checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
2177
2232
  dependencies = [
2178
2233
  "aho-corasick",
2179
2234
  "memchr",
@@ -2183,9 +2238,9 @@ dependencies = [
2183
2238
 
2184
2239
  [[package]]
2185
2240
  name = "regex-automata"
2186
- version = "0.4.13"
2241
+ version = "0.4.14"
2187
2242
  source = "registry+https://github.com/rust-lang/crates.io-index"
2188
- checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
2243
+ checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
2189
2244
  dependencies = [
2190
2245
  "aho-corasick",
2191
2246
  "memchr",
@@ -2194,9 +2249,9 @@ dependencies = [
2194
2249
 
2195
2250
  [[package]]
2196
2251
  name = "regex-syntax"
2197
- version = "0.8.8"
2252
+ version = "0.8.9"
2198
2253
  source = "registry+https://github.com/rust-lang/crates.io-index"
2199
- checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
2254
+ checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
2200
2255
 
2201
2256
  [[package]]
2202
2257
  name = "reserve-port"
@@ -2315,9 +2370,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
2315
2370
 
2316
2371
  [[package]]
2317
2372
  name = "ryu"
2318
- version = "1.0.22"
2373
+ version = "1.0.23"
2319
2374
  source = "registry+https://github.com/rust-lang/crates.io-index"
2320
- checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
2375
+ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
2321
2376
 
2322
2377
  [[package]]
2323
2378
  name = "same-file"
@@ -2416,13 +2471,14 @@ dependencies = [
2416
2471
 
2417
2472
  [[package]]
2418
2473
  name = "serde_qs"
2419
- version = "0.15.0"
2474
+ version = "1.0.0"
2420
2475
  source = "registry+https://github.com/rust-lang/crates.io-index"
2421
- checksum = "f3faaf9e727533a19351a43cc5a8de957372163c7d35cc48c90b75cdda13c352"
2476
+ checksum = "ac22439301a0b6f45a037681518e3169e8db1db76080e2e9600a08d1027df037"
2422
2477
  dependencies = [
2478
+ "itoa",
2423
2479
  "percent-encoding",
2480
+ "ryu",
2424
2481
  "serde",
2425
- "thiserror 2.0.18",
2426
2482
  ]
2427
2483
 
2428
2484
  [[package]]
@@ -2528,9 +2584,9 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
2528
2584
 
2529
2585
  [[package]]
2530
2586
  name = "simple_asn1"
2531
- version = "0.6.3"
2587
+ version = "0.6.4"
2532
2588
  source = "registry+https://github.com/rust-lang/crates.io-index"
2533
- checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
2589
+ checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d"
2534
2590
  dependencies = [
2535
2591
  "num-bigint",
2536
2592
  "num-traits",
@@ -2562,7 +2618,7 @@ dependencies = [
2562
2618
 
2563
2619
  [[package]]
2564
2620
  name = "spikard-bindings-shared"
2565
- version = "0.10.1"
2621
+ version = "0.10.2"
2566
2622
  dependencies = [
2567
2623
  "axum",
2568
2624
  "bytes",
@@ -2581,7 +2637,7 @@ dependencies = [
2581
2637
 
2582
2638
  [[package]]
2583
2639
  name = "spikard-core"
2584
- version = "0.10.1"
2640
+ version = "0.10.2"
2585
2641
  dependencies = [
2586
2642
  "anyhow",
2587
2643
  "base64",
@@ -2605,7 +2661,7 @@ dependencies = [
2605
2661
 
2606
2662
  [[package]]
2607
2663
  name = "spikard-http"
2608
- version = "0.10.1"
2664
+ version = "0.10.2"
2609
2665
  dependencies = [
2610
2666
  "ahash",
2611
2667
  "anyhow",
@@ -2654,7 +2710,7 @@ dependencies = [
2654
2710
 
2655
2711
  [[package]]
2656
2712
  name = "spikard-rb"
2657
- version = "0.10.1"
2713
+ version = "0.10.2"
2658
2714
  dependencies = [
2659
2715
  "async-stream",
2660
2716
  "axum",
@@ -2686,7 +2742,7 @@ dependencies = [
2686
2742
 
2687
2743
  [[package]]
2688
2744
  name = "spikard-rb-ext"
2689
- version = "0.10.1"
2745
+ version = "0.10.2"
2690
2746
  dependencies = [
2691
2747
  "magnus",
2692
2748
  "spikard-rb",
@@ -2694,7 +2750,7 @@ dependencies = [
2694
2750
 
2695
2751
  [[package]]
2696
2752
  name = "spikard-rb-macros"
2697
- version = "0.10.1"
2753
+ version = "0.10.2"
2698
2754
  dependencies = [
2699
2755
  "proc-macro2",
2700
2756
  "quote",
@@ -2740,9 +2796,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
2740
2796
 
2741
2797
  [[package]]
2742
2798
  name = "syn"
2743
- version = "2.0.114"
2799
+ version = "2.0.115"
2744
2800
  source = "registry+https://github.com/rust-lang/crates.io-index"
2745
- checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
2801
+ checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12"
2746
2802
  dependencies = [
2747
2803
  "proc-macro2",
2748
2804
  "quote",
@@ -2817,9 +2873,9 @@ dependencies = [
2817
2873
 
2818
2874
  [[package]]
2819
2875
  name = "time"
2820
- version = "0.3.46"
2876
+ version = "0.3.47"
2821
2877
  source = "registry+https://github.com/rust-lang/crates.io-index"
2822
- checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5"
2878
+ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
2823
2879
  dependencies = [
2824
2880
  "deranged",
2825
2881
  "itoa",
@@ -2838,9 +2894,9 @@ checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
2838
2894
 
2839
2895
  [[package]]
2840
2896
  name = "time-macros"
2841
- version = "0.2.26"
2897
+ version = "0.2.27"
2842
2898
  source = "registry+https://github.com/rust-lang/crates.io-index"
2843
- checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4"
2899
+ checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
2844
2900
  dependencies = [
2845
2901
  "num-conv",
2846
2902
  "time-core",
@@ -2922,9 +2978,9 @@ dependencies = [
2922
2978
 
2923
2979
  [[package]]
2924
2980
  name = "tonic"
2925
- version = "0.14.3"
2981
+ version = "0.14.4"
2926
2982
  source = "registry+https://github.com/rust-lang/crates.io-index"
2927
- checksum = "a286e33f82f8a1ee2df63f4fa35c0becf4a85a0cb03091a15fd7bf0b402dc94a"
2983
+ checksum = "7f32a6f80051a4111560201420c7885d0082ba9efe2ab61875c587bb6b18b9a0"
2928
2984
  dependencies = [
2929
2985
  "async-trait",
2930
2986
  "axum",
@@ -3161,9 +3217,15 @@ checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
3161
3217
 
3162
3218
  [[package]]
3163
3219
  name = "unicode-ident"
3164
- version = "1.0.22"
3220
+ version = "1.0.23"
3221
+ source = "registry+https://github.com/rust-lang/crates.io-index"
3222
+ checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e"
3223
+
3224
+ [[package]]
3225
+ name = "unicode-xid"
3226
+ version = "0.2.6"
3165
3227
  source = "registry+https://github.com/rust-lang/crates.io-index"
3166
- checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
3228
+ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
3167
3229
 
3168
3230
  [[package]]
3169
3231
  name = "url"
@@ -3259,11 +3321,11 @@ checksum = "e2eebbbfe4093922c2b6734d7c679ebfebd704a0d7e56dfcb0d05818ce28977d"
3259
3321
 
3260
3322
  [[package]]
3261
3323
  name = "uuid"
3262
- version = "1.20.0"
3324
+ version = "1.21.0"
3263
3325
  source = "registry+https://github.com/rust-lang/crates.io-index"
3264
- checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f"
3326
+ checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb"
3265
3327
  dependencies = [
3266
- "getrandom 0.3.4",
3328
+ "getrandom 0.4.1",
3267
3329
  "js-sys",
3268
3330
  "serde_core",
3269
3331
  "wasm-bindgen",
@@ -3343,6 +3405,15 @@ dependencies = [
3343
3405
  "wit-bindgen",
3344
3406
  ]
3345
3407
 
3408
+ [[package]]
3409
+ name = "wasip3"
3410
+ version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
3411
+ source = "registry+https://github.com/rust-lang/crates.io-index"
3412
+ checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
3413
+ dependencies = [
3414
+ "wit-bindgen",
3415
+ ]
3416
+
3346
3417
  [[package]]
3347
3418
  name = "wasm-bindgen"
3348
3419
  version = "0.2.108"
@@ -3388,6 +3459,40 @@ dependencies = [
3388
3459
  "unicode-ident",
3389
3460
  ]
3390
3461
 
3462
+ [[package]]
3463
+ name = "wasm-encoder"
3464
+ version = "0.244.0"
3465
+ source = "registry+https://github.com/rust-lang/crates.io-index"
3466
+ checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
3467
+ dependencies = [
3468
+ "leb128fmt",
3469
+ "wasmparser",
3470
+ ]
3471
+
3472
+ [[package]]
3473
+ name = "wasm-metadata"
3474
+ version = "0.244.0"
3475
+ source = "registry+https://github.com/rust-lang/crates.io-index"
3476
+ checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
3477
+ dependencies = [
3478
+ "anyhow",
3479
+ "indexmap",
3480
+ "wasm-encoder",
3481
+ "wasmparser",
3482
+ ]
3483
+
3484
+ [[package]]
3485
+ name = "wasmparser"
3486
+ version = "0.244.0"
3487
+ source = "registry+https://github.com/rust-lang/crates.io-index"
3488
+ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
3489
+ dependencies = [
3490
+ "bitflags",
3491
+ "hashbrown 0.15.5",
3492
+ "indexmap",
3493
+ "semver",
3494
+ ]
3495
+
3391
3496
  [[package]]
3392
3497
  name = "web-sys"
3393
3498
  version = "0.3.85"
@@ -3586,6 +3691,88 @@ name = "wit-bindgen"
3586
3691
  version = "0.51.0"
3587
3692
  source = "registry+https://github.com/rust-lang/crates.io-index"
3588
3693
  checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
3694
+ dependencies = [
3695
+ "wit-bindgen-rust-macro",
3696
+ ]
3697
+
3698
+ [[package]]
3699
+ name = "wit-bindgen-core"
3700
+ version = "0.51.0"
3701
+ source = "registry+https://github.com/rust-lang/crates.io-index"
3702
+ checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
3703
+ dependencies = [
3704
+ "anyhow",
3705
+ "heck",
3706
+ "wit-parser",
3707
+ ]
3708
+
3709
+ [[package]]
3710
+ name = "wit-bindgen-rust"
3711
+ version = "0.51.0"
3712
+ source = "registry+https://github.com/rust-lang/crates.io-index"
3713
+ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
3714
+ dependencies = [
3715
+ "anyhow",
3716
+ "heck",
3717
+ "indexmap",
3718
+ "prettyplease",
3719
+ "syn",
3720
+ "wasm-metadata",
3721
+ "wit-bindgen-core",
3722
+ "wit-component",
3723
+ ]
3724
+
3725
+ [[package]]
3726
+ name = "wit-bindgen-rust-macro"
3727
+ version = "0.51.0"
3728
+ source = "registry+https://github.com/rust-lang/crates.io-index"
3729
+ checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
3730
+ dependencies = [
3731
+ "anyhow",
3732
+ "prettyplease",
3733
+ "proc-macro2",
3734
+ "quote",
3735
+ "syn",
3736
+ "wit-bindgen-core",
3737
+ "wit-bindgen-rust",
3738
+ ]
3739
+
3740
+ [[package]]
3741
+ name = "wit-component"
3742
+ version = "0.244.0"
3743
+ source = "registry+https://github.com/rust-lang/crates.io-index"
3744
+ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
3745
+ dependencies = [
3746
+ "anyhow",
3747
+ "bitflags",
3748
+ "indexmap",
3749
+ "log",
3750
+ "serde",
3751
+ "serde_derive",
3752
+ "serde_json",
3753
+ "wasm-encoder",
3754
+ "wasm-metadata",
3755
+ "wasmparser",
3756
+ "wit-parser",
3757
+ ]
3758
+
3759
+ [[package]]
3760
+ name = "wit-parser"
3761
+ version = "0.244.0"
3762
+ source = "registry+https://github.com/rust-lang/crates.io-index"
3763
+ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
3764
+ dependencies = [
3765
+ "anyhow",
3766
+ "id-arena",
3767
+ "indexmap",
3768
+ "log",
3769
+ "semver",
3770
+ "serde",
3771
+ "serde_derive",
3772
+ "serde_json",
3773
+ "unicode-xid",
3774
+ "wasmparser",
3775
+ ]
3589
3776
 
3590
3777
  [[package]]
3591
3778
  name = "writeable"
@@ -3624,18 +3811,18 @@ dependencies = [
3624
3811
 
3625
3812
  [[package]]
3626
3813
  name = "zerocopy"
3627
- version = "0.8.37"
3814
+ version = "0.8.39"
3628
3815
  source = "registry+https://github.com/rust-lang/crates.io-index"
3629
- checksum = "7456cf00f0685ad319c5b1693f291a650eaf345e941d082fc4e03df8a03996ac"
3816
+ checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a"
3630
3817
  dependencies = [
3631
3818
  "zerocopy-derive",
3632
3819
  ]
3633
3820
 
3634
3821
  [[package]]
3635
3822
  name = "zerocopy-derive"
3636
- version = "0.8.37"
3823
+ version = "0.8.39"
3637
3824
  source = "registry+https://github.com/rust-lang/crates.io-index"
3638
- checksum = "1328722bbf2115db7e19d69ebcc15e795719e2d66b60827c6a69a117365e37a0"
3825
+ checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517"
3639
3826
  dependencies = [
3640
3827
  "proc-macro2",
3641
3828
  "quote",
@@ -3724,9 +3911,9 @@ checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3"
3724
3911
 
3725
3912
  [[package]]
3726
3913
  name = "zmij"
3727
- version = "1.0.18"
3914
+ version = "1.0.21"
3728
3915
  source = "registry+https://github.com/rust-lang/crates.io-index"
3729
- checksum = "1966f8ac2c1f76987d69a74d0e0f929241c10e78136434e3be70ff7f58f64214"
3916
+ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
3730
3917
 
3731
3918
  [[package]]
3732
3919
  name = "zopfli"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "spikard-rb-ext"
3
- version = "0.10.2"
3
+ version = "0.11.0"
4
4
  edition = "2024"
5
5
  license = "MIT"
6
6
  authors = ["Na'aman Hirschfeld <nhirschfeld@gmail.com>"]
@@ -7,8 +7,6 @@ default_profile = ENV.fetch('CARGO_PROFILE', 'release')
7
7
 
8
8
  create_rust_makefile('spikard_rb') do |config|
9
9
  config.profile = default_profile.to_sym
10
- # Always use --locked to maintain consistency with vendored crates.
11
- # The vendor-crates.sh script patches Cargo.toml files and relies on a
12
- # committed Cargo.lock to avoid workspace collision issues during builds.
13
- config.extra_cargo_args = ['--locked']
10
+ # NOTE: CI vendors internal crates and patches Cargo.toml files before building.
11
+ # That can legitimately require updating Cargo.lock, so we must not force --locked.
14
12
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Spikard
4
- VERSION = '0.10.2'
4
+ VERSION = '0.11.0'
5
5
  end
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "spikard-bindings-shared"
3
- version = "0.10.2"
3
+ version = "0.11.0"
4
4
  edition = "2024"
5
5
  authors = ["Na'aman Hirschfeld <nhirschfeld@gmail.com>"]
6
6
  license = "MIT"
@@ -38,12 +38,12 @@ ruby-support = ["magnus"]
38
38
  php-support = ["ext-php-rs"]
39
39
 
40
40
  [dependencies.pyo3]
41
- version = "0.27"
41
+ version = "0.28"
42
42
  optional = true
43
43
  features = ["abi3-py310"]
44
44
 
45
45
  [dependencies.pyo3-async-runtimes]
46
- version = "0.27"
46
+ version = "0.28"
47
47
  optional = true
48
48
  features = ["tokio-runtime"]
49
49
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "spikard-core"
3
- version = "0.10.2"
3
+ version = "0.11.0"
4
4
  edition = "2024"
5
5
  authors = ["Na'aman Hirschfeld <nhirschfeld@gmail.com>"]
6
6
  license = "MIT"
@@ -8,7 +8,11 @@ repository = "https://github.com/Goldziher/spikard"
8
8
  homepage = "https://github.com/Goldziher/spikard"
9
9
  description = "Shared transport-agnostic primitives for Spikard runtimes"
10
10
  keywords = ["http", "web", "framework", "validation", "middleware"]
11
- categories = ["web-programming::http-server", "web-programming", "development-tools"]
11
+ categories = [
12
+ "web-programming::http-server",
13
+ "web-programming",
14
+ "development-tools",
15
+ ]
12
16
  documentation = "https://docs.rs/spikard-core"
13
17
  readme = "README.md"
14
18
 
@@ -31,12 +35,17 @@ flate2 = { version = "=1.1.5", default-features = false, features = ["rust_backe
31
35
  brotli = "8.0"
32
36
  http = "1.4"
33
37
  base64 = "0.22"
34
- serde_qs = "0.15"
38
+ serde_qs = "1.0"
35
39
  url = "2.5"
36
40
  jiff = "0.2"
37
- uuid = "1.20"
41
+ uuid = "1.21"
38
42
  indexmap = "2.13"
39
- tokio = { version = "1", optional = true, default-features = false, features = ["rt", "macros", "sync", "time"] }
43
+ tokio = { version = "1", optional = true, default-features = false, features = [
44
+ "rt",
45
+ "macros",
46
+ "sync",
47
+ "time",
48
+ ] }
40
49
  bytes = { version = "1.11", optional = true }
41
50
  thiserror = "2.0"
42
51
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "spikard-http"
3
- version = "0.10.2"
3
+ version = "0.11.0"
4
4
  edition = "2024"
5
5
  authors = ["Na'aman Hirschfeld <nhirschfeld@gmail.com>"]
6
6
  license = "MIT"
@@ -32,7 +32,7 @@ spikard-core = { path = "../spikard-core" }
32
32
  futures-util = "0.3"
33
33
  futures = "0.3"
34
34
  jsonschema = { version = "0.37", default-features = false }
35
- serde_qs = "0.15"
35
+ serde_qs = "1.0"
36
36
  lazy_static = "1.5"
37
37
  lru = "0.16"
38
38
  regex = "1"
@@ -41,7 +41,7 @@ urlencoding = "2.1"
41
41
  url = "2.5"
42
42
  mime = "0.3"
43
43
  jiff = "0.2"
44
- uuid = "1.20"
44
+ uuid = "1.21"
45
45
  ahash = "0.8"
46
46
  bytes = "1.11"
47
47
  http-body-util = "0.1"
@@ -72,4 +72,4 @@ di = ["spikard-core/di"]
72
72
  [dev-dependencies]
73
73
  chrono = "0.4"
74
74
  doc-comment = "0.3"
75
- tempfile = "3.24"
75
+ tempfile = "3.25"
@@ -18,7 +18,7 @@ use url::form_urlencoded;
18
18
  pub fn parse_urlencoded_to_json(data: &[u8]) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
19
19
  if data.contains(&b'[') {
20
20
  let body_str = std::str::from_utf8(data)?;
21
- let config = serde_qs::Config::new(10, false);
21
+ let config = serde_qs::Config::new().max_depth(10).use_form_encoding(true);
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)?;
24
24
  convert_types_recursive(&mut json_value);
@@ -411,9 +411,12 @@ pub fn is_grpc_request(request: &Request<Body>) -> bool {
411
411
  mod tests {
412
412
  use super::*;
413
413
  use crate::grpc::handler::{GrpcHandler, GrpcHandlerResult, GrpcRequestData, GrpcResponseData, RpcMode};
414
+ use crate::grpc::streaming::{MessageStream, StreamingRequest, message_stream_from_vec};
414
415
  use bytes::Bytes;
416
+ use futures_util::StreamExt;
415
417
  use std::future::Future;
416
418
  use std::pin::Pin;
419
+ use tonic::metadata::MetadataMap;
417
420
 
418
421
  struct EchoHandler;
419
422
 
@@ -625,4 +628,228 @@ mod tests {
625
628
  assert_eq!(status, StatusCode::PAYLOAD_TOO_LARGE);
626
629
  assert!(message.contains("Message exceeds maximum size"));
627
630
  }
631
+
632
+ #[tokio::test]
633
+ async fn test_route_grpc_request_server_streaming_success() {
634
+ struct StreamHandler;
635
+
636
+ impl GrpcHandler for StreamHandler {
637
+ fn call(&self, _request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
638
+ Box::pin(async { Err(tonic::Status::unimplemented("Use server streaming")) })
639
+ }
640
+
641
+ fn service_name(&self) -> &str {
642
+ "test.StreamService"
643
+ }
644
+
645
+ fn call_server_stream(
646
+ &self,
647
+ _request: GrpcRequestData,
648
+ ) -> Pin<Box<dyn Future<Output = Result<MessageStream, tonic::Status>> + Send>> {
649
+ Box::pin(async {
650
+ let messages = vec![Bytes::from("m1"), Bytes::from("m2")];
651
+ Ok(message_stream_from_vec(messages))
652
+ })
653
+ }
654
+ }
655
+
656
+ let mut registry = GrpcRegistry::new();
657
+ registry.register("test.StreamService", Arc::new(StreamHandler), RpcMode::ServerStreaming);
658
+ let registry = Arc::new(registry);
659
+ let config = GrpcConfig::default();
660
+
661
+ let request = Request::builder()
662
+ .uri("/test.StreamService/Stream")
663
+ .header("content-type", "application/grpc")
664
+ .body(Body::from(Bytes::from("ignored")))
665
+ .unwrap();
666
+
667
+ let response = route_grpc_request(registry, &config, request).await.unwrap();
668
+ assert_eq!(response.status(), StatusCode::OK);
669
+ assert_eq!(
670
+ response.headers().get("content-type").unwrap(),
671
+ "application/grpc+proto"
672
+ );
673
+ assert_eq!(response.headers().get("grpc-status").unwrap(), "0");
674
+
675
+ // The body is a stream. For test purposes, just ensure we can read it.
676
+ let body = axum::body::to_bytes(response.into_body(), 1024).await.unwrap();
677
+ assert!(!body.is_empty());
678
+ }
679
+
680
+ #[tokio::test]
681
+ async fn test_route_grpc_request_client_streaming_success_and_response_metadata() {
682
+ struct ClientStreamHandler;
683
+
684
+ impl GrpcHandler for ClientStreamHandler {
685
+ fn call(&self, _request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
686
+ Box::pin(async { Err(tonic::Status::unimplemented("Use client streaming")) })
687
+ }
688
+
689
+ fn service_name(&self) -> &str {
690
+ "test.ClientStreamService"
691
+ }
692
+
693
+ fn call_client_stream(
694
+ &self,
695
+ mut request: StreamingRequest,
696
+ ) -> Pin<Box<dyn Future<Output = Result<GrpcResponseData, tonic::Status>> + Send>> {
697
+ Box::pin(async move {
698
+ // Make sure routing copied request headers into metadata.
699
+ assert!(request.metadata.get("x-request-id").is_some());
700
+
701
+ let mut first = None;
702
+ while let Some(item) = request.message_stream.next().await {
703
+ first = Some(item?);
704
+ break;
705
+ }
706
+
707
+ let payload = first.unwrap_or_else(Bytes::new);
708
+ let mut metadata = MetadataMap::new();
709
+ metadata.insert("x-response-id", "resp-123".parse().unwrap());
710
+
711
+ Ok(GrpcResponseData { payload, metadata })
712
+ })
713
+ }
714
+ }
715
+
716
+ let mut registry = GrpcRegistry::new();
717
+ registry.register(
718
+ "test.ClientStreamService",
719
+ Arc::new(ClientStreamHandler),
720
+ RpcMode::ClientStreaming,
721
+ );
722
+ let registry = Arc::new(registry);
723
+ let config = GrpcConfig::default();
724
+
725
+ // Single gRPC frame: compression=0, length=5, message="hello"
726
+ let frame = vec![
727
+ 0x00, // compression: no
728
+ 0x00, 0x00, 0x00, 0x05, // length: 5 bytes
729
+ b'h', b'e', b'l', b'l', b'o',
730
+ ];
731
+
732
+ let request = Request::builder()
733
+ .uri("/test.ClientStreamService/ClientStream")
734
+ .header("content-type", "application/grpc")
735
+ .header("x-request-id", "req-123")
736
+ .body(Body::from(frame))
737
+ .unwrap();
738
+
739
+ let response = route_grpc_request(registry, &config, request).await.unwrap();
740
+ assert_eq!(response.status(), StatusCode::OK);
741
+ assert_eq!(response.headers().get("grpc-status").unwrap(), "0");
742
+ assert_eq!(response.headers().get("x-response-id").unwrap(), "resp-123");
743
+
744
+ let body = axum::body::to_bytes(response.into_body(), 1024).await.unwrap();
745
+ assert_eq!(body, Bytes::from_static(b"hello"));
746
+ }
747
+
748
+ #[tokio::test]
749
+ async fn test_route_grpc_request_bidi_streaming_success() {
750
+ struct BidiHandler;
751
+
752
+ impl GrpcHandler for BidiHandler {
753
+ fn call(&self, _request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
754
+ Box::pin(async { Err(tonic::Status::unimplemented("Use bidi streaming")) })
755
+ }
756
+
757
+ fn service_name(&self) -> &str {
758
+ "test.BidiService"
759
+ }
760
+
761
+ fn call_bidi_stream(
762
+ &self,
763
+ _request: StreamingRequest,
764
+ ) -> Pin<Box<dyn Future<Output = Result<MessageStream, tonic::Status>> + Send>> {
765
+ Box::pin(async {
766
+ let messages = vec![Bytes::from("r1")];
767
+ Ok(message_stream_from_vec(messages))
768
+ })
769
+ }
770
+ }
771
+
772
+ let mut registry = GrpcRegistry::new();
773
+ registry.register(
774
+ "test.BidiService",
775
+ Arc::new(BidiHandler),
776
+ RpcMode::BidirectionalStreaming,
777
+ );
778
+ let registry = Arc::new(registry);
779
+ let config = GrpcConfig::default();
780
+
781
+ // Provide a well-formed request frame so the frame parser succeeds.
782
+ let frame = vec![
783
+ 0x00, // compression: no
784
+ 0x00, 0x00, 0x00, 0x01, // length: 1 byte
785
+ b'x',
786
+ ];
787
+
788
+ let request = Request::builder()
789
+ .uri("/test.BidiService/Chat")
790
+ .header("content-type", "application/grpc")
791
+ .body(Body::from(frame))
792
+ .unwrap();
793
+
794
+ let response = route_grpc_request(registry, &config, request).await.unwrap();
795
+ assert_eq!(response.status(), StatusCode::OK);
796
+ assert_eq!(response.headers().get("grpc-status").unwrap(), "0");
797
+
798
+ let body = axum::body::to_bytes(response.into_body(), 1024).await.unwrap();
799
+ assert!(!body.is_empty());
800
+ }
801
+
802
+ #[tokio::test]
803
+ async fn test_route_grpc_request_client_streaming_invalid_compression_flag_maps_to_501() {
804
+ // Use a handler that would succeed, but the request is invalid before handler gets called.
805
+ struct NeverCalledHandler;
806
+
807
+ impl GrpcHandler for NeverCalledHandler {
808
+ fn call(&self, _request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
809
+ Box::pin(async { Err(tonic::Status::unimplemented("not used")) })
810
+ }
811
+
812
+ fn service_name(&self) -> &str {
813
+ "test.BadClientStreamService"
814
+ }
815
+
816
+ fn call_client_stream(
817
+ &self,
818
+ _request: StreamingRequest,
819
+ ) -> Pin<Box<dyn Future<Output = Result<GrpcResponseData, tonic::Status>> + Send>> {
820
+ Box::pin(async {
821
+ Ok(GrpcResponseData {
822
+ payload: Bytes::from_static(b"ok"),
823
+ metadata: MetadataMap::new(),
824
+ })
825
+ })
826
+ }
827
+ }
828
+
829
+ let mut registry = GrpcRegistry::new();
830
+ registry.register(
831
+ "test.BadClientStreamService",
832
+ Arc::new(NeverCalledHandler),
833
+ RpcMode::ClientStreaming,
834
+ );
835
+ let registry = Arc::new(registry);
836
+ let config = GrpcConfig::default();
837
+
838
+ // Compression flag = 1 => UNIMPLEMENTED per parser.
839
+ let frame = vec![
840
+ 0x01, // compression: yes (unsupported)
841
+ 0x00, 0x00, 0x00, 0x01, // length: 1 byte
842
+ b'x',
843
+ ];
844
+
845
+ let request = Request::builder()
846
+ .uri("/test.BadClientStreamService/ClientStream")
847
+ .header("content-type", "application/grpc")
848
+ .body(Body::from(frame))
849
+ .unwrap();
850
+
851
+ let err = route_grpc_request(registry, &config, request).await.unwrap_err();
852
+ assert_eq!(err.0, StatusCode::NOT_IMPLEMENTED);
853
+ assert!(err.1.contains("compression") || err.1.contains("supported"));
854
+ }
628
855
  }
@@ -507,11 +507,7 @@ fn build_router_with_handlers_inner(
507
507
  for (route, handler) in route_handlers {
508
508
  #[cfg(feature = "di")]
509
509
  let handler = if let Some(ref container) = di_container {
510
- let mut required_deps = extract_handler_dependencies(&route);
511
- if required_deps.is_empty() {
512
- required_deps = container.keys();
513
- }
514
-
510
+ let required_deps = extract_handler_dependencies(&route);
515
511
  if !required_deps.is_empty() {
516
512
  Arc::new(crate::di_handler::DependencyInjectingHandler::new(
517
513
  handler,
@@ -720,7 +716,7 @@ pub fn build_router_with_handlers_and_config(
720
716
  config: ServerConfig,
721
717
  route_metadata: Vec<crate::RouteMetadata>,
722
718
  ) -> Result<AxumRouter, String> {
723
- #[cfg(feature = "di")]
719
+ #[cfg(all(feature = "di", debug_assertions))]
724
720
  if let Some(di_container) = config.di_container.as_ref() {
725
721
  eprintln!(
726
722
  "[spikard-di] build_router: di_container has keys: {:?}",
@@ -1093,14 +1089,19 @@ impl Server {
1093
1089
  }
1094
1090
 
1095
1091
  /// Initialize logging
1092
+ ///
1093
+ /// This function is idempotent - calling it multiple times is safe.
1094
+ /// It uses `try_init()` instead of `init()` to avoid panics when the
1095
+ /// global subscriber has already been set (e.g., by a language runtime
1096
+ /// or a previous call).
1096
1097
  pub fn init_logging() {
1097
- tracing_subscriber::registry()
1098
+ let _ = tracing_subscriber::registry()
1098
1099
  .with(
1099
1100
  tracing_subscriber::EnvFilter::try_from_default_env()
1100
1101
  .unwrap_or_else(|_| "spikard=info,tower_http=info".into()),
1101
1102
  )
1102
1103
  .with(tracing_subscriber::fmt::layer())
1103
- .init();
1104
+ .try_init();
1104
1105
  }
1105
1106
 
1106
1107
  /// Start the server
@@ -170,6 +170,46 @@ pub async fn snapshot_http_response(
170
170
  })
171
171
  }
172
172
 
173
+ /// Convert an Axum response into a reusable [`ResponseSnapshot`], allowing body stream errors.
174
+ ///
175
+ /// This is useful for streaming responses where a producer might abort mid-stream (for example,
176
+ /// a JavaScript async generator throwing). In those cases, collecting the whole body can fail
177
+ /// with a "Stream error". This helper returns the bytes read up to the first stream error.
178
+ pub async fn snapshot_http_response_allow_body_errors(
179
+ response: axum::response::Response<Body>,
180
+ ) -> Result<ResponseSnapshot, SnapshotError> {
181
+ let (parts, mut body) = response.into_parts();
182
+ let status = parts.status.as_u16();
183
+
184
+ let mut headers = HashMap::new();
185
+ for (name, value) in parts.headers.iter() {
186
+ let header_value = value
187
+ .to_str()
188
+ .map_err(|e| SnapshotError::InvalidHeader(e.to_string()))?;
189
+ headers.insert(name.to_string().to_ascii_lowercase(), header_value.to_string());
190
+ }
191
+
192
+ let mut bytes = Vec::<u8>::new();
193
+ while let Some(frame_result) = body.frame().await {
194
+ match frame_result {
195
+ Ok(frame) => {
196
+ if let Ok(data) = frame.into_data() {
197
+ bytes.extend_from_slice(&data);
198
+ }
199
+ }
200
+ Err(_) => break,
201
+ }
202
+ }
203
+
204
+ let decoded_body = decode_body(&headers, bytes)?;
205
+
206
+ Ok(ResponseSnapshot {
207
+ status,
208
+ headers,
209
+ body: decoded_body,
210
+ })
211
+ }
212
+
173
213
  fn decode_body(headers: &HashMap<String, String>, body: Vec<u8>) -> Result<Vec<u8>, SnapshotError> {
174
214
  let encoding = headers
175
215
  .get("content-encoding")
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "spikard-rb"
3
- version = "0.10.2"
3
+ version = "0.11.0"
4
4
  edition = "2024"
5
5
  authors = ["Na'aman Hirschfeld <nhirschfeld@gmail.com>"]
6
6
  license = "MIT"
@@ -40,7 +40,7 @@ tokio = { version = "1", features = ["full"] }
40
40
  tonic = { version = "0.14", features = ["transport", "codegen", "gzip"] }
41
41
  tungstenite = "0.28"
42
42
  tracing = "0.1"
43
- serde_qs = "0.15"
43
+ serde_qs = "1.0"
44
44
  urlencoding = "2.1"
45
45
  once_cell = "1.21"
46
46
  async-stream = "0.3"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "spikard-rb-macros"
3
- version = "0.10.2"
3
+ version = "0.11.0"
4
4
  edition = "2024"
5
5
  license = "MIT"
6
6
  publish = false
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spikard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.2
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Na'aman Hirschfeld
@@ -9,6 +9,20 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: base64
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: rb_sys
14
28
  requirement: !ruby/object:Gem::Requirement