@lovrozagar/honey 0.6.8 → 0.7.1

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 (674) hide show
  1. package/README.md +514 -192
  2. package/bin/honey.js +25 -0
  3. package/dist/accepts.d.ts +7 -1
  4. package/dist/accepts.d.ts.map +1 -1
  5. package/dist/accepts.js +126 -0
  6. package/dist/accepts.js.map +1 -0
  7. package/dist/body-kind.d.ts +26 -0
  8. package/dist/body-kind.d.ts.map +1 -0
  9. package/dist/body-kind.js +70 -0
  10. package/dist/body-kind.js.map +1 -0
  11. package/dist/body-limit.d.ts +5 -1
  12. package/dist/body-limit.d.ts.map +1 -1
  13. package/dist/body-limit.js +126 -0
  14. package/dist/body-limit.js.map +1 -0
  15. package/dist/build/index.d.ts +19 -3
  16. package/dist/build/index.d.ts.map +1 -1
  17. package/dist/build/index.js +173 -0
  18. package/dist/build/index.js.map +1 -0
  19. package/dist/cli-args.d.ts +33 -0
  20. package/dist/cli-args.d.ts.map +1 -0
  21. package/dist/cli-args.js +102 -0
  22. package/dist/cli-args.js.map +1 -0
  23. package/dist/cli-go/auth.go +26 -0
  24. package/dist/cli-go/config.go +192 -0
  25. package/dist/cli-go/errors.go +75 -0
  26. package/dist/cli-go/multipart.go +79 -0
  27. package/dist/cli-go/output.go +163 -0
  28. package/dist/cli-go/root.go +30 -0
  29. package/dist/cli-go/stream.go +88 -0
  30. package/dist/cli-go/version.go +5 -0
  31. package/dist/cli.d.ts +1 -1
  32. package/dist/cli.js +209 -0
  33. package/dist/cli.js.map +1 -0
  34. package/dist/client/defaults.js +48 -0
  35. package/dist/client/defaults.js.map +1 -0
  36. package/dist/client/error.d.ts +35 -0
  37. package/dist/client/error.d.ts.map +1 -1
  38. package/dist/client/error.js +71 -0
  39. package/dist/client/error.js.map +1 -0
  40. package/dist/client/http.d.ts +67 -8
  41. package/dist/client/http.d.ts.map +1 -1
  42. package/dist/client/http.js +571 -0
  43. package/dist/client/http.js.map +1 -0
  44. package/dist/client/index.d.ts +3 -2
  45. package/dist/client/index.d.ts.map +1 -1
  46. package/dist/client/index.js +95 -0
  47. package/dist/client/index.js.map +1 -0
  48. package/dist/client/path.d.ts +26 -0
  49. package/dist/client/path.d.ts.map +1 -0
  50. package/dist/client/path.js +163 -0
  51. package/dist/client/path.js.map +1 -0
  52. package/dist/client/redirect-policy.d.ts +12 -0
  53. package/dist/client/redirect-policy.d.ts.map +1 -0
  54. package/dist/client/redirect-policy.js +20 -0
  55. package/dist/client/redirect-policy.js.map +1 -0
  56. package/dist/client/sdk.d.ts +36 -0
  57. package/dist/client/sdk.d.ts.map +1 -1
  58. package/dist/client/sdk.js +292 -0
  59. package/dist/client/sdk.js.map +1 -0
  60. package/dist/client/sse.d.ts +13 -0
  61. package/dist/client/sse.d.ts.map +1 -1
  62. package/dist/client/sse.js +129 -0
  63. package/dist/client/sse.js.map +1 -0
  64. package/dist/client/types.js +2 -0
  65. package/dist/client/types.js.map +1 -0
  66. package/dist/client/ws.d.ts +9 -2
  67. package/dist/client/ws.d.ts.map +1 -1
  68. package/dist/client/ws.js +112 -0
  69. package/dist/client/ws.js.map +1 -0
  70. package/dist/client-go/errors.go +236 -0
  71. package/dist/client-go/export.go +44 -0
  72. package/dist/client-go/invalidation.go +371 -0
  73. package/dist/client-go/nullable.go +62 -0
  74. package/dist/client-go/realtime.go +354 -0
  75. package/dist/client-go/result.go +20 -0
  76. package/dist/client-go/runtime.go +975 -0
  77. package/dist/client-go/sse.go +159 -0
  78. package/dist/client-go/transport.go +495 -0
  79. package/dist/client-go/ws.go +128 -0
  80. package/dist/client-info.d.ts +36 -0
  81. package/dist/client-info.d.ts.map +1 -0
  82. package/dist/client-info.js +53 -0
  83. package/dist/client-info.js.map +1 -0
  84. package/dist/client-mcp/README.md +10 -0
  85. package/dist/client-mcp/server.ts +221 -0
  86. package/dist/client-mcp/types.ts +19 -0
  87. package/dist/client-python/_errors.py +115 -0
  88. package/dist/client-python/_invalidation.py +494 -0
  89. package/dist/client-python/_realtime.py +179 -0
  90. package/dist/client-python/_runtime.py +860 -0
  91. package/dist/client-python/_sse.py +155 -0
  92. package/dist/client-python/_transport.py +366 -0
  93. package/dist/client-python/_ws.py +159 -0
  94. package/dist/client-python/py.typed +0 -0
  95. package/dist/client-rust/errors.rs +220 -0
  96. package/dist/client-rust/invalidation.rs +412 -0
  97. package/dist/client-rust/invalidation_sync.rs +93 -0
  98. package/dist/client-rust/nullable.rs +17 -0
  99. package/dist/client-rust/realtime.rs +702 -0
  100. package/dist/client-rust/result.rs +22 -0
  101. package/dist/client-rust/runtime.rs +980 -0
  102. package/dist/client-rust/runtime_sync.rs +484 -0
  103. package/dist/client-rust/sse.rs +188 -0
  104. package/dist/client-rust/ws.rs +167 -0
  105. package/dist/codegen-go-cli.d.ts.map +1 -1
  106. package/dist/codegen-go-cli.js +964 -0
  107. package/dist/codegen-go-cli.js.map +1 -0
  108. package/dist/codegen-go.d.ts +15 -3
  109. package/dist/codegen-go.d.ts.map +1 -1
  110. package/dist/codegen-go.js +872 -0
  111. package/dist/codegen-go.js.map +1 -0
  112. package/dist/codegen-hash.js +9 -0
  113. package/dist/codegen-hash.js.map +1 -0
  114. package/dist/codegen-ir.d.ts +69 -2
  115. package/dist/codegen-ir.d.ts.map +1 -1
  116. package/dist/codegen-ir.js +661 -0
  117. package/dist/codegen-ir.js.map +1 -0
  118. package/dist/codegen-lang.d.ts +65 -0
  119. package/dist/codegen-lang.d.ts.map +1 -0
  120. package/dist/codegen-lang.js +526 -0
  121. package/dist/codegen-lang.js.map +1 -0
  122. package/dist/codegen-load.d.ts +31 -0
  123. package/dist/codegen-load.d.ts.map +1 -0
  124. package/dist/codegen-load.js +138 -0
  125. package/dist/codegen-load.js.map +1 -0
  126. package/dist/codegen-loaders.js +55 -0
  127. package/dist/codegen-loaders.js.map +1 -0
  128. package/dist/codegen-mcp.d.ts.map +1 -1
  129. package/dist/codegen-mcp.js +368 -0
  130. package/dist/codegen-mcp.js.map +1 -0
  131. package/dist/codegen-python.d.ts +13 -1
  132. package/dist/codegen-python.d.ts.map +1 -1
  133. package/dist/codegen-python.js +828 -0
  134. package/dist/codegen-python.js.map +1 -0
  135. package/dist/codegen-route-tree-intern.d.ts +15 -5
  136. package/dist/codegen-route-tree-intern.d.ts.map +1 -1
  137. package/dist/codegen-route-tree-intern.js +221 -0
  138. package/dist/codegen-route-tree-intern.js.map +1 -0
  139. package/dist/codegen-rust.d.ts.map +1 -1
  140. package/dist/codegen-rust.js +1293 -0
  141. package/dist/codegen-rust.js.map +1 -0
  142. package/dist/codegen-sanitize.d.ts.map +1 -1
  143. package/dist/codegen-sanitize.js +56 -0
  144. package/dist/codegen-sanitize.js.map +1 -0
  145. package/dist/codegen-schema-naming.d.ts +5 -0
  146. package/dist/codegen-schema-naming.d.ts.map +1 -1
  147. package/dist/codegen-schema-naming.js +150 -0
  148. package/dist/codegen-schema-naming.js.map +1 -0
  149. package/dist/codegen-sdk-model.d.ts +71 -0
  150. package/dist/codegen-sdk-model.d.ts.map +1 -0
  151. package/dist/codegen-sdk-model.js +179 -0
  152. package/dist/codegen-sdk-model.js.map +1 -0
  153. package/dist/codegen.d.ts +41 -23
  154. package/dist/codegen.d.ts.map +1 -1
  155. package/dist/codegen.js +4610 -0
  156. package/dist/codegen.js.map +1 -0
  157. package/dist/context.d.ts +36 -6
  158. package/dist/context.d.ts.map +1 -1
  159. package/dist/context.js +242 -0
  160. package/dist/context.js.map +1 -0
  161. package/dist/cookie-sign.d.ts +16 -2
  162. package/dist/cookie-sign.d.ts.map +1 -1
  163. package/dist/cookie-sign.js +104 -0
  164. package/dist/cookie-sign.js.map +1 -0
  165. package/dist/cookie.d.ts +12 -0
  166. package/dist/cookie.d.ts.map +1 -1
  167. package/dist/cookie.js +138 -0
  168. package/dist/cookie.js.map +1 -0
  169. package/dist/cors.d.ts +1 -0
  170. package/dist/cors.d.ts.map +1 -1
  171. package/dist/cors.js +87 -0
  172. package/dist/cors.js.map +1 -0
  173. package/dist/crypto.d.ts +6 -1
  174. package/dist/crypto.d.ts.map +1 -1
  175. package/dist/crypto.js +38 -0
  176. package/dist/crypto.js.map +1 -0
  177. package/dist/csrf.d.ts +19 -0
  178. package/dist/csrf.d.ts.map +1 -1
  179. package/dist/csrf.js +79 -0
  180. package/dist/csrf.js.map +1 -0
  181. package/dist/curl-logger.d.ts +14 -3
  182. package/dist/curl-logger.d.ts.map +1 -1
  183. package/dist/curl-logger.js +199 -0
  184. package/dist/curl-logger.js.map +1 -0
  185. package/dist/detect-runtime.js +13 -0
  186. package/dist/detect-runtime.js.map +1 -0
  187. package/dist/dict.d.ts +11 -0
  188. package/dist/dict.d.ts.map +1 -0
  189. package/dist/dict.js +13 -0
  190. package/dist/dict.js.map +1 -0
  191. package/dist/error.d.ts.map +1 -1
  192. package/dist/error.js +44 -0
  193. package/dist/error.js.map +1 -0
  194. package/dist/errors.js +39 -0
  195. package/dist/errors.js.map +1 -0
  196. package/dist/etag.d.ts +2 -0
  197. package/dist/etag.d.ts.map +1 -1
  198. package/dist/etag.js +82 -0
  199. package/dist/etag.js.map +1 -0
  200. package/dist/feature-detect.d.ts +25 -0
  201. package/dist/feature-detect.d.ts.map +1 -0
  202. package/dist/feature-detect.js +144 -0
  203. package/dist/feature-detect.js.map +1 -0
  204. package/dist/feature-load.js +36 -0
  205. package/dist/feature-load.js.map +1 -0
  206. package/dist/feature-slots.d.ts +44 -0
  207. package/dist/feature-slots.d.ts.map +1 -0
  208. package/dist/feature-slots.js +48 -0
  209. package/dist/feature-slots.js.map +1 -0
  210. package/dist/fetch-request.d.ts +10 -0
  211. package/dist/fetch-request.d.ts.map +1 -0
  212. package/dist/fetch-request.js +13 -0
  213. package/dist/fetch-request.js.map +1 -0
  214. package/dist/gen-process.d.ts +15 -0
  215. package/dist/gen-process.d.ts.map +1 -0
  216. package/dist/gen-process.js +43 -0
  217. package/dist/gen-process.js.map +1 -0
  218. package/dist/gen-write.d.ts +19 -0
  219. package/dist/gen-write.d.ts.map +1 -0
  220. package/dist/gen-write.js +120 -0
  221. package/dist/gen-write.js.map +1 -0
  222. package/dist/glob.d.ts +21 -0
  223. package/dist/glob.d.ts.map +1 -0
  224. package/dist/glob.js +115 -0
  225. package/dist/glob.js.map +1 -0
  226. package/dist/go-type-emitter.d.ts +18 -4
  227. package/dist/go-type-emitter.d.ts.map +1 -1
  228. package/dist/go-type-emitter.js +594 -0
  229. package/dist/go-type-emitter.js.map +1 -0
  230. package/dist/honey-response.d.ts.map +1 -1
  231. package/dist/honey-response.js +338 -0
  232. package/dist/honey-response.js.map +1 -0
  233. package/dist/i18n-slot.d.ts +8 -0
  234. package/dist/i18n-slot.d.ts.map +1 -1
  235. package/dist/i18n-slot.js +14 -0
  236. package/dist/i18n-slot.js.map +1 -0
  237. package/dist/i18n.d.ts +3 -0
  238. package/dist/i18n.d.ts.map +1 -1
  239. package/dist/i18n.js +250 -0
  240. package/dist/i18n.js.map +1 -0
  241. package/dist/index.d.ts +408 -148
  242. package/dist/index.d.ts.map +1 -1
  243. package/dist/index.js +2324 -0
  244. package/dist/index.js.map +1 -0
  245. package/dist/init.d.ts +3 -5
  246. package/dist/init.d.ts.map +1 -1
  247. package/dist/init.js +140 -0
  248. package/dist/init.js.map +1 -0
  249. package/dist/input.js +4 -0
  250. package/dist/input.js.map +1 -0
  251. package/dist/invalidate-check.d.ts +11 -0
  252. package/dist/invalidate-check.d.ts.map +1 -1
  253. package/dist/invalidate-check.js +163 -0
  254. package/dist/invalidate-check.js.map +1 -0
  255. package/dist/invoke-user.d.ts +35 -0
  256. package/dist/invoke-user.d.ts.map +1 -0
  257. package/dist/invoke-user.js +73 -0
  258. package/dist/invoke-user.js.map +1 -0
  259. package/dist/ip-restrict.d.ts +13 -1
  260. package/dist/ip-restrict.d.ts.map +1 -1
  261. package/dist/ip-restrict.js +60 -0
  262. package/dist/ip-restrict.js.map +1 -0
  263. package/dist/ip.d.ts +39 -0
  264. package/dist/ip.d.ts.map +1 -0
  265. package/dist/ip.js +209 -0
  266. package/dist/ip.js.map +1 -0
  267. package/dist/logger.d.ts.map +1 -1
  268. package/dist/logger.js +113 -0
  269. package/dist/logger.js.map +1 -0
  270. package/dist/media-type.d.ts +30 -0
  271. package/dist/media-type.d.ts.map +1 -0
  272. package/dist/media-type.js +97 -0
  273. package/dist/media-type.js.map +1 -0
  274. package/dist/meta-spec-merge.d.ts +32 -0
  275. package/dist/meta-spec-merge.d.ts.map +1 -0
  276. package/dist/meta-spec-merge.js +68 -0
  277. package/dist/meta-spec-merge.js.map +1 -0
  278. package/dist/meta-spec.d.ts +16 -3
  279. package/dist/meta-spec.d.ts.map +1 -1
  280. package/dist/meta-spec.js +535 -0
  281. package/dist/meta-spec.js.map +1 -0
  282. package/dist/middleware.d.ts +23 -6
  283. package/dist/middleware.d.ts.map +1 -1
  284. package/dist/middleware.js +253 -0
  285. package/dist/middleware.js.map +1 -0
  286. package/dist/node-request.d.ts +57 -12
  287. package/dist/node-request.d.ts.map +1 -1
  288. package/dist/node-request.js +407 -0
  289. package/dist/node-request.js.map +1 -0
  290. package/dist/node.d.ts +20 -3
  291. package/dist/node.d.ts.map +1 -1
  292. package/dist/node.js +396 -0
  293. package/dist/node.js.map +1 -0
  294. package/dist/openapi/collect.d.ts +6 -3
  295. package/dist/openapi/collect.d.ts.map +1 -1
  296. package/dist/openapi/collect.js +30 -0
  297. package/dist/openapi/collect.js.map +1 -0
  298. package/dist/openapi/docs-page.d.ts +42 -0
  299. package/dist/openapi/docs-page.d.ts.map +1 -0
  300. package/dist/openapi/docs-page.js +80 -0
  301. package/dist/openapi/docs-page.js.map +1 -0
  302. package/dist/openapi/document.d.ts +4 -0
  303. package/dist/openapi/document.d.ts.map +1 -1
  304. package/dist/openapi/document.js +785 -0
  305. package/dist/openapi/document.js.map +1 -0
  306. package/dist/openapi/json-schema-slot.d.ts +7 -0
  307. package/dist/openapi/json-schema-slot.d.ts.map +1 -1
  308. package/dist/openapi/json-schema-slot.js +38 -0
  309. package/dist/openapi/json-schema-slot.js.map +1 -0
  310. package/dist/openapi/register.d.ts.map +1 -1
  311. package/dist/openapi/register.js +20 -0
  312. package/dist/openapi/register.js.map +1 -0
  313. package/dist/openapi/scalar.d.ts +3 -1
  314. package/dist/openapi/scalar.d.ts.map +1 -1
  315. package/dist/openapi/scalar.js +21 -0
  316. package/dist/openapi/scalar.js.map +1 -0
  317. package/dist/openapi/serve.d.ts +9 -0
  318. package/dist/openapi/serve.d.ts.map +1 -0
  319. package/dist/openapi/serve.js +36 -0
  320. package/dist/openapi/serve.js.map +1 -0
  321. package/dist/openapi/spec-factory.d.ts +30 -1
  322. package/dist/openapi/spec-factory.d.ts.map +1 -1
  323. package/dist/openapi/spec-factory.js +52 -0
  324. package/dist/openapi/spec-factory.js.map +1 -0
  325. package/dist/openapi/spec.d.ts +14 -5
  326. package/dist/openapi/spec.d.ts.map +1 -1
  327. package/dist/openapi/spec.js +50 -0
  328. package/dist/openapi/spec.js.map +1 -0
  329. package/dist/openapi/swagger.d.ts +3 -1
  330. package/dist/openapi/swagger.d.ts.map +1 -1
  331. package/dist/openapi/swagger.js +22 -0
  332. package/dist/openapi/swagger.js.map +1 -0
  333. package/dist/pattern.d.ts +71 -0
  334. package/dist/pattern.d.ts.map +1 -0
  335. package/dist/pattern.js +239 -0
  336. package/dist/pattern.js.map +1 -0
  337. package/dist/peer.d.ts +17 -0
  338. package/dist/peer.d.ts.map +1 -0
  339. package/dist/peer.js +50 -0
  340. package/dist/peer.js.map +1 -0
  341. package/dist/plugin.d.ts +67 -16
  342. package/dist/plugin.d.ts.map +1 -1
  343. package/dist/plugin.js +649 -0
  344. package/dist/plugin.js.map +1 -0
  345. package/dist/powered-by.d.ts.map +1 -1
  346. package/dist/powered-by.js +11 -0
  347. package/dist/powered-by.js.map +1 -0
  348. package/dist/pretty-json.d.ts.map +1 -1
  349. package/dist/pretty-json.js +47 -0
  350. package/dist/pretty-json.js.map +1 -0
  351. package/dist/producer-stream.d.ts +47 -0
  352. package/dist/producer-stream.d.ts.map +1 -0
  353. package/dist/producer-stream.js +267 -0
  354. package/dist/producer-stream.js.map +1 -0
  355. package/dist/proxy.d.ts +47 -8
  356. package/dist/proxy.d.ts.map +1 -1
  357. package/dist/proxy.js +274 -0
  358. package/dist/proxy.js.map +1 -0
  359. package/dist/python-type-emitter.d.ts +14 -1
  360. package/dist/python-type-emitter.d.ts.map +1 -1
  361. package/dist/python-type-emitter.js +157 -0
  362. package/dist/python-type-emitter.js.map +1 -0
  363. package/dist/realtime/bus.d.ts +17 -1
  364. package/dist/realtime/bus.d.ts.map +1 -1
  365. package/dist/realtime/bus.js +112 -0
  366. package/dist/realtime/bus.js.map +1 -0
  367. package/dist/realtime/register.d.ts +3 -0
  368. package/dist/realtime/register.d.ts.map +1 -0
  369. package/dist/realtime/register.js +10 -0
  370. package/dist/realtime/register.js.map +1 -0
  371. package/dist/realtime/route.d.ts +65 -26
  372. package/dist/realtime/route.d.ts.map +1 -1
  373. package/dist/realtime/route.js +72 -0
  374. package/dist/realtime/route.js.map +1 -0
  375. package/dist/realtime/server.d.ts +43 -0
  376. package/dist/realtime/server.d.ts.map +1 -0
  377. package/dist/realtime/server.js +255 -0
  378. package/dist/realtime/server.js.map +1 -0
  379. package/dist/request-id.d.ts +7 -0
  380. package/dist/request-id.d.ts.map +1 -1
  381. package/dist/request-id.js +16 -0
  382. package/dist/request-id.js.map +1 -0
  383. package/dist/request-limits.d.ts +3 -0
  384. package/dist/request-limits.d.ts.map +1 -0
  385. package/dist/request-limits.js +3 -0
  386. package/dist/request-limits.js.map +1 -0
  387. package/dist/request-path.d.ts +33 -0
  388. package/dist/request-path.d.ts.map +1 -0
  389. package/dist/request-path.js +178 -0
  390. package/dist/request-path.js.map +1 -0
  391. package/dist/request-to-curl.d.ts +7 -1
  392. package/dist/request-to-curl.d.ts.map +1 -1
  393. package/dist/request-to-curl.js +48 -0
  394. package/dist/request-to-curl.js.map +1 -0
  395. package/dist/response.d.ts +25 -2
  396. package/dist/response.d.ts.map +1 -1
  397. package/dist/response.js +313 -0
  398. package/dist/response.js.map +1 -0
  399. package/dist/rust-type-emitter.d.ts +14 -2
  400. package/dist/rust-type-emitter.d.ts.map +1 -1
  401. package/dist/rust-type-emitter.js +463 -0
  402. package/dist/rust-type-emitter.js.map +1 -0
  403. package/dist/secure-headers.d.ts.map +1 -1
  404. package/dist/secure-headers.js +53 -0
  405. package/dist/secure-headers.js.map +1 -0
  406. package/dist/serve-register.d.ts +4 -0
  407. package/dist/serve-register.d.ts.map +1 -1
  408. package/dist/serve-register.js +9 -0
  409. package/dist/serve-register.js.map +1 -0
  410. package/dist/serve-slot.d.ts +3 -0
  411. package/dist/serve-slot.d.ts.map +1 -1
  412. package/dist/serve-slot.js +52 -0
  413. package/dist/serve-slot.js.map +1 -0
  414. package/dist/serve.d.ts +18 -1
  415. package/dist/serve.d.ts.map +1 -1
  416. package/dist/serve.js +157 -0
  417. package/dist/serve.js.map +1 -0
  418. package/dist/server-timing.d.ts.map +1 -1
  419. package/dist/server-timing.js +51 -0
  420. package/dist/server-timing.js.map +1 -0
  421. package/dist/static.d.ts +9 -0
  422. package/dist/static.d.ts.map +1 -1
  423. package/dist/static.js +102 -0
  424. package/dist/static.js.map +1 -0
  425. package/dist/telemetry/otel.d.ts +2 -0
  426. package/dist/telemetry/otel.d.ts.map +1 -1
  427. package/dist/telemetry/otel.js +135 -0
  428. package/dist/telemetry/otel.js.map +1 -0
  429. package/dist/testing.d.ts +12 -0
  430. package/dist/testing.d.ts.map +1 -1
  431. package/dist/testing.js +124 -0
  432. package/dist/testing.js.map +1 -0
  433. package/dist/timeout.d.ts +6 -0
  434. package/dist/timeout.d.ts.map +1 -1
  435. package/dist/timeout.js +36 -0
  436. package/dist/timeout.js.map +1 -0
  437. package/dist/tree.d.ts +133 -36
  438. package/dist/tree.d.ts.map +1 -1
  439. package/dist/tree.js +437 -0
  440. package/dist/tree.js.map +1 -0
  441. package/dist/trust.d.ts +21 -0
  442. package/dist/trust.d.ts.map +1 -0
  443. package/dist/trust.js +100 -0
  444. package/dist/trust.js.map +1 -0
  445. package/dist/ts-type-emitter.d.ts.map +1 -1
  446. package/dist/ts-type-emitter.js +150 -0
  447. package/dist/ts-type-emitter.js.map +1 -0
  448. package/dist/type-emitter.d.ts +16 -2
  449. package/dist/type-emitter.d.ts.map +1 -1
  450. package/dist/type-emitter.js +729 -0
  451. package/dist/type-emitter.js.map +1 -0
  452. package/dist/type-extractor.d.ts +8 -4
  453. package/dist/type-extractor.d.ts.map +1 -1
  454. package/dist/type-extractor.js +819 -0
  455. package/dist/type-extractor.js.map +1 -0
  456. package/dist/types.d.ts +7 -0
  457. package/dist/types.d.ts.map +1 -1
  458. package/dist/types.js +96 -0
  459. package/dist/types.js.map +1 -0
  460. package/dist/validation.d.ts +14 -2
  461. package/dist/validation.d.ts.map +1 -1
  462. package/dist/validation.js +335 -0
  463. package/dist/validation.js.map +1 -0
  464. package/dist/with-headers.d.ts +15 -0
  465. package/dist/with-headers.d.ts.map +1 -0
  466. package/dist/with-headers.js +45 -0
  467. package/dist/with-headers.js.map +1 -0
  468. package/dist/ws/bun.d.ts +11 -5
  469. package/dist/ws/bun.d.ts.map +1 -1
  470. package/dist/ws/bun.js +82 -0
  471. package/dist/ws/bun.js.map +1 -0
  472. package/dist/ws/cloudflare.d.ts +25 -3
  473. package/dist/ws/cloudflare.d.ts.map +1 -1
  474. package/dist/ws/cloudflare.js +117 -0
  475. package/dist/ws/cloudflare.js.map +1 -0
  476. package/dist/ws/deno.d.ts +4 -4
  477. package/dist/ws/deno.d.ts.map +1 -1
  478. package/dist/ws/deno.js +133 -0
  479. package/dist/ws/deno.js.map +1 -0
  480. package/dist/ws/node.d.ts +20 -3
  481. package/dist/ws/node.d.ts.map +1 -1
  482. package/dist/ws/node.js +173 -0
  483. package/dist/ws/node.js.map +1 -0
  484. package/dist/ws/session.d.ts +11 -0
  485. package/dist/ws/session.d.ts.map +1 -0
  486. package/dist/ws/session.js +60 -0
  487. package/dist/ws/session.js.map +1 -0
  488. package/dist/ws/shared.d.ts +65 -0
  489. package/dist/ws/shared.d.ts.map +1 -0
  490. package/dist/ws/shared.js +103 -0
  491. package/dist/ws/shared.js.map +1 -0
  492. package/dist/ws-origin.d.ts +20 -0
  493. package/dist/ws-origin.d.ts.map +1 -0
  494. package/dist/ws-origin.js +41 -0
  495. package/dist/ws-origin.js.map +1 -0
  496. package/dist/yaml.d.ts +15 -1
  497. package/dist/yaml.d.ts.map +1 -1
  498. package/dist/yaml.js +101 -0
  499. package/dist/yaml.js.map +1 -0
  500. package/docs/invalidate-spec.md +13 -1
  501. package/docs/meta-spec.md +36 -12
  502. package/docs/packaging.md +43 -24
  503. package/docs/regression-matrix/README.md +32 -0
  504. package/docs/regression-matrix/review.md +44 -0
  505. package/docs/regression-matrix/ws1-3.md +97 -0
  506. package/docs/regression-matrix/ws4-8.md +107 -0
  507. package/docs/regression-matrix/ws9-12.md +266 -0
  508. package/docs/sdk.md +102 -52
  509. package/package.json +183 -52
  510. package/src/accepts.ts +91 -33
  511. package/src/body-kind.ts +67 -0
  512. package/src/body-limit.ts +102 -87
  513. package/src/build/index.ts +104 -54
  514. package/src/cli-args.ts +123 -0
  515. package/src/cli-go/auth.go +12 -1
  516. package/src/cli-go/config.go +29 -2
  517. package/src/cli-go/errors.go +28 -10
  518. package/src/cli-go/multipart.go +51 -27
  519. package/src/cli-go/output.go +58 -4
  520. package/src/cli-go/root.go +1 -1
  521. package/src/cli-go/version.go +2 -2
  522. package/src/cli.ts +154 -148
  523. package/src/client/error.ts +56 -1
  524. package/src/client/http.ts +479 -256
  525. package/src/client/index.ts +55 -17
  526. package/src/client/path.ts +169 -0
  527. package/src/client/redirect-policy.ts +19 -0
  528. package/src/client/sdk.ts +180 -101
  529. package/src/client/sse.ts +92 -66
  530. package/src/client/ws.ts +67 -31
  531. package/src/client-go/errors.go +41 -12
  532. package/src/client-go/export.go +44 -0
  533. package/src/client-go/invalidation.go +52 -54
  534. package/src/client-go/nullable.go +62 -0
  535. package/src/client-go/realtime.go +147 -110
  536. package/src/client-go/runtime.go +646 -88
  537. package/src/client-go/sse.go +104 -94
  538. package/src/client-go/transport.go +67 -27
  539. package/src/client-go/ws.go +49 -25
  540. package/src/client-info.ts +79 -0
  541. package/src/client-mcp/server.ts +133 -3
  542. package/src/client-python/_invalidation.py +207 -291
  543. package/src/client-python/_realtime.py +63 -40
  544. package/src/client-python/_runtime.py +501 -119
  545. package/src/client-python/_sse.py +130 -74
  546. package/src/client-python/_transport.py +37 -9
  547. package/src/client-python/_ws.py +50 -22
  548. package/src/client-rust/errors.rs +21 -1
  549. package/src/client-rust/invalidation.rs +202 -231
  550. package/src/client-rust/invalidation_sync.rs +16 -229
  551. package/src/client-rust/nullable.rs +17 -0
  552. package/src/client-rust/realtime.rs +217 -217
  553. package/src/client-rust/runtime.rs +602 -177
  554. package/src/client-rust/runtime_sync.rs +169 -145
  555. package/src/client-rust/sse.rs +142 -121
  556. package/src/client-rust/ws.rs +61 -37
  557. package/src/codegen-go-cli.ts +607 -728
  558. package/src/codegen-go.ts +710 -770
  559. package/src/codegen-ir.ts +381 -75
  560. package/src/codegen-lang.ts +517 -0
  561. package/src/codegen-load.ts +150 -0
  562. package/src/codegen-mcp.ts +207 -108
  563. package/src/codegen-python.ts +684 -816
  564. package/src/codegen-route-tree-intern.ts +51 -25
  565. package/src/codegen-rust.ts +946 -1240
  566. package/src/codegen-sanitize.ts +0 -4
  567. package/src/codegen-schema-naming.ts +19 -6
  568. package/src/codegen-sdk-model.ts +228 -0
  569. package/src/codegen.ts +1488 -1140
  570. package/src/context.ts +130 -11
  571. package/src/cookie-sign.ts +85 -16
  572. package/src/cookie.ts +66 -11
  573. package/src/cors.ts +55 -54
  574. package/src/crypto.ts +27 -5
  575. package/src/csrf.ts +57 -25
  576. package/src/curl-logger.ts +155 -114
  577. package/src/dict.ts +12 -0
  578. package/src/error.ts +4 -2
  579. package/src/etag.ts +58 -34
  580. package/src/feature-detect.ts +147 -0
  581. package/src/feature-slots.ts +70 -0
  582. package/src/fetch-request.ts +13 -0
  583. package/src/gen-process.ts +42 -0
  584. package/src/gen-write.ts +127 -0
  585. package/src/glob.ts +109 -0
  586. package/src/go-type-emitter.ts +406 -155
  587. package/src/honey-response.ts +36 -9
  588. package/src/i18n-slot.ts +10 -0
  589. package/src/i18n.ts +98 -23
  590. package/src/index.ts +1887 -1756
  591. package/src/init.ts +40 -28
  592. package/src/invalidate-check.ts +42 -4
  593. package/src/invoke-user.ts +95 -0
  594. package/src/ip-restrict.ts +47 -161
  595. package/src/ip.ts +194 -0
  596. package/src/logger.ts +65 -36
  597. package/src/media-type.ts +104 -0
  598. package/src/meta-spec-merge.ts +80 -0
  599. package/src/meta-spec.ts +0 -0
  600. package/src/middleware.ts +112 -12
  601. package/src/node-request.ts +242 -52
  602. package/src/node.ts +318 -75
  603. package/src/openapi/collect.ts +19 -44
  604. package/src/openapi/docs-page.ts +89 -0
  605. package/src/openapi/document.ts +503 -224
  606. package/src/openapi/json-schema-slot.ts +40 -0
  607. package/src/openapi/register.ts +7 -2
  608. package/src/openapi/scalar.ts +11 -10
  609. package/src/openapi/serve.ts +39 -0
  610. package/src/openapi/spec-factory.ts +50 -1
  611. package/src/openapi/spec.ts +53 -30
  612. package/src/openapi/swagger.ts +12 -11
  613. package/src/pattern.ts +245 -0
  614. package/src/peer.ts +53 -0
  615. package/src/plugin.ts +270 -185
  616. package/src/powered-by.ts +2 -2
  617. package/src/pretty-json.ts +25 -9
  618. package/src/producer-stream.ts +296 -0
  619. package/src/proxy.ts +253 -55
  620. package/src/python-type-emitter.ts +68 -56
  621. package/src/realtime/bus.ts +56 -9
  622. package/src/realtime/register.ts +13 -0
  623. package/src/realtime/route.ts +129 -80
  624. package/src/realtime/server.ts +298 -0
  625. package/src/request-id.ts +13 -3
  626. package/src/request-limits.ts +2 -0
  627. package/src/request-path.ts +165 -0
  628. package/src/request-to-curl.ts +28 -7
  629. package/src/response.ts +143 -103
  630. package/src/rust-type-emitter.ts +265 -170
  631. package/src/secure-headers.ts +8 -4
  632. package/src/serve-register.ts +4 -0
  633. package/src/serve-slot.ts +38 -0
  634. package/src/serve.ts +113 -25
  635. package/src/server-timing.ts +11 -7
  636. package/src/static.ts +85 -32
  637. package/src/telemetry/otel.ts +29 -7
  638. package/src/testing.ts +76 -14
  639. package/src/timeout.ts +15 -7
  640. package/src/tree.ts +405 -333
  641. package/src/trust.ts +117 -0
  642. package/src/ts-type-emitter.ts +18 -12
  643. package/src/type-emitter.ts +227 -89
  644. package/src/type-extractor.ts +603 -423
  645. package/src/types.ts +8 -0
  646. package/src/validation.ts +221 -96
  647. package/src/with-headers.ts +45 -0
  648. package/src/ws/bun.ts +53 -15
  649. package/src/ws/cloudflare.ts +81 -19
  650. package/src/ws/deno.ts +124 -36
  651. package/src/ws/node.ts +173 -79
  652. package/src/ws/session.ts +73 -0
  653. package/src/ws/shared.ts +140 -0
  654. package/src/ws-origin.ts +53 -0
  655. package/src/yaml.ts +45 -22
  656. package/dist/client-mcp/server.d.ts +0 -10
  657. package/dist/client-mcp/server.d.ts.map +0 -1
  658. package/dist/client-mcp/types.d.ts +0 -16
  659. package/dist/client-mcp/types.d.ts.map +0 -1
  660. package/dist/cloudflare/realtime.d.ts +0 -30
  661. package/dist/cloudflare/realtime.d.ts.map +0 -1
  662. package/dist/realtime/buffer.d.ts +0 -18
  663. package/dist/realtime/buffer.d.ts.map +0 -1
  664. package/dist/realtime/longpoll.d.ts +0 -23
  665. package/dist/realtime/longpoll.d.ts.map +0 -1
  666. package/dist/realtime/protocol.d.ts +0 -50
  667. package/dist/realtime/protocol.d.ts.map +0 -1
  668. package/dist/realtime/runtime.d.ts +0 -74
  669. package/dist/realtime/runtime.d.ts.map +0 -1
  670. package/src/cloudflare/realtime.ts +0 -234
  671. package/src/realtime/buffer.ts +0 -101
  672. package/src/realtime/longpoll.ts +0 -148
  673. package/src/realtime/protocol.ts +0 -128
  674. package/src/realtime/runtime.ts +0 -427
@@ -1,20 +1,30 @@
1
- /* Rust SDK emitter — generates a complete Rust crate from an OpenAPI 3.1 spec. */
1
+ /* Rust SDK emitter — generates a complete Rust crate from an OpenAPI 3.1 spec.
2
+ *
3
+ * Input is the SDK request model (codegen-sdk-model.ts). Every crate-level type name comes from one
4
+ * NameScope seeded with the prelude and the runtime's own types; fields, params and methods are
5
+ * claimed per scope; spec strings reach source only through rustString / rustDoc.
6
+ */
2
7
 
3
8
  import { readFileSync } from "node:fs"
4
9
  import { fileURLToPath } from "node:url"
5
- import { collectSDKMethods, isSSEOperation, isStandardErrEnvelope } from "./codegen.ts"
6
- import { detectAuthScheme } from "./codegen-go.ts"
7
- import { methodsOf, namespacesOf, toIR } from "./codegen-ir.ts"
8
- import type { IRNamespace, IROperation } from "./codegen-ir.ts"
10
+ import type { OpenApiSpecInput } from "./codegen.ts"
11
+ import { serviceMapOf } from "./codegen.ts"
12
+ import { irErrorEnvelope, methodsOf, namespacesOf } from "./codegen-ir.ts"
13
+ import type { IRNamespace, IRSchema } from "./codegen-ir.ts"
9
14
  import {
10
- RUST_KEYWORDS,
11
- renderTopLevelRust,
12
- renderUseRust,
13
- rustIdent,
14
- rustMethodIdent,
15
- rustPascal,
16
- rustSnake,
17
- } from "./rust-type-emitter.ts"
15
+ NameScope,
16
+ RUST_PRELUDE_TYPES,
17
+ cmpCodeUnit,
18
+ rustDoc,
19
+ rustPlainSnake,
20
+ rustString,
21
+ rustTypeIdent,
22
+ rustValueIdent,
23
+ } from "./codegen-lang.ts"
24
+ import { buildSdkModel, isJsonMedia, valueCycleSchemas } from "./codegen-sdk-model.ts"
25
+ import type { SdkModel, SdkOp } from "./codegen-sdk-model.ts"
26
+ import { irRenderTopLevelRust, irRenderUseRust } from "./rust-type-emitter.ts"
27
+ import type { RustTypeNames } from "./rust-type-emitter.ts"
18
28
 
19
29
  export type RustSDKOptions = {
20
30
  crateName?: string
@@ -31,15 +41,6 @@ export type GeneratedRustSDK = {
31
41
  serviceMap: Record<string, Record<string, unknown>>
32
42
  }
33
43
 
34
- type OpenApiSpec = {
35
- components?: {
36
- schemas?: Record<string, Record<string, unknown>>
37
- }
38
- info: { description?: string; title: string; version: string }
39
- openapi: string
40
- paths: Record<string, Record<string, Record<string, unknown>>>
41
- }
42
-
43
44
  let runtimeCache: Map<string, string> | null = null
44
45
 
45
46
  export function loadRustRuntimeTemplates(): Map<string, string> {
@@ -55,6 +56,7 @@ export function loadRustRuntimeTemplates(): Map<string, string> {
55
56
  "ws.rs",
56
57
  "realtime.rs",
57
58
  "runtime_sync.rs",
59
+ "nullable.rs",
58
60
  ]
59
61
  const cache = new Map<string, string>()
60
62
  for (const name of names) {
@@ -79,351 +81,395 @@ export function validateCrateName(name: string): string {
79
81
  return name
80
82
  }
81
83
 
82
- function safeResourceName(name: string): string {
83
- const snake = rustSnake(name)
84
- return RUST_KEYWORDS.has(snake) ? `${snake}_` : snake
85
- }
86
-
87
- function rustResponseType(op: Record<string, unknown>): string {
88
- const responses = op.responses as Record<string, Record<string, unknown>> | undefined
89
- if (!responses) return "serde_json::Value"
90
-
91
- for (const [status, response] of Object.entries(responses)) {
92
- const code = Number.parseInt(status, 10)
93
- if (Number.isNaN(code) || code < 200 || code >= 300) continue
94
- if (code === 204) return ""
95
- const content = response.content as Record<string, Record<string, unknown>> | undefined
96
- if (!content) return ""
97
- const jsonSchema = content["application/json"]?.schema as Record<string, unknown> | undefined
98
- if (!jsonSchema) return "serde_json::Value"
99
- if (jsonSchema.$ref) {
100
- return rustPascal((jsonSchema.$ref as string).split("/").pop() ?? "")
101
- }
102
- if (jsonSchema.type === "array" && jsonSchema.items) {
103
- const items = jsonSchema.items as Record<string, unknown>
104
- if (items.$ref) {
105
- return `Vec<${rustPascal((items.$ref as string).split("/").pop() ?? "")}>`
106
- }
107
- return `Vec<${renderUseRust(items, { decls: new Map(), fieldName: "item", parentName: "Body" })}>`
108
- }
109
- if (jsonSchema.type === "string") return "String"
110
- if (jsonSchema.type === "integer") return "i64"
111
- if (jsonSchema.type === "number") return "f64"
112
- if (jsonSchema.type === "boolean") return "bool"
113
- return "serde_json::Value"
114
- }
115
- return "serde_json::Value"
116
- }
117
-
118
- type BodyInfo = {
119
- kind: "json" | "form-url" | "multipart" | "stream" | "none"
120
- type: string
84
+ /* Names the runtime modules and generated client declare or glob-import. */
85
+ const RUST_RUNTIME_TYPES = [
86
+ "ApiError",
87
+ "AuthState",
88
+ "BadGatewayError",
89
+ "BadRequestError",
90
+ "Client",
91
+ "ClientConfig",
92
+ "ClientInner",
93
+ "ConflictError",
94
+ "ConnectionState",
95
+ "ErrField",
96
+ "Error",
97
+ "ForbiddenError",
98
+ "GatewayTimeoutError",
99
+ "InternalServerError",
100
+ "InvalidationConfig",
101
+ "LogEntry",
102
+ "LongpollConn",
103
+ "LongpollState",
104
+ "LongpollTransport",
105
+ "NotFoundError",
106
+ "OnAuthExpiredHook",
107
+ "OnLogHook",
108
+ "OnRequestHook",
109
+ "OnResponseHook",
110
+ "QueryValue",
111
+ "RateLimitError",
112
+ "RealtimeError",
113
+ "RequestBody",
114
+ "RequestContext",
115
+ "RequestMeta",
116
+ "RequestResult",
117
+ "ResponseContext",
118
+ "ResponseMeta",
119
+ "ResumableConnection",
120
+ "ResumableConnectionOpts",
121
+ "SdkResult",
122
+ "ServiceEntry",
123
+ "ServiceUnavailableError",
124
+ "SseConn",
125
+ "SseEvent",
126
+ "SseParser",
127
+ "SseState",
128
+ "SseTransport",
129
+ "StaleEntry",
130
+ "StaleTracker",
131
+ "StaleTrackerSync",
132
+ "StatusError",
133
+ "SyncAuthState",
134
+ "SyncClient",
135
+ "SyncClientConfig",
136
+ "SyncClientInner",
137
+ "SyncOnRequestHook",
138
+ "SyncOnResponseHook",
139
+ "SyncRequestBody",
140
+ "SyncRequestContext",
141
+ "SyncRequestResult",
142
+ "SyncResponseContext",
143
+ "Transport",
144
+ "TransportConn",
145
+ "TransportKind",
146
+ "TransportOpts",
147
+ "TypedWebSocket",
148
+ "UnauthorizedError",
149
+ "UnprocessableEntityError",
150
+ "WsConn",
151
+ "WsTransport",
152
+ ]
153
+
154
+ type RustNames = {
155
+ scope: NameScope
156
+ types: RustTypeNames
157
+ decls: Map<string, string>
121
158
  }
122
159
 
123
- function rustBodyInfo(op: Record<string, unknown>): BodyInfo {
124
- const requestBody = op.requestBody as Record<string, unknown> | undefined
125
- if (!requestBody) return { kind: "none", type: "" }
126
- const content = requestBody.content as Record<string, Record<string, unknown>> | undefined
127
- if (!content) return { kind: "none", type: "" }
128
-
129
- /* octet-stream takes precedence: a single binary body has no sibling JSON/multipart
130
- * entry in practice; detect early so the emitted method accepts a stream/reader. */
131
- if (content["application/octet-stream"]) {
132
- return { kind: "stream", type: "" }
133
- }
134
- if (content["multipart/form-data"]) {
135
- return { kind: "multipart", type: "reqwest::multipart::Form" }
160
+ function buildRustNames(model: SdkModel): RustNames {
161
+ const scope = new NameScope([...RUST_PRELUDE_TYPES, ...RUST_RUNTIME_TYPES])
162
+ const schemaNames = new Map<string, string>()
163
+ for (const name of model.schemaNames) {
164
+ const base = rustTypeIdent(name)
165
+ schemaNames.set(name, scope.has(base) ? scope.claim(`${base}Model`) : scope.claim(base))
136
166
  }
137
- if (content["application/x-www-form-urlencoded"]) {
138
- return { kind: "form-url", type: "std::collections::HashMap<String, String>" }
139
- }
140
- const jsonContent = content["application/json"]
141
- if (!jsonContent?.schema) return { kind: "none", type: "" }
142
- const schema = jsonContent.schema as Record<string, unknown>
143
- if (schema.$ref) {
144
- return { kind: "json", type: rustPascal((schema.$ref as string).split("/").pop() ?? "") }
167
+ const hoistKeys = new Map<string, string>()
168
+ return {
169
+ decls: new Map(),
170
+ scope,
171
+ types: {
172
+ cyclic: valueCycleSchemas(model.ir.schemas),
173
+ hoist: (key, base) => {
174
+ const hit = hoistKeys.get(key)
175
+ if (hit) return hit
176
+ const name = scope.claim(rustTypeIdent(base))
177
+ hoistKeys.set(key, name)
178
+ return name
179
+ },
180
+ ref: (name) => schemaNames.get(name) ?? rustTypeIdent(name),
181
+ resolve: model.resolve,
182
+ },
145
183
  }
146
- return { kind: "json", type: "serde_json::Value" }
147
- }
148
-
149
- type QueryParam = {
150
- name: string
151
- required: boolean
152
- schema: Record<string, unknown>
153
- }
154
-
155
- function getQueryParams(op: Record<string, unknown>): QueryParam[] {
156
- const parameters = op.parameters as Array<Record<string, unknown>> | undefined
157
- if (!parameters) return []
158
- return parameters
159
- .filter((p) => p.in === "query")
160
- .map((p) => ({
161
- name: String(p.name),
162
- required: p.required === true,
163
- schema: (p.schema as Record<string, unknown>) ?? { type: "string" },
164
- }))
165
- .sort((a, b) => a.name.localeCompare(b.name))
166
184
  }
167
185
 
168
- type ErrEnvelopeInfo = {
169
- keyEnumName: string
170
- keys: string[]
171
- status: number
172
- variantName: string
186
+ /* ── per-op plan ── */
187
+
188
+ type RustOptField = { ident: string; type: string; wire: string; where: "query" | "header"; required: boolean }
189
+
190
+ type ErrEnvelopeInfo = { keyEnumName: string; keys: string[]; status: number; structName: string; variantName: string }
191
+
192
+ type RustOpPlan = {
193
+ op: SdkOp
194
+ fnName: string
195
+ optsName: string
196
+ optFields: RustOptField[]
197
+ pathParams: Array<{ local: string; wire: string }>
198
+ bodyParam?: {
199
+ local: string
200
+ asyncType: string
201
+ syncType: string
202
+ generic?: { async: string; asyncWhere?: string; sync: string }
203
+ }
204
+ /** Rust type of the decoded success payload, "" for none */
205
+ resultType: string
206
+ errorEnum?: string
207
+ envelopes: ErrEnvelopeInfo[]
208
+ /** invalidation map keys */
209
+ resource: string
210
+ action: string
173
211
  }
174
212
 
175
- /** Returns per-status error envelope infos for an operation (only standard envelope shapes). */
176
- function getOperationErrorEnvelopes(opId: string, op: Record<string, unknown>): ErrEnvelopeInfo[] {
177
- const responses = op.responses as Record<string, Record<string, unknown>> | undefined
178
- if (!responses) return []
179
- const envelopes: ErrEnvelopeInfo[] = []
180
- for (const [status, response] of Object.entries(responses)) {
213
+ const OPTS_RESERVED = [
214
+ "last_event_id",
215
+ "reconnect_token",
216
+ "protocols",
217
+ "idempotency_key",
218
+ "headers",
219
+ "timeout",
220
+ "cancel_token",
221
+ "sync_cancel_token",
222
+ "max_reconnect_attempts",
223
+ "reconnect_delay_ms",
224
+ ]
225
+
226
+ /** Locals every emitted method body uses. */
227
+ const METHOD_LOCALS = [
228
+ "self",
229
+ "opts",
230
+ "query",
231
+ "url_path",
232
+ "result",
233
+ "out",
234
+ "req",
235
+ "resp",
236
+ "stream",
237
+ "cfg",
238
+ "client",
239
+ "auth",
240
+ "body_value",
241
+ "call_headers",
242
+ "path_params",
243
+ "concrete_path",
244
+ "selector",
245
+ "request_meta",
246
+ "stale",
247
+ "targets",
248
+ "headers",
249
+ ]
250
+
251
+ /** Per-status error envelopes (standard shapes only), read from the IR with refs resolved. */
252
+ function errorEnvelopes(op: SdkOp, opPascal: string, model: SdkModel, names: RustNames): ErrEnvelopeInfo[] {
253
+ const out: ErrEnvelopeInfo[] = []
254
+ for (const [status, response] of Object.entries(op.ir.responses).sort(([a], [b]) => cmpCodeUnit(a, b))) {
181
255
  const code = Number.parseInt(status, 10)
182
- if (code < 400) continue
183
- const content = response.content as Record<string, Record<string, unknown>> | undefined
184
- const schema = content?.["application/json"]?.schema as Record<string, unknown> | undefined
185
- if (!schema) continue
186
- const envelope = isStandardErrEnvelope(schema)
256
+ if (!/^[0-9]{3}$/.test(status) || code < 400) continue
257
+ if (!response.schema || !response.contentType || !isJsonMedia(response.contentType)) continue
258
+ const envelope = irErrorEnvelope(response.schema, model.resolve)
187
259
  if (!envelope) continue
188
- const opPascal = pascalizeOpId(opId)
189
- envelopes.push({
190
- keyEnumName: `${opPascal}Err${code}Key`,
260
+ out.push({
261
+ keyEnumName: names.scope.claim(`${opPascal}Err${code}Key`),
191
262
  keys: envelope.keys,
192
263
  status: code,
264
+ structName: names.scope.claim(`${opPascal}Err${code}`),
193
265
  variantName: `Status${code}`,
194
266
  })
195
267
  }
196
- return envelopes
268
+ return out
197
269
  }
198
270
 
199
- /** Emits per-op typed error enum + key enums + per-status structs. Returns lines for the resource file. */
200
- function emitOperationErrorTypes(opId: string, op: Record<string, unknown>): string[] {
201
- const envelopes = getOperationErrorEnvelopes(opId, op)
202
- if (envelopes.length === 0) return []
203
- const opPascal = pascalizeOpId(opId)
204
- const errorEnumName = `${opPascal}Error`
271
+ function emitErrorTypes(plan: RustOpPlan): string[] {
272
+ if (plan.envelopes.length === 0 || !plan.errorEnum) return []
205
273
  const l: string[] = []
206
-
207
- for (const env of envelopes) {
208
- /* key enum */
209
- const variants = env.keys.map((k) => rustPascal(k))
274
+ for (const env of plan.envelopes) {
275
+ const variants = new NameScope()
210
276
  l.push(`#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]`)
211
- l.push(`#[serde(rename_all = "snake_case")]`)
212
277
  l.push(`pub enum ${env.keyEnumName} {`)
213
- for (const v of variants) {
278
+ for (const k of env.keys) {
279
+ const v = variants.claim(k === "" ? "Empty" : rustTypeIdent(k))
280
+ l.push(`\t#[serde(rename = ${rustString(k)})]`)
214
281
  l.push(`\t${v},`)
215
282
  }
216
283
  l.push(`}`)
217
284
  l.push(``)
218
-
219
- /* per-status struct with error_key typed field */
220
- const structName = `${opPascal}Err${env.status}`
221
285
  l.push(`#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]`)
222
- l.push(`pub struct ${structName} {`)
286
+ l.push(`pub struct ${env.structName} {`)
223
287
  l.push(`\tpub error_key: ${env.keyEnumName},`)
224
288
  l.push(`\tpub message: String,`)
225
289
  l.push(`\tpub fields: std::collections::HashMap<String, Vec<crate::types::ErrField>>,`)
226
290
  l.push(`}`)
227
291
  l.push(``)
228
292
  }
229
-
230
293
  l.push(`#[derive(Debug)]`)
231
- l.push(`pub enum ${errorEnumName} {`)
232
- for (const env of envelopes) {
233
- const structName = `${opPascal}Err${env.status}`
234
- l.push(`\t${env.variantName}(${structName}),`)
235
- }
294
+ l.push(`pub enum ${plan.errorEnum} {`)
295
+ for (const env of plan.envelopes) l.push(`\t${env.variantName}(${env.structName}),`)
236
296
  l.push(`\tOther(Error),`)
237
297
  l.push(`}`)
238
298
  l.push(``)
239
299
  return l
240
300
  }
241
301
 
242
- /** Returns the per-op typed error enum name if operation has typed error envelopes, else null. */
243
- function getOpErrorEnumName(opId: string, op: Record<string, unknown>): string | null {
244
- const envelopes = getOperationErrorEnvelopes(opId, op)
245
- if (envelopes.length === 0) return null
246
- return `${pascalizeOpId(opId)}Error`
247
- }
248
-
249
- type MethodInfo = {
250
- /* first segment of operationId — invalidationMap key */
251
- resource: string
252
- /* segments[1..] joined with "." — invalidationMap action key */
253
- action: string
254
- /* all segments except last — namespace path for struct name generation */
255
- namePath: string[]
256
- /* last segment of operationId — used for the Rust fn name */
257
- methodAction: string
258
- bodyInfo: BodyInfo
259
- description: string
260
- httpMethod: string
261
- isIdempotent: boolean
262
- isSSE: boolean
263
- isWS: boolean
264
- isRealtime: boolean
265
- op: Record<string, unknown>
266
- opId: string
267
- path: string
268
- pathParams: string[]
269
- queryParams: QueryParam[]
270
- responseType: string
271
- summary: string
272
- /** x-invalidate targets from the raw OpenAPI op, falling back to IR extensions. */
273
- invalidates: string[]
274
- }
275
-
276
- function pascalizeOpId(opId: string): string {
277
- return opId.split(".").map(rustPascal).join("")
278
- }
279
-
280
- function invalidatesOf(irOp: IROperation, rawOp: Record<string, unknown>): string[] {
281
- const fromRaw = rawOp["x-invalidate"]
282
- if (Array.isArray(fromRaw) && fromRaw.length > 0 && fromRaw.every((x) => typeof x === "string")) {
283
- return fromRaw as string[]
284
- }
285
- const fromIr = irOp.extensions.invalidates
286
- if (Array.isArray(fromIr) && fromIr.length > 0) return fromIr
287
- return []
288
- }
289
-
290
- function methodInfoFromIROp(irOp: IROperation, rawOp: Record<string, unknown>): MethodInfo {
291
- const segments = irOp.id.split(".")
292
- const isTopLevel = segments.length === 1
293
- const resource = segments[0] ?? irOp.id
294
- const action = isTopLevel ? "_call" : segments.slice(1).join(".")
295
- const namePath = segments.slice(0, -1)
296
- const methodAction = segments[segments.length - 1] ?? irOp.id
297
- return {
298
- action,
299
- bodyInfo: rustBodyInfo(rawOp),
300
- description: String(rawOp.description ?? ""),
301
- httpMethod: irOp.method,
302
- invalidates: invalidatesOf(irOp, rawOp),
303
- isIdempotent: rawOp["x-idempotency-key"] === true,
304
- isRealtime: rawOp["x-realtime"] === true,
305
- isSSE: isSSEOperation(rawOp),
306
- isWS: rawOp["x-websocket"] === true,
307
- methodAction,
308
- namePath,
309
- op: rawOp,
310
- opId: irOp.id,
311
- path: irOp.path,
312
- pathParams: irOp.params.path.map((p) => p.name),
313
- queryParams: getQueryParams(rawOp),
314
- resource,
315
- responseType: rustResponseType(rawOp),
316
- summary: String(rawOp.summary ?? ""),
317
- }
318
- }
319
-
320
- function collectMethodsFromIR(ns: IRNamespace, spec: OpenApiSpec): MethodInfo[] {
321
- const rawOpById = new Map<string, Record<string, unknown>>()
322
- for (const [, pathMethods] of Object.entries(spec.paths ?? {})) {
323
- for (const [, operation] of Object.entries(pathMethods)) {
324
- const op = operation as Record<string, unknown>
325
- const id = op.operationId as string | undefined
326
- if (id) rawOpById.set(id, op)
302
+ function planOp(
303
+ op: SdkOp,
304
+ fnName: string,
305
+ pathSegs: string[],
306
+ names: RustNames,
307
+ throwOnError: boolean,
308
+ model: SdkModel,
309
+ ): RustOpPlan {
310
+ const opPascal = pathSegs.map(rustTypeIdent).join("")
311
+ const optsName = names.scope.claim(`${opPascal}Opts`)
312
+ const locals = new NameScope(METHOD_LOCALS)
313
+ const plan: RustOpPlan = {
314
+ action: op.segments.length === 1 ? "_call" : op.segments.slice(1).join("."),
315
+ envelopes: [],
316
+ fnName,
317
+ op,
318
+ optFields: [],
319
+ optsName,
320
+ pathParams: [],
321
+ resource: op.segments[0] ?? op.id,
322
+ resultType: "",
323
+ }
324
+
325
+ const hasBody = op.body !== undefined && op.stream !== "ws" && op.stream !== "realtime"
326
+ const bodyLocal = hasBody ? locals.claim("body") : ""
327
+ for (const p of op.pathParams) plan.pathParams.push({ local: locals.claim(rustPlainSnake(p.name)), wire: p.name })
328
+
329
+ if (op.stream !== "realtime") {
330
+ const fieldScope = new NameScope(OPTS_RESERVED)
331
+ const params: Array<[{ name: string; schema: IRSchema; required?: boolean }, "query" | "header"]> = [
332
+ ...op.query.map((p): [typeof p, "query"] => [p, "query"]),
333
+ ...op.headers.map((p): [typeof p, "header"] => [p, "header"]),
334
+ ]
335
+ for (const [p, where] of params) {
336
+ if (op.stream === "sse" && where === "query" && /^last[-_]?event[-_]?id$/i.test(p.name)) continue
337
+ const ident = fieldScope.claim(rustPlainSnake(p.name))
338
+ const t = irRenderUseRust(p.schema, {
339
+ decls: names.decls,
340
+ fieldName: p.name,
341
+ names: names.types,
342
+ parentName: optsName,
343
+ })
344
+ plan.optFields.push({
345
+ ident,
346
+ required: p.required === true,
347
+ type: p.required === true || t.startsWith("Option<") ? t : `Option<${t}>`,
348
+ where,
349
+ wire: p.name,
350
+ })
327
351
  }
328
352
  }
329
- const results: MethodInfo[] = []
330
- function walk(cur: IRNamespace): void {
331
- for (const [, entry] of methodsOf(cur)) {
332
- const raw = rawOpById.get(entry.id) ?? {}
333
- results.push(methodInfoFromIROp(entry, raw))
353
+
354
+ if (hasBody && op.body) {
355
+ const b = op.body
356
+ switch (b.kind) {
357
+ case "json": {
358
+ let t: string
359
+ if (b.schema.kind === "ref") t = names.types.ref(b.schema.name)
360
+ else
361
+ t = irRenderUseRust(b.schema, {
362
+ decls: names.decls,
363
+ fieldName: "Body",
364
+ names: names.types,
365
+ parentName: opPascal,
366
+ })
367
+ plan.bodyParam = { asyncType: `&${t}`, local: bodyLocal, syncType: `&${t}` }
368
+ break
369
+ }
370
+ case "form":
371
+ plan.bodyParam = {
372
+ asyncType: "&std::collections::HashMap<String, String>",
373
+ local: bodyLocal,
374
+ syncType: "&std::collections::HashMap<String, String>",
375
+ }
376
+ break
377
+ case "multipart":
378
+ /* multipart Form is consumed (not Clone), passed by value */
379
+ plan.bodyParam = {
380
+ asyncType: "reqwest::multipart::Form",
381
+ local: bodyLocal,
382
+ syncType: "reqwest::blocking::multipart::Form",
383
+ }
384
+ break
385
+ case "binary":
386
+ /* octet-stream body: generic over any byte stream / reader; single-shot, not retried on 401 */
387
+ plan.bodyParam = {
388
+ asyncType: "S",
389
+ generic: {
390
+ async: `S`,
391
+ asyncWhere: ` where S: futures::TryStream + Send + 'static, S::Error: Into<Box<dyn std::error::Error + Send + Sync>>, bytes::Bytes: From<S::Ok>`,
392
+ sync: `R: std::io::Read + Send + 'static`,
393
+ },
394
+ local: bodyLocal,
395
+ syncType: "R",
396
+ }
397
+ break
398
+ case "text":
399
+ plan.bodyParam = { asyncType: "&str", local: bodyLocal, syncType: "&str" }
400
+ break
334
401
  }
335
- for (const [, childNs] of namespacesOf(cur)) walk(childNs)
336
402
  }
337
- walk(ns)
338
- return results
339
- }
340
403
 
341
- function optsStructName(m: MethodInfo): string {
342
- /* namePath=[checkout,sessions], methodAction=create → CheckoutSessionsCreateOpts
343
- * namePath=[], methodAction=getUser → GetUserOpts (top-level single-segment) */
344
- const prefix = m.namePath.map(rustPascal).join("")
345
- const suffix = rustPascal(m.methodAction)
346
- return `${prefix}${suffix}Opts`
404
+ if (op.stream === null) {
405
+ const s = op.success
406
+ if (s.kind === "json") {
407
+ if (!s.schema) plan.resultType = "serde_json::Value"
408
+ else if (s.schema.kind === "ref") plan.resultType = names.types.ref(s.schema.name)
409
+ else {
410
+ const t = irRenderUseRust(s.schema, {
411
+ decls: names.decls,
412
+ fieldName: "Response",
413
+ names: names.types,
414
+ parentName: opPascal,
415
+ })
416
+ plan.resultType = t.startsWith("Box<") ? t.slice(4, -1) : t
417
+ }
418
+ } else if (s.kind === "text") plan.resultType = "String"
419
+ else if (s.kind === "binary") plan.resultType = "Vec<u8>"
420
+ /* the typed error enums exist in both modes; only safe-mode methods return them */
421
+ plan.envelopes = errorEnvelopes(op, opPascal, model, names)
422
+ if (plan.envelopes.length > 0) plan.errorEnum = names.scope.claim(`${opPascal}Error`)
423
+ }
424
+ void model
425
+ return plan
347
426
  }
348
427
 
349
- function emitOptsStruct(m: MethodInfo): string[] {
428
+ function emitOptsStruct(plan: RustOpPlan): string[] {
350
429
  const l: string[] = []
351
- const optsName = optsStructName(m)
352
- const decls = new Map<string, string>()
430
+ const { op, optsName } = plan
353
431
  const fields: string[] = []
354
-
355
- const emitted = new Set<string>()
356
- for (const qp of m.queryParams) {
357
- const fieldIdent = rustIdent(qp.name)
358
- emitted.add(fieldIdent)
359
- const rustType = renderUseRust(qp.schema, {
360
- decls,
361
- fieldName: qp.name,
362
- parentName: optsName,
363
- })
364
- const finalType = qp.required ? rustType : `Option<${rustType}>`
365
- const needsRename = fieldIdent !== qp.name
366
- const needsSkip = !qp.required
367
- if (needsRename && needsSkip) {
368
- fields.push(`\t#[serde(rename = ${JSON.stringify(qp.name)}, skip_serializing_if = "Option::is_none")]`)
369
- } else if (needsRename) {
370
- fields.push(`\t#[serde(rename = ${JSON.stringify(qp.name)})]`)
371
- } else if (needsSkip) {
372
- fields.push(`\t#[serde(skip_serializing_if = "Option::is_none")]`)
373
- }
374
- fields.push(`\tpub ${fieldIdent}: ${finalType},`)
375
- }
376
-
377
- if (m.isSSE && !emitted.has("last_event_id")) {
378
- fields.push(`\tpub last_event_id: Option<String>,`)
379
- }
380
- if (m.isWS && !emitted.has("reconnect_token")) {
432
+ if (op.stream === "realtime") {
381
433
  fields.push(`\tpub reconnect_token: Option<String>,`)
382
- }
383
- if (m.isWS && !emitted.has("protocols")) {
434
+ fields.push(`\tpub last_event_id: Option<String>,`)
384
435
  fields.push(`\tpub protocols: Option<Vec<String>>,`)
385
- }
386
-
387
- /* idempotency-key auto-injection opt-in field (9.a.5). Shared between async + sync
388
- * method bodies; emitted once per opts struct. Not meaningful for SSE/WS/realtime. */
389
- if (m.isIdempotent && !m.isSSE && !m.isWS && !m.isRealtime && !emitted.has("idempotency_key")) {
390
- fields.push(`\t#[serde(skip_serializing_if = "Option::is_none")]`)
391
- fields.push(`\tpub idempotency_key: Option<String>,`)
392
- }
393
-
394
- /* per-call headers for all non-WS methods (SSE included) */
395
- if (!m.isWS && !emitted.has("headers")) {
436
+ fields.push(`\tpub max_reconnect_attempts: Option<u32>,`)
437
+ fields.push(`\tpub reconnect_delay_ms: Option<u64>,`)
396
438
  fields.push(`\t#[serde(skip)]`)
397
439
  fields.push(`\tpub headers: Option<std::collections::HashMap<String, String>>,`)
398
- }
399
-
400
- /* per-call timeout for non-SSE non-WS methods */
401
- if (!m.isSSE && !m.isWS && !emitted.has("timeout")) {
402
- fields.push(`\t#[serde(skip)]`)
403
- fields.push(`\tpub timeout: Option<std::time::Duration>,`)
404
- }
405
-
406
- /* per-call cancellation tokens for non-SSE non-WS methods. Two separate fields:
407
- async path consumes `cancel_token` (tokio_util::sync::CancellationToken);
408
- sync path consumes `sync_cancel_token` (Arc<AtomicBool>). The opts struct is
409
- shared between async + sync emitted methods, so both fields coexist; each
410
- path ignores the other. Both #[serde(skip)] — runtime-only state. */
411
- if (!m.isSSE && !m.isWS) {
412
- if (!emitted.has("cancel_token")) {
440
+ } else {
441
+ for (const f of plan.optFields) {
442
+ const bare = f.ident.startsWith("r#") ? f.ident.slice(2) : f.ident
443
+ const attrs: string[] = []
444
+ if (bare !== f.wire) attrs.push(`rename = ${rustString(f.wire)}`)
445
+ if (!f.required) attrs.push(`skip_serializing_if = "Option::is_none"`)
446
+ if (attrs.length > 0) fields.push(`\t#[serde(${attrs.join(", ")})]`)
447
+ fields.push(`\tpub ${f.ident}: ${f.type},`)
448
+ }
449
+ if (op.stream === "sse") fields.push(`\tpub last_event_id: Option<String>,`)
450
+ if (op.stream === "ws") {
451
+ fields.push(`\tpub reconnect_token: Option<String>,`)
452
+ fields.push(`\tpub protocols: Option<Vec<String>>,`)
453
+ }
454
+ if (op.idempotent && op.stream === null) {
455
+ fields.push(`\t#[serde(skip_serializing_if = "Option::is_none")]`)
456
+ fields.push(`\tpub idempotency_key: Option<String>,`)
457
+ }
458
+ /* per-call headers for all non-WS methods (SSE included) */
459
+ if (op.stream !== "ws") {
413
460
  fields.push(`\t#[serde(skip)]`)
414
- fields.push(`\tpub cancel_token: Option<tokio_util::sync::CancellationToken>,`)
461
+ fields.push(`\tpub headers: Option<std::collections::HashMap<String, String>>,`)
415
462
  }
416
- if (!emitted.has("sync_cancel_token")) {
463
+ if (op.stream === null) {
464
+ fields.push(`\t#[serde(skip)]`)
465
+ fields.push(`\tpub timeout: Option<std::time::Duration>,`)
466
+ /* async consumes `cancel_token`, sync consumes `sync_cancel_token`; both runtime-only */
467
+ fields.push(`\t#[serde(skip)]`)
468
+ fields.push(`\tpub cancel_token: Option<tokio_util::sync::CancellationToken>,`)
417
469
  fields.push(`\t#[serde(skip)]`)
418
470
  fields.push(`\tpub sync_cancel_token: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,`)
419
471
  }
420
472
  }
421
-
422
- for (const decl of decls.values()) {
423
- l.push(decl)
424
- l.push(``)
425
- }
426
-
427
473
  l.push(`#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]`)
428
474
  l.push(`pub struct ${optsName} {`)
429
475
  l.push(...fields)
@@ -432,681 +478,478 @@ function emitOptsStruct(m: MethodInfo): string[] {
432
478
  return l
433
479
  }
434
480
 
435
- function emitSSEMethod(
436
- m: MethodInfo,
437
- params: string[],
438
- cfgAccess: string,
439
- httpClientAccess: string,
440
- actionFn: string,
441
- optsName: string,
442
- l: string[],
443
- ): string[] {
444
- params.push(`opts: &${optsName}`)
445
- l.push(`pub fn ${actionFn}(${params.join(", ")}) -> impl Stream<Item = Result<SseEvent, Error>> {`)
446
- /* clone all opts fields into owned locals before the stream block to avoid capturing &opts lifetime */
447
- l.push(`\tlet last_event_id_owned = opts.last_event_id.clone();`)
448
- l.push(`\tlet opts_headers_owned = opts.headers.clone();`)
449
- for (const qp of m.queryParams) {
450
- if (qp.name === "last-event-id") continue
451
- const fieldIdent = rustIdent(qp.name)
452
- l.push(`\tlet ${fieldIdent}_owned = opts.${fieldIdent}.clone();`)
453
- }
454
- const urlExpr = buildUrlExpr(m.path, m.pathParams)
455
- l.push(`\tlet url_path = ${urlExpr};`)
456
- const sseQueryMut = m.queryParams.filter((q) => q.name !== "last-event-id").length > 0 ? "mut " : ""
457
- l.push(`\tlet ${sseQueryMut}query: Vec<(String, String)> = Vec::new();`)
458
- for (const qp of m.queryParams) {
459
- if (qp.name === "last-event-id") continue
460
- const fieldIdent = rustIdent(qp.name)
461
- if (qp.required) {
462
- l.push(`\tquery.push((${JSON.stringify(qp.name)}.to_string(), ${fieldIdent}_owned.clone()));`)
463
- } else {
464
- l.push(
465
- `\tif let Some(ref v) = ${fieldIdent}_owned { query.push((${JSON.stringify(qp.name)}.to_string(), v.clone())); }`,
466
- )
467
- }
481
+ function docLines(op: SdkOp): string[] {
482
+ const l: string[] = []
483
+ if (op.summary) l.push(...rustDoc(op.summary))
484
+ if (op.description) {
485
+ if (op.summary) l.push(`///`)
486
+ l.push(...rustDoc(op.description))
468
487
  }
469
- l.push(`\tlet cfg_clone = ${cfgAccess}.clone();`)
470
- l.push(`\tlet client_clone = ${httpClientAccess}.clone();`)
471
- l.push(`\tasync_stream::stream! {`)
472
- l.push(`\t\tuse crate::sse::parse_sse_stream;`)
473
- l.push(`\t\tlet mut req_headers = std::collections::HashMap::new();`)
474
- l.push(`\t\treq_headers.insert("Accept".to_string(), "text/event-stream".to_string());`)
475
- l.push(`\t\tif let Some(ref lei) = last_event_id_owned {`)
476
- l.push(`\t\t\treq_headers.insert("Last-Event-ID".to_string(), lei.clone());`)
477
- l.push(`\t\t}`)
478
- l.push(`\t\tfor (k, v) in &cfg_clone.headers {`)
479
- l.push(`\t\t\treq_headers.insert(k.clone(), v.clone());`)
480
- l.push(`\t\t}`)
481
- l.push(`\t\tif let Some(ref oh) = opts_headers_owned {`)
482
- l.push(`\t\t\tfor (k, v) in oh { req_headers.insert(k.clone(), v.clone()); }`)
483
- l.push(`\t\t}`)
484
- l.push(`\t\tlet mut req = client_clone`)
485
- l.push(`\t\t\t.get(format!("{}{}", cfg_clone.base_url, url_path))`)
486
- l.push(`\t\t\t.header("Accept", "text/event-stream");`)
487
- l.push(`\t\tif let Some(ref lei) = last_event_id_owned {`)
488
- l.push(`\t\t\treq = req.header("Last-Event-ID", lei.clone());`)
489
- l.push(`\t\t}`)
490
- l.push(`\t\tmatch req.send().await {`)
491
- l.push(`\t\t\tErr(e) => { yield Err(Error::Transport(e)); return; }`)
492
- l.push(`\t\t\tOk(r) => {`)
493
- l.push(`\t\t\t\tuse futures_util::StreamExt;`)
494
- l.push(`\t\t\t\tlet mut s = std::pin::pin!(parse_sse_stream(r));`)
495
- l.push(`\t\t\t\twhile let Some(item) = s.next().await { yield item; }`)
496
- l.push(`\t\t\t}`)
497
- l.push(`\t\t}`)
498
- l.push(`\t}`)
499
- l.push(`}`)
500
- l.push(``)
488
+ if (op.deprecated) l.push(`#[deprecated(note = "the API marks this operation deprecated")]`)
501
489
  return l
502
490
  }
503
491
 
504
- function emitWSMethod(
505
- m: MethodInfo,
506
- params: string[],
507
- cfgAccess: string,
508
- actionFn: string,
509
- optsName: string,
510
- l: string[],
511
- ): string[] {
512
- params.push(`opts: &${optsName}`)
513
- l.push(`pub async fn ${actionFn}(${params.join(", ")}) -> Result<TypedWebSocket, Error> {`)
514
- const urlExpr = buildUrlExpr(m.path, m.pathParams)
515
- l.push(`\tlet url_path = ${urlExpr};`)
516
- l.push(`\tlet mut query_str = String::new();`)
517
- for (const qp of m.queryParams) {
518
- if (qp.name === "reconnect_token") continue
519
- const fieldIdent = rustIdent(qp.name)
520
- if (qp.required) {
521
- l.push(`\tif !query_str.is_empty() { query_str.push('&'); }`)
522
- l.push(
523
- `\tquery_str.push_str(&format!("{}={}", ${JSON.stringify(qp.name)}, urlencoding::encode(&opts.${fieldIdent})));`,
524
- )
525
- } else {
526
- l.push(`\tif let Some(ref v) = opts.${fieldIdent} {`)
527
- l.push(`\t\tif !query_str.is_empty() { query_str.push('&'); }`)
528
- l.push(`\t\tquery_str.push_str(&format!("{}={}", ${JSON.stringify(qp.name)}, urlencoding::encode(v)));`)
529
- l.push(`\t}`)
530
- }
531
- }
532
- l.push(`\tif let Some(ref rt) = opts.reconnect_token {`)
533
- l.push(`\t\tif !query_str.is_empty() { query_str.push('&'); }`)
534
- l.push(`\t\tquery_str.push_str(&format!("reconnect_token={}", urlencoding::encode(rt)));`)
535
- l.push(`\t}`)
536
- l.push(`\tlet full_url = if query_str.is_empty() {`)
537
- l.push(`\t\tformat!("{}{}", ${cfgAccess}.base_url, url_path)`)
538
- l.push(`\t} else {`)
539
- l.push(`\t\tformat!("{}{}?{}", ${cfgAccess}.base_url, url_path, query_str)`)
540
- l.push(`\t};`)
541
- l.push(`\t/* convert http(s) → ws(s) */`)
542
- l.push(`\tlet ws_url = full_url.replacen("https://", "wss://", 1).replacen("http://", "ws://", 1);`)
543
- l.push(`\tuse tokio_tungstenite::tungstenite::client::IntoClientRequest;`)
544
- l.push(`\tlet mut req = ws_url.as_str().into_client_request()`)
545
- l.push(`\t\t.map_err(|e| Error::Other(e.to_string()))?;`)
546
- l.push(`\tif let Some(ref protos) = opts.protocols {`)
547
- l.push(`\t\tif !protos.is_empty() {`)
548
- l.push(`\t\t\treq.headers_mut().insert(`)
549
- l.push(`\t\t\t\t"Sec-WebSocket-Protocol",`)
550
- l.push(
551
- `\t\t\t\tprotos.join(", ").parse().map_err(|e: tokio_tungstenite::tungstenite::http::header::InvalidHeaderValue| Error::Other(e.to_string()))?,`,
552
- )
553
- l.push(`\t\t\t);`)
554
- l.push(`\t\t}`)
555
- l.push(`\t}`)
556
- l.push(`\tlet (stream, _) = tokio_tungstenite::connect_async(req).await`)
557
- l.push(`\t\t.map_err(|e| Error::Other(e.to_string()))?;`)
558
- l.push(`\tOk(TypedWebSocket::new(stream))`)
559
- l.push(`}`)
560
- l.push(``)
561
- return l
492
+ function pathLines(plan: RustOpPlan): string[] {
493
+ if (plan.pathParams.length === 0) return [`\tlet url_path = ${rustString(plan.op.path)}.to_string();`]
494
+ const params = plan.pathParams.map((p) => `(${rustString(p.wire)}, ${p.local})`).join(", ")
495
+ return [`\tlet url_path = crate::runtime::expand_path(${rustString(plan.op.path)}, &[${params}])?;`]
562
496
  }
563
497
 
564
- function buildAccessors(selfAccess: "self" | "self.client"): { cfg: string; stale: string; httpClient: string } {
565
- return selfAccess === "self"
566
- ? { cfg: "self.inner.cfg", stale: "self.inner.stale", httpClient: "self.inner.http_client" }
567
- : { cfg: "self.client.cfg", stale: "self.client.stale", httpClient: "self.client.http_client" }
498
+ function queryLines(plan: RustOpPlan, opts = "opts"): string[] {
499
+ const q = plan.optFields.filter((f) => f.where === "query")
500
+ const l = [`\tlet ${q.length > 0 ? "mut " : ""}query: Vec<(String, String)> = Vec::new();`]
501
+ for (const f of q)
502
+ l.push(`\tcrate::runtime::push_serialized(${rustString(f.wire)}, &${opts}.${f.ident}, &mut query);`)
503
+ return l
568
504
  }
569
505
 
570
- function emitMethod(m: MethodInfo, selfAccess: "self" | "self.client", throwOnError: boolean): string[] {
571
- const l: string[] = []
572
- const actionFn = rustMethodIdent(m.action === "_call" ? m.resource : m.methodAction)
573
- const optsName = optsStructName(m)
574
- const httpMethod = m.httpMethod
575
-
576
- if (m.summary) {
577
- l.push(`/// ${m.summary}`)
578
- if (m.description) l.push(`/// ${m.description}`)
579
- }
580
-
581
- const params: string[] = [`&self`]
582
- for (const pp of m.pathParams) {
583
- params.push(`${rustSnake(pp)}: &str`)
584
- }
585
- /* body param depends on body kind; `asyncGenerics` collects generic parameter
586
- * declarations for the function signature (only stream bodies need generics
587
- * for zero-cost monomorphization over any `Stream<Bytes>`). */
588
- const asyncGenerics: string[] = []
589
- const bi = m.bodyInfo
590
- if (bi.kind === "json") {
591
- params.push(`body: &${bi.type}`)
592
- } else if (bi.kind === "form-url") {
593
- params.push(`body: &std::collections::HashMap<String, String>`)
594
- } else if (bi.kind === "multipart") {
595
- /* multipart Form is consumed (not Clone), passed by value */
596
- params.push(`body: reqwest::multipart::Form`)
597
- } else if (bi.kind === "stream") {
598
- /* octet-stream body: generic over any `Stream<Item = Result<Bytes, reqwest::Error>>`
599
- * (reqwest::Body::wrap_stream bound). Owned body, single-shot, non-retryable on 401. */
600
- asyncGenerics.push(`S: futures::Stream<Item = Result<bytes::Bytes, reqwest::Error>> + Send + Sync + 'static`)
601
- params.push(`body: S`)
602
- }
603
-
604
- const { cfg: cfgAccess, stale: staleAccess, httpClient: httpClientAccess } = buildAccessors(selfAccess)
605
-
606
- if (m.isSSE) {
607
- params.push(`opts: &${optsName}`)
608
- l.push(`pub fn ${actionFn}(${params.join(", ")}) -> impl Stream<Item = Result<SseEvent, Error>> {`)
609
- /* clone all opts fields into owned locals before the stream block to avoid capturing &opts lifetime */
610
- l.push(`\tlet last_event_id_owned = opts.last_event_id.clone();`)
611
- l.push(`\tlet opts_headers_owned = opts.headers.clone();`)
612
- for (const qp of m.queryParams) {
613
- if (qp.name === "last-event-id") continue
614
- const fieldIdent = rustIdent(qp.name)
615
- l.push(`\tlet ${fieldIdent}_owned = opts.${fieldIdent}.clone();`)
616
- }
617
-
618
- const urlExpr = buildUrlExpr(m.path, m.pathParams)
619
- l.push(`\tlet url_path = ${urlExpr};`)
620
- const sseQueryMut = m.queryParams.filter((q) => q.name !== "last-event-id").length > 0 ? "mut " : ""
621
- l.push(`\tlet ${sseQueryMut}query: Vec<(String, String)> = Vec::new();`)
622
- for (const qp of m.queryParams) {
623
- if (qp.name === "last-event-id") continue
624
- const fieldIdent = rustIdent(qp.name)
625
- if (qp.required) {
626
- l.push(`\tquery.push((${JSON.stringify(qp.name)}.to_string(), ${fieldIdent}_owned.clone()));`)
627
- } else {
628
- l.push(
629
- `\tif let Some(ref v) = ${fieldIdent}_owned { query.push((${JSON.stringify(qp.name)}.to_string(), v.clone())); }`,
630
- )
631
- }
632
- }
633
- l.push(`\tlet cfg_clone = ${cfgAccess}.clone();`)
634
- l.push(`\tlet client_clone = ${httpClientAccess}.clone();`)
635
- l.push(`\tasync_stream::stream! {`)
636
- l.push(`\t\tuse crate::sse::parse_sse_stream;`)
637
- l.push(`\t\tlet mut req_headers = std::collections::HashMap::new();`)
638
- l.push(`\t\treq_headers.insert("Accept".to_string(), "text/event-stream".to_string());`)
639
- l.push(`\t\tif let Some(ref lei) = last_event_id_owned {`)
640
- l.push(`\t\t\treq_headers.insert("Last-Event-ID".to_string(), lei.clone());`)
641
- l.push(`\t\t}`)
642
- l.push(`\t\tfor (k, v) in &cfg_clone.headers {`)
643
- l.push(`\t\t\treq_headers.insert(k.clone(), v.clone());`)
644
- l.push(`\t\t}`)
645
- l.push(`\t\tif let Some(ref oh) = opts_headers_owned {`)
646
- l.push(`\t\t\tfor (k, v) in oh { req_headers.insert(k.clone(), v.clone()); }`)
647
- l.push(`\t\t}`)
648
- l.push(`\t\tlet mut req = client_clone`)
649
- l.push(`\t\t\t.get(format!("{}{}", cfg_clone.base_url, url_path))`)
650
- l.push(`\t\t\t.header("Accept", "text/event-stream");`)
651
- l.push(`\t\tif let Some(ref lei) = last_event_id_owned {`)
652
- l.push(`\t\t\treq = req.header("Last-Event-ID", lei.clone());`)
653
- l.push(`\t\t}`)
654
- l.push(`\t\tmatch req.send().await {`)
655
- l.push(`\t\t\tErr(e) => { yield Err(Error::Transport(e)); return; }`)
656
- l.push(`\t\t\tOk(r) => {`)
657
- l.push(`\t\t\t\tuse futures_util::StreamExt;`)
658
- l.push(`\t\t\t\tlet mut s = std::pin::pin!(parse_sse_stream(r));`)
659
- l.push(`\t\t\t\twhile let Some(item) = s.next().await { yield item; }`)
660
- l.push(`\t\t\t}`)
661
- l.push(`\t\t}`)
662
- l.push(`\t}`)
663
- l.push(`}`)
664
- l.push(``)
665
- return l
666
- }
667
-
668
- if (m.isWS) {
669
- params.push(`opts: &${optsName}`)
670
- l.push(`pub async fn ${actionFn}(${params.join(", ")}) -> Result<TypedWebSocket, Error> {`)
671
- const urlExpr = buildUrlExpr(m.path, m.pathParams)
672
- l.push(`\tlet url_path = ${urlExpr};`)
673
- l.push(`\tlet mut query_str = String::new();`)
674
- for (const qp of m.queryParams) {
675
- if (qp.name === "reconnect_token") continue
676
- const fieldIdent = rustIdent(qp.name)
677
- if (qp.required) {
678
- l.push(`\tif !query_str.is_empty() { query_str.push('&'); }`)
679
- l.push(
680
- `\tquery_str.push_str(&format!("{}={}", ${JSON.stringify(qp.name)}, urlencoding::encode(&opts.${fieldIdent})));`,
681
- )
682
- } else {
683
- l.push(`\tif let Some(ref v) = opts.${fieldIdent} {`)
684
- l.push(`\t\tif !query_str.is_empty() { query_str.push('&'); }`)
685
- l.push(`\t\tquery_str.push_str(&format!("{}={}", ${JSON.stringify(qp.name)}, urlencoding::encode(v)));`)
686
- l.push(`\t}`)
687
- }
688
- }
689
- l.push(`\tif let Some(ref rt) = opts.reconnect_token {`)
690
- l.push(`\t\tif !query_str.is_empty() { query_str.push('&'); }`)
691
- l.push(`\t\tquery_str.push_str(&format!("reconnect_token={}", urlencoding::encode(rt)));`)
692
- l.push(`\t}`)
693
- l.push(`\tlet full_url = if query_str.is_empty() {`)
694
- l.push(`\t\tformat!("{}{}", ${cfgAccess}.base_url, url_path)`)
695
- l.push(`\t} else {`)
696
- l.push(`\t\tformat!("{}{}?{}", ${cfgAccess}.base_url, url_path, query_str)`)
697
- l.push(`\t};`)
698
- l.push(`\t/* convert http(s) → ws(s) */`)
699
- l.push(`\tlet ws_url = full_url.replacen("https://", "wss://", 1).replacen("http://", "ws://", 1);`)
700
- l.push(`\tuse tokio_tungstenite::tungstenite::client::IntoClientRequest;`)
701
- l.push(`\tlet mut req = ws_url.as_str().into_client_request()`)
702
- l.push(`\t\t.map_err(|e| Error::Other(e.to_string()))?;`)
703
- l.push(`\tif let Some(ref protos) = opts.protocols {`)
704
- l.push(`\t\tif !protos.is_empty() {`)
705
- l.push(`\t\t\treq.headers_mut().insert(`)
706
- l.push(`\t\t\t\t"Sec-WebSocket-Protocol",`)
506
+ /** Lines that build `call_headers` from opts.headers, header params, then `extra` entries. */
507
+ function headerLines(plan: RustOpPlan, extra: string[] = []): string[] {
508
+ const l = [
509
+ `\tlet mut call_headers: std::collections::HashMap<String, String> = opts.headers.clone().unwrap_or_default();`,
510
+ ]
511
+ for (const f of plan.optFields.filter((x) => x.where === "header")) {
512
+ l.push(`\t{`)
513
+ l.push(`\t\tlet mut values: Vec<(String, String)> = Vec::new();`)
514
+ l.push(`\t\tcrate::runtime::push_serialized(${rustString(f.wire)}, &opts.${f.ident}, &mut values);`)
515
+ l.push(`\t\tif !values.is_empty() {`)
707
516
  l.push(
708
- `\t\t\t\tprotos.join(", ").parse().map_err(|e: tokio_tungstenite::tungstenite::http::header::InvalidHeaderValue| Error::Other(e.to_string()))?,`,
517
+ `\t\t\tcrate::runtime::set_header(&mut call_headers, ${rustString(f.wire)}, values.into_iter().map(|(_, v)| v).collect::<Vec<_>>().join(","));`,
709
518
  )
710
- l.push(`\t\t\t);`)
711
519
  l.push(`\t\t}`)
712
520
  l.push(`\t}`)
713
- l.push(`\tlet (stream, _) = tokio_tungstenite::connect_async(req).await`)
714
- l.push(`\t\t.map_err(|e| Error::Other(e.to_string()))?;`)
715
- l.push(`\tOk(TypedWebSocket::new(stream))`)
716
- l.push(`}`)
717
- l.push(``)
718
- return l
719
521
  }
522
+ l.push(...extra)
523
+ return l
524
+ }
720
525
 
721
- /* standard HTTP method */
722
- const opErrEnum = !throwOnError ? getOpErrorEnumName(m.opId, m.op) : null
723
- let retType: string
724
- if (throwOnError) {
725
- retType = m.responseType !== "" ? `Result<${m.responseType}, Error>` : `Result<(), Error>`
726
- } else if (opErrEnum && m.responseType !== "") {
727
- /* typed per-op error: SdkResult<T, OpError> — uses shorthand (no verbose Error default) */
728
- retType = `Result<SdkResult<${m.responseType}, ${opErrEnum}>, Error>`
729
- } else {
730
- /* SdkResult<T> — shorthand, E defaults to Error */
731
- retType = m.responseType !== "" ? `Result<SdkResult<${m.responseType}>, Error>` : `Result<(), Error>`
526
+ function bodyExpr(plan: RustOpPlan, sync: boolean): { lines: string[]; expr: string } {
527
+ const prefix = sync ? "crate::runtime_sync::SyncRequestBody" : "crate::runtime::RequestBody"
528
+ const b = plan.op.body
529
+ const local = plan.bodyParam?.local
530
+ if (!b || !local) return { expr: `${prefix}::None`, lines: [] }
531
+ switch (b.kind) {
532
+ case "json":
533
+ return { expr: `${prefix}::Json(&body_value)`, lines: [`\tlet body_value = serde_json::to_value(${local})?;`] }
534
+ case "form":
535
+ return { expr: `${prefix}::FormUrl(crate::runtime::form_pairs(${local}))`, lines: [] }
536
+ case "multipart":
537
+ return { expr: `${prefix}::Multipart(${local})`, lines: [] }
538
+ case "binary":
539
+ return {
540
+ expr: sync
541
+ ? `${prefix}::Stream(reqwest::blocking::Body::new(${local}))`
542
+ : `${prefix}::Stream(reqwest::Body::wrap_stream(${local}))`,
543
+ lines: [],
544
+ }
545
+ case "text":
546
+ return {
547
+ expr: `${prefix}::Raw(${rustString(b.contentType)}, bytes::Bytes::from(${local}.to_string()))`,
548
+ lines: [],
549
+ }
732
550
  }
551
+ }
733
552
 
734
- params.push(`opts: &${optsName}`)
735
- const asyncGenericStr = asyncGenerics.length > 0 ? `<${asyncGenerics.join(", ")}>` : ""
736
- l.push(`pub async fn ${actionFn}${asyncGenericStr}(${params.join(", ")}) -> ${retType} {`)
737
-
738
- const urlExpr = buildUrlExpr(m.path, m.pathParams)
739
- l.push(`\tlet url_path = ${urlExpr};`)
740
- const queryMut = m.queryParams.length > 0 ? "mut " : ""
741
- l.push(`\tlet ${queryMut}query: Vec<(String, String)> = Vec::new();`)
742
- emitQueryParamLoop(m.queryParams, l)
743
-
744
- /* RequestBody variant based on body kind */
745
- let bodyArg: string
746
- if (bi.kind === "json") {
747
- bodyArg = `crate::runtime::RequestBody::Json(&serde_json::to_value(body).unwrap_or_default())`
748
- } else if (bi.kind === "form-url") {
749
- bodyArg = `crate::runtime::RequestBody::FormUrl(body)`
750
- } else if (bi.kind === "multipart") {
751
- bodyArg = `crate::runtime::RequestBody::Multipart(body)`
752
- } else if (bi.kind === "stream") {
753
- bodyArg = `crate::runtime::RequestBody::Stream(reqwest::Body::wrap_stream(body))`
754
- } else {
755
- bodyArg = `crate::runtime::RequestBody::None`
553
+ function decodeLines(plan: RustOpPlan, throwOnError: boolean): string[] {
554
+ switch (plan.op.success.kind) {
555
+ case "text":
556
+ return [`\tlet out: String = String::from_utf8_lossy(&result.body).into_owned();`]
557
+ case "binary":
558
+ /* the bytes move into the result; only a safe-mode call keeps a second copy in `response` */
559
+ return [`\tlet out: Vec<u8> = ${throwOnError ? "result.body" : "result.body.clone()"};`]
560
+ default:
561
+ /* an empty JSON body decodes as `null` (Option / Value types accept it) */
562
+ return [
563
+ `\tlet out: ${plan.resultType} = serde_json::from_slice(if result.body.is_empty() { b"null" } else { &result.body })?;`,
564
+ ]
756
565
  }
566
+ }
757
567
 
758
- /* invalidation pre-flight — build path-params map, interpolate to concrete
759
- * selector, snapshot RequestMeta from the tracker. Emitted only for ops
760
- * with x-invalidate; BuildRequestMeta short-circuits to None when disabled. */
761
- const hasInvalidate = m.invalidates.length > 0
762
- if (hasInvalidate) {
763
- const pathParamsMapExpr =
764
- m.pathParams.length > 0
765
- ? `std::collections::HashMap::from([${m.pathParams.map((p) => `(${JSON.stringify(p)}.to_string(), ${rustSnake(p)}.to_string())`).join(", ")}])`
766
- : `std::collections::HashMap::<String, String>::new()`
767
- l.push(`\tlet _path_params_map: std::collections::HashMap<String, String> = ${pathParamsMapExpr};`)
768
- l.push(
769
- `\tlet _concrete_path = crate::invalidation::interpolate_path(${JSON.stringify(m.path)}, &_path_params_map).unwrap_or_else(|_| ${JSON.stringify(m.path)}.to_string());`,
770
- )
771
- l.push(`\tlet _concrete_selector = format!("${httpMethod} {}", _concrete_path);`)
772
- l.push(`\tlet _request_meta: Option<crate::invalidation::RequestMeta> = match ${staleAccess} {`)
773
- l.push(
774
- `\t\tSome(ref s) => s.build_request_meta(&_concrete_selector, &_concrete_path, ${JSON.stringify(httpMethod)}).await,`,
775
- )
776
- l.push(`\t\tNone => None,`)
777
- l.push(`\t};`)
778
- }
568
+ type Access = { cfg: string; stale: string; http: string; auth: string }
779
569
 
780
- /* idempotency-key auto-injection: build a local headers map that carries the key.
781
- * opts is &-borrowed, so mutation happens on a clone. Precedence:
782
- * explicit opts.headers["Idempotency-Key"] (case-insensitive) → kept
783
- * opts.idempotency_key → used
784
- * else → uuid::Uuid::new_v4()
785
- * Matches TS/Python/Go emitter semantics (9.a.2 / 9.a.3 / 9.a.4). */
786
- const emitIdem = m.isIdempotent && !m.isSSE && !m.isWS && !m.isRealtime
787
- if (emitIdem) {
788
- l.push(
789
- `\tlet mut _call_headers: std::collections::HashMap<String, String> = opts.headers.clone().unwrap_or_default();`,
790
- )
791
- l.push(`\tlet _has_idem = _call_headers.keys().any(|k| k.eq_ignore_ascii_case("Idempotency-Key"));`)
792
- l.push(`\tif !_has_idem {`)
793
- l.push(`\t\tlet _k = opts.idempotency_key.clone().unwrap_or_else(|| uuid::Uuid::new_v4().to_string());`)
794
- l.push(`\t\t_call_headers.insert("Idempotency-Key".to_string(), _k);`)
795
- l.push(`\t}`)
796
- }
570
+ function access(selfAccess: "self" | "self.client"): Access {
571
+ return selfAccess === "self"
572
+ ? { auth: "self.inner.auth", cfg: "self.inner.cfg", http: "self.inner.http_client", stale: "self.inner.stale" }
573
+ : { auth: "self.client.auth", cfg: "self.client.cfg", http: "self.client.http_client", stale: "self.client.stale" }
574
+ }
797
575
 
798
- const operationStr = JSON.stringify(`${httpMethod} ${m.path}`)
799
- const requestMetaArg = hasInvalidate ? `_request_meta.as_ref()` : `None`
800
- const headersArg = emitIdem ? `Some(&_call_headers)` : `opts.headers.as_ref()`
801
- l.push(`\tlet result = crate::runtime::do_request(`)
802
- l.push(`\t\t&${httpClientAccess},`)
803
- l.push(`\t\t&${cfgAccess},`)
804
- l.push(`\t\treqwest::Method::${httpMethod},`)
805
- l.push(`\t\t&url_path,`)
806
- l.push(`\t\t&query,`)
807
- l.push(`\t\t${bodyArg},`)
808
- l.push(`\t\t${headersArg},`)
809
- l.push(`\t\topts.timeout,`)
810
- l.push(`\t\topts.cancel_token.clone(),`)
811
- l.push(`\t\t${operationStr},`)
812
- l.push(`\t\t${requestMetaArg},`)
813
- l.push(`\t).await?;`)
814
-
815
- /* invalidation post-flight — expand templated x-invalidate targets with the
816
- * call's concrete params (TS parity: GET /users/{id} → GET /users/abc),
817
- * mark them stale, then clear keys that were already stale before this
818
- * mutation completed (seq-bounded to avoid clobbering newer invalidations). */
819
- if (hasInvalidate) {
820
- l.push(`\tif let Some(ref stale) = ${staleAccess} {`)
821
- l.push(`\t\tif let Some(resource_map) = crate::client::INVALIDATION_MAP.get(${JSON.stringify(m.resource)}) {`)
822
- l.push(`\t\t\tif let Some(entry) = resource_map.get(${JSON.stringify(m.action)}) {`)
823
- l.push(`\t\t\t\tlet targets: Vec<String> = entry.invalidate.iter().map(|s| s.to_string()).collect();`)
824
- l.push(`\t\t\t\tstale.mark_stale(&targets, &_path_params_map, &_concrete_selector).await;`)
825
- l.push(`\t\t\t}`)
576
+ function returnType(plan: RustOpPlan, throwOnError: boolean): string {
577
+ if (plan.resultType === "") return "Result<(), Error>"
578
+ if (throwOnError) return `Result<${plan.resultType}, Error>`
579
+ if (plan.errorEnum) return `Result<SdkResult<${plan.resultType}, ${plan.errorEnum}>, Error>`
580
+ return `Result<SdkResult<${plan.resultType}>, Error>`
581
+ }
582
+
583
+ /** Shared tail: run the request (already bound to `result`), invalidate, decode. */
584
+ function resultLines(plan: RustOpPlan, throwOnError: boolean, sync: boolean, a: Access, call: string[]): string[] {
585
+ const l: string[] = []
586
+ const aw = sync ? "" : ".await"
587
+ const method = plan.op.method
588
+
589
+ /* invalidation pre-flight for every call: reads see and clear staleness too */
590
+ const pp = plan.pathParams.map((p) => `(${rustString(p.wire)}.to_string(), ${p.local}.to_string())`).join(", ")
591
+ l.push(
592
+ `\tlet path_params: std::collections::HashMap<String, String> = ${plan.pathParams.length > 0 ? `std::collections::HashMap::from([${pp}])` : "std::collections::HashMap::new()"};`,
593
+ )
594
+ l.push(`\tlet selector = format!("{} {}", ${rustString(method)}, url_path);`)
595
+ l.push(`\tlet request_meta: Option<crate::invalidation::RequestMeta> = match ${a.stale} {`)
596
+ l.push(`\t\tSome(ref s) => s.build_request_meta(&selector, &url_path, ${rustString(method)})${aw},`)
597
+ l.push(`\t\tNone => None,`)
598
+ l.push(`\t};`)
599
+
600
+ const safe = !throwOnError && plan.resultType !== ""
601
+ if (safe) {
602
+ l.push(`\tlet result = match ${call.join("\n\t\t")} {`)
603
+ l.push(`\t\tOk(r) => r,`)
604
+ l.push(`\t\tErr(Error::Api(api)) => {`)
605
+ l.push(`\t\t\tlet status = api.status();`)
606
+ if (plan.errorEnum) {
607
+ l.push(`\t\t\tlet body = api.body().to_vec();`)
608
+ l.push(`\t\t\tlet error = match status {`)
609
+ for (const env of plan.envelopes) {
610
+ l.push(`\t\t\t\t${env.status} => match serde_json::from_slice::<${env.structName}>(&body) {`)
611
+ l.push(`\t\t\t\t\tOk(v) => ${plan.errorEnum}::${env.variantName}(v),`)
612
+ l.push(`\t\t\t\t\tErr(_) => ${plan.errorEnum}::Other(Error::Api(api)),`)
613
+ l.push(`\t\t\t\t},`)
614
+ }
615
+ l.push(`\t\t\t\t_ => ${plan.errorEnum}::Other(Error::Api(api)),`)
616
+ l.push(`\t\t\t};`)
617
+ l.push(`\t\t\tlet mut response = crate::runtime::ResponseMeta::empty(status);`)
618
+ l.push(`\t\t\tresponse.body = body;`)
619
+ l.push(`\t\t\treturn Ok(SdkResult { data: None, error: Some(error), status, response });`)
620
+ } else {
621
+ l.push(`\t\t\tlet mut response = crate::runtime::ResponseMeta::empty(status);`)
622
+ l.push(`\t\t\tresponse.body = api.body().to_vec();`)
623
+ l.push(`\t\t\treturn Ok(SdkResult { data: None, error: Some(Error::Api(api)), status, response });`)
624
+ }
826
625
  l.push(`\t\t}`)
827
- l.push(`\t\tif let Some(ref meta) = _request_meta {`)
828
- l.push(`\t\t\tif meta.is_stale {`)
626
+ l.push(`\t\tErr(e) => return Err(e),`)
627
+ l.push(`\t};`)
628
+ } else {
629
+ l.push(`\tlet result = ${call.join("\n\t\t")}?;`)
630
+ }
631
+
632
+ /* invalidation post-flight: mark declared targets stale (templated ones expanded with this
633
+ * call's params), then clear this key if the call read through a stale window */
634
+ l.push(`\tif let Some(ref stale) = ${a.stale} {`)
635
+ if (plan.op.invalidates.length > 0) {
829
636
  l.push(
830
- `\t\t\t\tstale.clear_stale(&_concrete_selector, &_concrete_path, ${JSON.stringify(httpMethod)}, meta.seq_snapshot).await;`,
637
+ `\t\tlet targets: Vec<String> = vec![${plan.op.invalidates.map((t) => `${rustString(t)}.to_string()`).join(", ")}];`,
831
638
  )
832
- l.push(`\t\t\t}`)
833
- l.push(`\t\t}`)
834
- l.push(`\t}`)
639
+ l.push(`\t\tstale.mark_stale(&targets, &path_params, &selector)${aw};`)
835
640
  }
641
+ l.push(`\t\tif let Some(ref meta) = request_meta {`)
642
+ l.push(`\t\t\tif meta.is_stale {`)
643
+ l.push(`\t\t\t\tstale.clear_stale(&selector, &url_path, ${rustString(method)}, meta.seq_snapshot)${aw};`)
644
+ l.push(`\t\t\t}`)
645
+ l.push(`\t\t}`)
646
+ l.push(`\t}`)
836
647
 
837
- if (m.responseType === "") {
648
+ if (plan.resultType === "") {
649
+ l.push(`\tlet _ = result;`)
838
650
  l.push(`\tOk(())`)
839
- l.push(`}`)
840
- l.push(``)
841
651
  return l
842
652
  }
843
-
653
+ l.push(...decodeLines(plan, throwOnError))
844
654
  if (throwOnError) {
845
- l.push(`\tlet out: ${m.responseType} = serde_json::from_slice(&result.body)?;`)
846
655
  l.push(`\tOk(out)`)
847
656
  } else {
848
- l.push(`\tlet out: ${m.responseType} = serde_json::from_slice(&result.body)?;`)
657
+ /* the response snapshot takes the body, headers and URL by move */
849
658
  l.push(
850
- `\tOk(SdkResult { data: Some(out), error: None, status: result.status, response: crate::runtime::ResponseMeta { status: result.status, headers: result.headers.clone(), url: result.url.clone(), body: result.body.clone() } })`,
659
+ `\tOk(SdkResult { data: Some(out), error: None, status: result.status, response: crate::runtime::ResponseMeta { status: result.status, headers: result.headers, url: result.url, body: result.body } })`,
851
660
  )
852
661
  }
853
-
854
- l.push(`}`)
855
- l.push(``)
856
662
  return l
857
663
  }
858
664
 
859
- /** emitSyncMethod is the blocking twin of emitMethod — no async, calls do_request_blocking. */
860
- function emitSyncMethod(m: MethodInfo, selfAccess: "self" | "self.client", throwOnError: boolean): string[] {
861
- /* SSE and WS operations have no sync variant */
862
- if (m.isSSE || m.isWS) return []
863
-
864
- const l: string[] = []
865
- const actionFn = rustMethodIdent(m.action === "_call" ? m.resource : m.methodAction)
866
- const optsName = optsStructName(m)
867
- const httpMethod = m.httpMethod
868
-
869
- if (m.summary) {
870
- l.push(`/// ${m.summary}`)
871
- if (m.description) l.push(`/// ${m.description}`)
872
- }
873
-
665
+ function signature(plan: RustOpPlan, sync: boolean): { generics: string; params: string; where: string } {
874
666
  const params: string[] = [`&self`]
875
- for (const pp of m.pathParams) {
876
- params.push(`${rustSnake(pp)}: &str`)
877
- }
878
- /* sync generics: stream body is generic over `R: Read + Send + 'static`
879
- * (reqwest::blocking::Body::new bound). */
880
- const syncGenerics: string[] = []
881
- const bi = m.bodyInfo
882
- if (bi.kind === "json") {
883
- params.push(`body: &${bi.type}`)
884
- } else if (bi.kind === "form-url") {
885
- params.push(`body: &std::collections::HashMap<String, String>`)
886
- } else if (bi.kind === "multipart") {
887
- params.push(`body: reqwest::blocking::multipart::Form`)
888
- } else if (bi.kind === "stream") {
889
- syncGenerics.push(`R: std::io::Read + Send + 'static`)
890
- params.push(`body: R`)
891
- }
892
-
893
- const { cfg: cfgAccess, stale: staleAccess, httpClient: httpClientAccess } = buildAccessors(selfAccess)
894
-
895
- const opErrEnum = !throwOnError ? getOpErrorEnumName(m.opId, m.op) : null
896
- let retType: string
897
- if (throwOnError) {
898
- retType = m.responseType !== "" ? `Result<${m.responseType}, Error>` : `Result<(), Error>`
899
- } else if (opErrEnum && m.responseType !== "") {
900
- retType = `Result<SdkResult<${m.responseType}, ${opErrEnum}>, Error>`
901
- } else {
902
- retType = m.responseType !== "" ? `Result<SdkResult<${m.responseType}>, Error>` : `Result<(), Error>`
667
+ for (const p of plan.pathParams) params.push(`${p.local}: &str`)
668
+ let generics = ""
669
+ let where = ""
670
+ if (plan.bodyParam) {
671
+ params.push(`${plan.bodyParam.local}: ${sync ? plan.bodyParam.syncType : plan.bodyParam.asyncType}`)
672
+ if (plan.bodyParam.generic) {
673
+ generics = `<${sync ? plan.bodyParam.generic.sync : plan.bodyParam.generic.async}>`
674
+ if (!sync) where = plan.bodyParam.generic.asyncWhere ?? ""
675
+ }
903
676
  }
677
+ params.push(`opts: &${plan.optsName}`)
678
+ return { generics, params: params.join(", "), where }
679
+ }
904
680
 
905
- params.push(`opts: &${optsName}`)
906
- const syncGenericStr = syncGenerics.length > 0 ? `<${syncGenerics.join(", ")}>` : ""
907
- l.push(`pub fn ${actionFn}${syncGenericStr}(${params.join(", ")}) -> ${retType} {`)
908
-
909
- const urlExpr = buildUrlExpr(m.path, m.pathParams)
910
- l.push(`\tlet url_path = ${urlExpr};`)
911
- const queryMut = m.queryParams.length > 0 ? "mut " : ""
912
- l.push(`\tlet ${queryMut}query: Vec<(String, String)> = Vec::new();`)
913
- emitQueryParamLoop(m.queryParams, l)
914
-
915
- let bodyArg: string
916
- if (bi.kind === "json") {
917
- bodyArg = `crate::runtime_sync::SyncRequestBody::Json(&serde_json::to_value(body).unwrap_or_default())`
918
- } else if (bi.kind === "form-url") {
919
- bodyArg = `crate::runtime_sync::SyncRequestBody::FormUrl(body)`
920
- } else if (bi.kind === "multipart") {
921
- bodyArg = `crate::runtime_sync::SyncRequestBody::Multipart(body)`
922
- } else if (bi.kind === "stream") {
923
- bodyArg = `crate::runtime_sync::SyncRequestBody::Stream(reqwest::blocking::Body::new(body))`
924
- } else {
925
- bodyArg = `crate::runtime_sync::SyncRequestBody::None`
926
- }
681
+ function emitMethod(plan: RustOpPlan, selfAccess: "self" | "self.client", throwOnError: boolean): string[] {
682
+ const l: string[] = []
683
+ const { op } = plan
684
+ const a = access(selfAccess)
685
+ l.push(...docLines(op))
686
+ const sig = signature(plan, false)
927
687
 
928
- /* invalidation pre-flight (sync) — same shape as async, no .await. */
929
- const hasInvalidate = m.invalidates.length > 0
930
- if (hasInvalidate) {
931
- const pathParamsMapExpr =
932
- m.pathParams.length > 0
933
- ? `std::collections::HashMap::from([${m.pathParams.map((p) => `(${JSON.stringify(p)}.to_string(), ${rustSnake(p)}.to_string())`).join(", ")}])`
934
- : `std::collections::HashMap::<String, String>::new()`
935
- l.push(`\tlet _path_params_map: std::collections::HashMap<String, String> = ${pathParamsMapExpr};`)
688
+ if (op.stream === "realtime") {
936
689
  l.push(
937
- `\tlet _concrete_path = crate::invalidation::interpolate_path(${JSON.stringify(m.path)}, &_path_params_map).unwrap_or_else(|_| ${JSON.stringify(m.path)}.to_string());`,
690
+ `pub async fn ${plan.fnName}(${sig.params}) -> Result<crate::realtime::ResumableConnection<serde_json::Value, serde_json::Value>, crate::realtime::RealtimeError> {`,
938
691
  )
939
- l.push(`\tlet _concrete_selector = format!("${httpMethod} {}", _concrete_path);`)
940
- l.push(`\tlet _request_meta: Option<crate::invalidation::RequestMeta> = match ${staleAccess} {`)
692
+ l.push(`\tlet connect_err = |e: Error| crate::realtime::RealtimeError::Connect(e);`)
693
+ l.push(...pathLines(plan).map((x) => x.replace(/\?;$/, ".map_err(connect_err)?;")))
694
+ l.push(`\tlet url = crate::runtime::build_url(&${a.cfg}.base_url, &url_path, &[]).map_err(connect_err)?;`)
695
+ l.push(`\tlet headers = crate::runtime::auth_headers(&${a.cfg}, &${a.auth}, opts.headers.as_ref()).await;`)
696
+ l.push(`\tlet rt_opts = crate::realtime::ResumableConnectionOpts {`)
697
+ l.push(`\t\treconnect_token: opts.reconnect_token.clone(),`)
698
+ l.push(`\t\tprotocols: opts.protocols.clone(),`)
699
+ l.push(`\t\tmax_reconnect_attempts: opts.max_reconnect_attempts,`)
700
+ l.push(`\t\treconnect_delay_ms: opts.reconnect_delay_ms,`)
701
+ l.push(`\t\tlast_event_id: opts.last_event_id.clone(),`)
702
+ l.push(`\t\theaders,`)
703
+ l.push(`\t\thttp_client: Some(${a.http}.clone()),`)
704
+ l.push(`\t};`)
941
705
  l.push(
942
- `\t\tSome(ref s) => s.build_request_meta(&_concrete_selector, &_concrete_path, ${JSON.stringify(httpMethod)}),`,
706
+ `\tcrate::realtime::ResumableConnection::<serde_json::Value, serde_json::Value>::connect_with_defaults(url.to_string(), rt_opts).await`,
943
707
  )
944
- l.push(`\t\tNone => None,`)
945
- l.push(`\t};`)
708
+ l.push(`}`)
709
+ l.push(``)
710
+ return l
946
711
  }
947
712
 
948
- /* idempotency-key auto-injection (sync mirror of async path — same semantics). */
949
- const emitIdem = m.isIdempotent && !m.isSSE && !m.isWS && !m.isRealtime
950
- if (emitIdem) {
713
+ if (op.stream === "sse") {
951
714
  l.push(
952
- `\tlet mut _call_headers: std::collections::HashMap<String, String> = opts.headers.clone().unwrap_or_default();`,
715
+ `pub fn ${plan.fnName}${sig.generics}(${sig.params}) -> impl Stream<Item = Result<SseEvent, Error>>${sig.where} {`,
953
716
  )
954
- l.push(`\tlet _has_idem = _call_headers.keys().any(|k| k.eq_ignore_ascii_case("Idempotency-Key"));`)
955
- l.push(`\tif !_has_idem {`)
956
- l.push(`\t\tlet _k = opts.idempotency_key.clone().unwrap_or_else(|| uuid::Uuid::new_v4().to_string());`)
957
- l.push(`\t\t_call_headers.insert("Idempotency-Key".to_string(), _k);`)
958
- l.push(`\t}`)
959
- }
960
-
961
- const operationStr = JSON.stringify(`${httpMethod} ${m.path}`)
962
- const requestMetaArg = hasInvalidate ? `_request_meta.as_ref()` : `None`
963
- const headersArg = emitIdem ? `Some(&_call_headers)` : `opts.headers.as_ref()`
964
- l.push(`\tlet result = crate::runtime_sync::do_request_blocking(`)
965
- l.push(`\t\t&${httpClientAccess},`)
966
- l.push(`\t\t&${cfgAccess},`)
967
- l.push(`\t\treqwest::Method::${httpMethod},`)
968
- l.push(`\t\t&url_path,`)
969
- l.push(`\t\t&query,`)
970
- l.push(`\t\t${bodyArg},`)
971
- l.push(`\t\t${headersArg},`)
972
- l.push(`\t\topts.timeout,`)
973
- l.push(`\t\topts.sync_cancel_token.clone(),`)
974
- l.push(`\t\t${operationStr},`)
975
- l.push(`\t\t${requestMetaArg},`)
976
- l.push(`\t)?;`)
977
-
978
- if (hasInvalidate) {
979
- l.push(`\tif let Some(ref stale) = ${staleAccess} {`)
980
- l.push(`\t\tif let Some(resource_map) = crate::client::INVALIDATION_MAP.get(${JSON.stringify(m.resource)}) {`)
981
- l.push(`\t\t\tif let Some(entry) = resource_map.get(${JSON.stringify(m.action)}) {`)
982
- l.push(`\t\t\t\tlet targets: Vec<String> = entry.invalidate.iter().map(|s| s.to_string()).collect();`)
983
- l.push(`\t\t\t\tstale.mark_stale(&targets, &_path_params_map, &_concrete_selector);`)
984
- l.push(`\t\t\t}`)
985
- l.push(`\t\t}`)
986
- l.push(`\t\tif let Some(ref meta) = _request_meta {`)
987
- l.push(`\t\t\tif meta.is_stale {`)
717
+ /* clone everything the stream needs into owned locals: the stream outlives &self/&opts */
718
+ if (plan.pathParams.length === 0) {
719
+ l.push(`\tlet url_path: Result<String, Error> = Ok(${rustString(op.path)}.to_string());`)
720
+ } else {
721
+ const params = plan.pathParams.map((p) => `(${rustString(p.wire)}, ${p.local})`).join(", ")
722
+ l.push(`\tlet url_path = crate::runtime::expand_path(${rustString(op.path)}, &[${params}]);`)
723
+ }
724
+ l.push(...queryLines(plan))
725
+ l.push(
726
+ ...headerLines(plan, [
727
+ `\tcrate::runtime::set_header(&mut call_headers, "Accept", "text/event-stream".to_string());`,
728
+ `\tif let Some(ref lei) = opts.last_event_id {`,
729
+ `\t\tcrate::runtime::set_header(&mut call_headers, "Last-Event-ID", lei.clone());`,
730
+ `\t}`,
731
+ ]),
732
+ )
733
+ const body = op.body && plan.bodyParam?.local ? op.body : undefined
734
+ if (body?.kind === "json") l.push(`\tlet body_value = serde_json::to_value(${plan.bodyParam?.local});`)
735
+ l.push(`\tlet cfg = ${a.cfg}.clone();`)
736
+ l.push(`\tlet client = ${a.http}.clone();`)
737
+ l.push(`\tlet auth = std::sync::Arc::clone(&${a.auth});`)
738
+ if (body?.kind === "form") l.push(`\tlet form = crate::runtime::form_pairs(${plan.bodyParam?.local});`)
739
+ if (body?.kind === "text") l.push(`\tlet text = bytes::Bytes::from(${plan.bodyParam?.local}.to_string());`)
740
+ if (body?.kind === "multipart" || body?.kind === "binary") l.push(`\tlet raw_body = ${plan.bodyParam?.local};`)
741
+ l.push(`\tasync_stream::stream! {`)
742
+ l.push(`\t\tuse futures_util::StreamExt;`)
743
+ l.push(`\t\tlet url_path = match url_path {`)
744
+ l.push(`\t\t\tOk(p) => p,`)
745
+ l.push(`\t\t\tErr(e) => { yield Err(e); return; }`)
746
+ l.push(`\t\t};`)
747
+ let expr = "crate::runtime::RequestBody::None"
748
+ if (body?.kind === "json") {
749
+ l.push(`\t\tlet body_value = match body_value {`)
750
+ l.push(`\t\t\tOk(v) => v,`)
751
+ l.push(`\t\t\tErr(e) => { yield Err(Error::Json(e)); return; }`)
752
+ l.push(`\t\t};`)
753
+ expr = "crate::runtime::RequestBody::Json(&body_value)"
754
+ } else if (body?.kind === "form") expr = "crate::runtime::RequestBody::FormUrl(form)"
755
+ else if (body?.kind === "text") expr = `crate::runtime::RequestBody::Raw(${rustString(body.contentType)}, text)`
756
+ else if (body?.kind === "multipart") expr = "crate::runtime::RequestBody::Multipart(raw_body)"
757
+ else if (body?.kind === "binary") expr = "crate::runtime::RequestBody::Stream(reqwest::Body::wrap_stream(raw_body))"
988
758
  l.push(
989
- `\t\t\t\tstale.clear_stale(&_concrete_selector, &_concrete_path, ${JSON.stringify(httpMethod)}, meta.seq_snapshot);`,
759
+ `\t\tlet opened = crate::runtime::open_stream(&client, &cfg, &auth, reqwest::Method::${op.method}, &url_path, &query, ${expr}, Some(&call_headers)).await;`,
990
760
  )
761
+ l.push(`\t\tmatch opened {`)
762
+ l.push(`\t\t\tErr(e) => { yield Err(e); }`)
763
+ l.push(`\t\t\tOk(r) => {`)
764
+ l.push(`\t\t\t\tlet mut s = std::pin::pin!(crate::sse::parse_sse_stream(r));`)
765
+ l.push(`\t\t\t\twhile let Some(item) = s.next().await { yield item; }`)
991
766
  l.push(`\t\t\t}`)
992
767
  l.push(`\t\t}`)
993
768
  l.push(`\t}`)
994
- }
995
-
996
- if (m.responseType === "") {
997
- l.push(`\tOk(())`)
998
769
  l.push(`}`)
999
770
  l.push(``)
1000
771
  return l
1001
772
  }
1002
773
 
1003
- if (throwOnError) {
1004
- l.push(`\tlet out: ${m.responseType} = serde_json::from_slice(&result.body)?;`)
1005
- l.push(`\tOk(out)`)
1006
- } else {
1007
- l.push(`\tlet out: ${m.responseType} = serde_json::from_slice(&result.body)?;`)
774
+ if (op.stream === "ws") {
775
+ l.push(`pub async fn ${plan.fnName}(${sig.params}) -> Result<TypedWebSocket, Error> {`)
776
+ l.push(...pathLines(plan))
777
+ l.push(...queryLines(plan))
778
+ l.push(`\tif let Some(ref rt) = opts.reconnect_token {`)
779
+ l.push(`\t\tquery.push(("reconnect_token".to_string(), rt.clone()));`)
780
+ l.push(`\t}`)
781
+ l.push(`\tlet url = crate::runtime::build_url(&${a.cfg}.base_url, &url_path, &query)?;`)
782
+ l.push(`\tlet ws_url = crate::runtime::to_ws_url(url.as_str());`)
783
+ l.push(`\tuse tokio_tungstenite::tungstenite::client::IntoClientRequest;`)
784
+ l.push(`\tlet mut req = ws_url.as_str().into_client_request().map_err(|e| Error::Other(e.to_string()))?;`)
785
+ l.push(`\tfor (k, v) in crate::runtime::auth_headers(&${a.cfg}, &${a.auth}, None).await {`)
786
+ l.push(`\t\tif let (Ok(name), Ok(val)) = (`)
787
+ l.push(`\t\t\ttokio_tungstenite::tungstenite::http::HeaderName::from_bytes(k.as_bytes()),`)
788
+ l.push(`\t\t\tv.parse::<tokio_tungstenite::tungstenite::http::HeaderValue>(),`)
789
+ l.push(`\t\t) {`)
790
+ l.push(`\t\t\treq.headers_mut().insert(name, val);`)
791
+ l.push(`\t\t}`)
792
+ l.push(`\t}`)
793
+ l.push(`\tif let Some(ref protos) = opts.protocols {`)
794
+ l.push(`\t\tif !protos.is_empty() {`)
795
+ l.push(`\t\t\treq.headers_mut().insert(`)
796
+ l.push(`\t\t\t\t"Sec-WebSocket-Protocol",`)
1008
797
  l.push(
1009
- `\tOk(SdkResult { data: Some(out), error: None, status: result.status, response: crate::runtime::ResponseMeta { status: result.status, headers: result.headers.clone(), url: result.url.clone(), body: result.body.clone() } })`,
798
+ `\t\t\t\tprotos.join(", ").parse().map_err(|e: tokio_tungstenite::tungstenite::http::header::InvalidHeaderValue| Error::Other(e.to_string()))?,`,
1010
799
  )
800
+ l.push(`\t\t\t);`)
801
+ l.push(`\t\t}`)
802
+ l.push(`\t}`)
803
+ l.push(`\tlet (stream, _) = tokio_tungstenite::connect_async(req).await.map_err(|e| Error::Other(e.to_string()))?;`)
804
+ l.push(`\tOk(TypedWebSocket::new(stream))`)
805
+ l.push(`}`)
806
+ l.push(``)
807
+ return l
1011
808
  }
1012
809
 
810
+ l.push(`pub async fn ${plan.fnName}${sig.generics}(${sig.params}) -> ${returnType(plan, throwOnError)}${sig.where} {`)
811
+ l.push(...pathLines(plan))
812
+ l.push(...queryLines(plan))
813
+ l.push(...headerLines(plan, idempotencyLines(plan)))
814
+ const body = bodyExpr(plan, false)
815
+ l.push(...body.lines)
816
+ const call = [
817
+ `crate::runtime::do_request(`,
818
+ `\t&${a.http},`,
819
+ `\t&${a.cfg},`,
820
+ `\t&${a.auth},`,
821
+ `\treqwest::Method::${op.method},`,
822
+ `\t&url_path,`,
823
+ `\t&query,`,
824
+ `\t${body.expr},`,
825
+ `\tSome(&call_headers),`,
826
+ `\topts.timeout,`,
827
+ `\topts.cancel_token.clone(),`,
828
+ `\t${rustString(`${op.method} ${op.path}`)},`,
829
+ `\trequest_meta.as_ref(),`,
830
+ `).await`,
831
+ ]
832
+ l.push(...resultLines(plan, throwOnError, false, a, call))
1013
833
  l.push(`}`)
1014
834
  l.push(``)
1015
835
  return l
1016
836
  }
1017
837
 
1018
- /** Builds a Rust expression that produces an owned `String` from a query-param value place.
1019
- * `valuePlace` is the place expression — either `opts.<field>` (required, owned T) or
1020
- * `v` from `if let Some(ref v) = opts.<field>` (optional, &T). Both accept the same method
1021
- * call syntax thanks to Rust's auto-deref on method dispatch.
1022
- * - String → `.clone()` (owned copy)
1023
- * - i64 / f64 / bool → `.to_string()` via `Display`
1024
- * - Enums + complex types → `serde_json::to_string`; strip surrounding quotes so plain-string
1025
- * enum serialisations land as `value` not `"value"` in the URL.
1026
- */
1027
- function rustQueryValueToString(schema: Record<string, unknown>, valuePlace: string): string {
1028
- if (Array.isArray(schema.enum)) {
1029
- return `serde_json::to_string(&${valuePlace}).map(|s| s.trim_matches('"').to_string()).unwrap_or_default()`
1030
- }
1031
- const t = schema.type
1032
- if (t === "string") return `${valuePlace}.clone()`
1033
- if (t === "integer" || t === "number" || t === "boolean") return `${valuePlace}.to_string()`
1034
- return `serde_json::to_string(&${valuePlace}).unwrap_or_default()`
838
+ function idempotencyLines(plan: RustOpPlan): string[] {
839
+ if (!plan.op.idempotent || plan.op.stream !== null) return []
840
+ /* explicit header (any casing) > opts.idempotency_key > fresh UUID */
841
+ return [
842
+ `\tif !call_headers.keys().any(|k| k.eq_ignore_ascii_case("Idempotency-Key")) {`,
843
+ `\t\tlet key = opts.idempotency_key.clone().unwrap_or_else(|| uuid::Uuid::new_v4().to_string());`,
844
+ `\t\tcall_headers.insert("Idempotency-Key".to_string(), key);`,
845
+ `\t}`,
846
+ ]
1035
847
  }
1036
848
 
1037
- /** Emits query param push lines for a standard REST method (not SSE/WS). */
1038
- function emitQueryParamLoop(qps: QueryParam[], l: string[]): void {
1039
- for (const qp of qps) {
1040
- const fieldIdent = rustIdent(qp.name)
1041
- if (qp.required) {
1042
- const expr = rustQueryValueToString(qp.schema, `opts.${fieldIdent}`)
1043
- l.push(`\tquery.push((${JSON.stringify(qp.name)}.to_string(), ${expr}));`)
1044
- } else {
1045
- const expr = rustQueryValueToString(qp.schema, `v`)
1046
- l.push(
1047
- `\tif let Some(ref v) = opts.${fieldIdent} { query.push((${JSON.stringify(qp.name)}.to_string(), ${expr})); }`,
1048
- )
1049
- }
1050
- }
849
+ /** emitSyncMethod is the blocking twin of emitMethod — no async, calls do_request_blocking. */
850
+ function emitSyncMethod(plan: RustOpPlan, selfAccess: "self" | "self.client", throwOnError: boolean): string[] {
851
+ if (!isSyncCapable(plan.op)) return []
852
+ const l: string[] = []
853
+ const { op } = plan
854
+ const a = access(selfAccess)
855
+ l.push(...docLines(op))
856
+ const sig = signature(plan, true)
857
+ l.push(`pub fn ${plan.fnName}${sig.generics}(${sig.params}) -> ${returnType(plan, throwOnError)} {`)
858
+ l.push(...pathLines(plan))
859
+ l.push(...queryLines(plan))
860
+ l.push(...headerLines(plan, idempotencyLines(plan)))
861
+ const body = bodyExpr(plan, true)
862
+ l.push(...body.lines)
863
+ const call = [
864
+ `crate::runtime_sync::do_request_blocking(`,
865
+ `\t&${a.http},`,
866
+ `\t&${a.cfg},`,
867
+ `\t&${a.auth},`,
868
+ `\treqwest::Method::${op.method},`,
869
+ `\t&url_path,`,
870
+ `\t&query,`,
871
+ `\t${body.expr},`,
872
+ `\tSome(&call_headers),`,
873
+ `\topts.timeout,`,
874
+ `\topts.sync_cancel_token.clone(),`,
875
+ `\t${rustString(`${op.method} ${op.path}`)},`,
876
+ `\trequest_meta.as_ref(),`,
877
+ `)`,
878
+ ]
879
+ l.push(...resultLines(plan, throwOnError, true, a, call))
880
+ l.push(`}`)
881
+ l.push(``)
882
+ return l
1051
883
  }
1052
884
 
1053
- function buildUrlExpr(path: string, pathParams: string[]): string {
1054
- /* always returns a String (not &str) so &url_path is &String, not &&str */
1055
- if (pathParams.length === 0) return `${JSON.stringify(path)}.to_string()`
1056
- let fmtStr = path
1057
- const args: string[] = []
1058
- for (const p of pathParams) {
1059
- fmtStr = fmtStr.replace(`{${p}}`, `{}`)
1060
- args.push(`urlencoding::encode(${rustSnake(p)}).into_owned()`)
1061
- }
1062
- return `format!(${JSON.stringify(fmtStr)}, ${args.join(", ")})`
885
+ function isSyncCapable(op: SdkOp): boolean {
886
+ return op.stream === null
1063
887
  }
1064
888
 
1065
- /* Schema names defined in Rust runtime files — skip to avoid redeclaration. */
1066
- const RUST_RUNTIME_TYPES = new Set([
1067
- "ClientConfig",
1068
- "Error",
1069
- "InvalidationConfig",
1070
- "LogEntry",
1071
- "OnAuthExpiredHook",
1072
- "OnLogHook",
1073
- "OnRequestHook",
1074
- "OnResponseHook",
1075
- "RequestBody",
1076
- "RequestContext",
1077
- "RequestMeta",
1078
- "ResponseContext",
1079
- "ResponseMeta",
1080
- "SdkResult",
1081
- "SseEvent",
1082
- "StaleEntry",
1083
- "StaleTracker",
1084
- "StaleTrackerSync",
1085
- "StatusError",
1086
- "SyncClientConfig",
1087
- "SyncRequestBody",
1088
- "SyncRequestContext",
1089
- "SyncResponseContext",
1090
- "TypedWebSocket",
1091
- ])
889
+ /* ── resource tree ── */
1092
890
 
1093
- function buildRustTypes(spec: OpenApiSpec): string {
1094
- const schemas = spec.components?.schemas ?? {}
1095
- const l: string[] = []
1096
- l.push(`/* Code generated by honey. DO NOT EDIT. */`)
1097
- l.push(``)
1098
- l.push(`#![allow(unused_imports)]`)
1099
- l.push(`use serde::{Serialize, Deserialize};`)
1100
- l.push(`use serde_repr::{Serialize_repr, Deserialize_repr};`)
1101
- l.push(`use std::collections::HashMap;`)
1102
- l.push(``)
891
+ type RustNode = {
892
+ path: string[]
893
+ modName: string
894
+ structName: string
895
+ syncStructName: string
896
+ ns: IRNamespace
897
+ children: Array<{ accessor: string; node: RustNode }>
898
+ plans: RustOpPlan[]
899
+ }
1103
900
 
1104
- const sortedSchemas = Object.entries(schemas)
1105
- .filter(([name]) => !RUST_RUNTIME_TYPES.has(name))
1106
- .sort(([a], [b]) => a.localeCompare(b))
1107
- const body: string[] = []
1108
- const decls = new Map<string, string>()
901
+ function hasSync(node: RustNode): boolean {
902
+ return node.plans.some((p) => isSyncCapable(p.op)) || node.children.some((c) => hasSync(c.node))
903
+ }
904
+
905
+ function buildTree(model: SdkModel, names: RustNames, throwOnError: boolean): { root: RustNode; plans: RustOpPlan[] } {
906
+ const plans: RustOpPlan[] = []
907
+ function visit(ns: IRNamespace, path: string[], modName: string, reserved: string[]): RustNode {
908
+ const isRoot = path.length === 0
909
+ const pascal = path.map(rustTypeIdent).join("")
910
+ const node: RustNode = {
911
+ children: [],
912
+ modName,
913
+ ns,
914
+ path,
915
+ plans: [],
916
+ structName: isRoot ? "Client" : names.scope.claim(`${pascal}Resource`),
917
+ syncStructName: isRoot ? "SyncClient" : names.scope.claim(`${pascal}ResourceSync`),
918
+ }
919
+ const members = new NameScope(reserved)
920
+ const modules = new NameScope()
921
+ const childInfo: Array<{ accessor: string; seg: string; ns: IRNamespace; mod: string }> = []
922
+ for (const [seg, childNs] of namespacesOf(ns)) {
923
+ childInfo.push({
924
+ accessor: members.claim(rustPlainSnake(seg)),
925
+ mod: modules.claim(rustPlainSnake(seg)),
926
+ ns: childNs,
927
+ seg,
928
+ })
929
+ }
930
+ for (const [seg, irOp] of methodsOf(ns)) {
931
+ const op = model.opsById.get(irOp.id)
932
+ if (!op) continue
933
+ const ident = rustValueIdent(seg)
934
+ const plain = ident.startsWith("r#") ? ident.slice(2) : ident
935
+ const claimed = members.claim(plain)
936
+ const plan = planOp(op, claimed === plain ? ident : claimed, [...path, seg], names, throwOnError, model)
937
+ node.plans.push(plan)
938
+ plans.push(plan)
939
+ }
940
+ for (const c of childInfo) {
941
+ node.children.push({ accessor: c.accessor, node: visit(c.ns, [...path, c.seg], c.mod, ["client", "new"]) })
942
+ }
943
+ return node
944
+ }
945
+ const root = visit(model.ir.tree, [], "", ["inner", "new", "is_stale"])
946
+ return { plans, root }
947
+ }
948
+
949
+ /* ── files ── */
1109
950
 
951
+ function buildRustTypes(model: SdkModel, names: RustNames): string {
952
+ const body: string[] = []
1110
953
  /* always emit shared error-envelope field type used by per-op error structs */
1111
954
  body.push(`#[derive(Debug, Clone, Serialize, Deserialize)]`)
1112
955
  body.push(`pub struct ErrField {`)
@@ -1115,20 +958,30 @@ function buildRustTypes(spec: OpenApiSpec): string {
1115
958
  body.push(`\tpub path: String,`)
1116
959
  body.push(`}`)
1117
960
  body.push(``)
1118
-
1119
- for (const [name, schema] of sortedSchemas) {
1120
- const rustType = renderTopLevelRust(name, schema, decls)
1121
- body.push(rustType)
961
+ for (const name of model.schemaNames) {
962
+ if (Object.hasOwn(model.ir.schemaDescriptions, name)) body.push(...rustDoc(model.ir.schemaDescriptions[name]))
963
+ body.push(irRenderTopLevelRust(names.types.ref(name), model.ir.schemas[name], names.decls, undefined, names.types))
1122
964
  body.push(``)
1123
965
  }
966
+ return body.join("\n")
967
+ }
1124
968
 
1125
- const hoistedNames = [...decls.keys()].sort((a, b) => a.localeCompare(b))
1126
- for (const hname of hoistedNames) {
1127
- body.push(decls.get(hname) as string)
1128
- body.push(``)
969
+ function typesFile(schemaSource: string, names: RustNames): string {
970
+ const l: string[] = []
971
+ l.push(`/* Code generated by honey. DO NOT EDIT. */`)
972
+ l.push(``)
973
+ l.push(`#![allow(unused_imports, clippy::large_enum_variant)]`)
974
+ l.push(`use serde::{Serialize, Deserialize};`)
975
+ l.push(`use serde_repr::{Serialize_repr, Deserialize_repr};`)
976
+ l.push(`use std::collections::HashMap;`)
977
+ l.push(``)
978
+ l.push(schemaSource)
979
+ for (const name of [...names.decls.keys()].sort(cmpCodeUnit)) {
980
+ const decl = names.decls.get(name)
981
+ if (!decl) continue
982
+ l.push(decl)
983
+ l.push(``)
1129
984
  }
1130
-
1131
- l.push(body.join("\n"))
1132
985
  return l.join("\n")
1133
986
  }
1134
987
 
@@ -1136,6 +989,7 @@ function buildCargoToml(
1136
989
  crateName: string,
1137
990
  meta: { version: string; description?: string; homepage?: string; repository?: string; license?: string },
1138
991
  ): string {
992
+ /* TOML basic strings share JSON's escapes */
1139
993
  const pkgLines = [
1140
994
  `[package]`,
1141
995
  `name = "${crateName}"`,
@@ -1180,6 +1034,7 @@ function buildLibRs(hasRealtime: boolean): string {
1180
1034
  `pub mod errors;`,
1181
1035
  `pub mod invalidation;`,
1182
1036
  `pub mod invalidation_sync;`,
1037
+ `pub mod nullable;`,
1183
1038
  `pub mod resources;`,
1184
1039
  `pub mod result;`,
1185
1040
  `pub mod runtime;`,
@@ -1188,9 +1043,7 @@ function buildLibRs(hasRealtime: boolean): string {
1188
1043
  `pub mod types;`,
1189
1044
  `pub mod ws;`,
1190
1045
  ]
1191
- if (hasRealtime) {
1192
- lines.push(`pub mod realtime;`)
1193
- }
1046
+ if (hasRealtime) lines.push(`pub mod realtime;`)
1194
1047
  lines.push(``)
1195
1048
  lines.push(`pub use client::Client;`)
1196
1049
  lines.push(`pub use client::SyncClient;`)
@@ -1202,21 +1055,51 @@ function buildLibRs(hasRealtime: boolean): string {
1202
1055
  return lines.join("\n")
1203
1056
  }
1204
1057
 
1205
- function buildRustClient(spec: OpenApiSpec, options: RustSDKOptions, ir: ReturnType<typeof toIR>): string {
1206
- const throwOnError = options.throwOnError ?? true
1207
- const methods = collectMethodsFromIR(ir.tree, spec)
1208
- const topLevel = methods.filter((m) => m.action === "_call")
1058
+ function fileHeader(sync: boolean): string[] {
1059
+ const lines: string[] = []
1060
+ lines.push(`/* Code generated by honey. DO NOT EDIT. */`)
1061
+ lines.push(``)
1062
+ lines.push(`#![allow(unused_imports, unused_variables, unused_mut, dead_code, deprecated)]`)
1063
+ lines.push(`use std::sync::Arc;`)
1064
+ lines.push(`use futures_util::{self, Stream};`)
1065
+ lines.push(`use reqwest;`)
1066
+ lines.push(`use serde::{Serialize, Deserialize};`)
1067
+ lines.push(`use tokio_tungstenite;`)
1068
+ lines.push(`use crate::client::ClientInner;`)
1069
+ if (sync) lines.push(`use crate::client::SyncClientInner;`)
1070
+ lines.push(`use crate::errors::Error;`)
1071
+ lines.push(`use crate::result::SdkResult;`)
1072
+ lines.push(`use crate::types::*;`)
1073
+ lines.push(`use crate::sse::SseEvent;`)
1074
+ lines.push(`use crate::ws::TypedWebSocket;`)
1075
+ lines.push(``)
1076
+ return lines
1077
+ }
1209
1078
 
1210
- const serviceMap = collectSDKMethods(spec as Parameters<typeof collectSDKMethods>[0]).serviceMap
1079
+ function staleInit(tracker: string): string[] {
1080
+ return [
1081
+ `\t\tlet stale = match cfg.invalidation.clone() {`,
1082
+ `\t\t\tSome(mut inv) if inv.stale_time > 0 => {`,
1083
+ `\t\t\t\tif inv.stale_max_entries == 0 {`,
1084
+ `\t\t\t\t\tinv.stale_max_entries = cfg.stale_max_entries;`,
1085
+ `\t\t\t\t}`,
1086
+ `\t\t\t\tSome(${tracker}::new(Some(&inv)))`,
1087
+ `\t\t\t}`,
1088
+ `\t\t\t_ => None,`,
1089
+ `\t\t};`,
1090
+ ]
1091
+ }
1211
1092
 
1093
+ function buildRustClient(
1094
+ root: RustNode,
1095
+ plans: RustOpPlan[],
1096
+ throwOnError: boolean,
1097
+ auth: { headerName: string; prefix: string },
1098
+ serviceMap: Record<string, Record<string, unknown>>,
1099
+ realtimeIds: Set<string>,
1100
+ ): string {
1212
1101
  const body: string[] = []
1213
1102
 
1214
- /* build a realtime lookup map from collected methods */
1215
- const realtimeLookup = new Map<string, boolean>()
1216
- for (const m of methods) {
1217
- realtimeLookup.set(`${m.resource}.${m.action}`, m.isRealtime)
1218
- }
1219
-
1220
1103
  body.push(`pub(crate) struct ServiceEntry {`)
1221
1104
  body.push(`\tpub method: &'static str,`)
1222
1105
  body.push(`\tpub path: &'static str,`)
@@ -1227,28 +1110,25 @@ function buildRustClient(spec: OpenApiSpec, options: RustSDKOptions, ir: ReturnT
1227
1110
  body.push(`\tpub invalidate: &'static [&'static str],`)
1228
1111
  body.push(`}`)
1229
1112
  body.push(``)
1230
-
1231
1113
  body.push(
1232
1114
  `pub(crate) static INVALIDATION_MAP: once_cell::sync::Lazy<std::collections::HashMap<&'static str, std::collections::HashMap<&'static str, ServiceEntry>>> =`,
1233
1115
  )
1234
1116
  body.push(`\tonce_cell::sync::Lazy::new(|| {`)
1235
1117
  body.push(`\t\tlet mut map = std::collections::HashMap::new();`)
1236
- for (const [resource, actions] of Object.entries(serviceMap).sort(([a], [b]) => a.localeCompare(b))) {
1118
+ for (const [resource, actions] of Object.entries(serviceMap).sort(([a], [b]) => cmpCodeUnit(a, b))) {
1237
1119
  body.push(`\t\t{`)
1238
1120
  body.push(`\t\t\tlet mut resource_map = std::collections::HashMap::new();`)
1239
1121
  for (const [action, entry] of Object.entries(actions as Record<string, Record<string, unknown>>).sort(([a], [b]) =>
1240
- a.localeCompare(b),
1122
+ cmpCodeUnit(a, b),
1241
1123
  )) {
1242
- const inv = entry.invalidate as string[] | undefined
1243
- const invStr = inv && inv.length > 0 ? `&[${inv.map((s) => JSON.stringify(s)).join(", ")}]` : `&[]`
1244
- const params = entry.params as string[] | undefined
1245
- const paramsStr = params && params.length > 0 ? `&[${params.map((s) => JSON.stringify(s)).join(", ")}]` : `&[]`
1246
- const isRealtime = realtimeLookup.get(`${resource}.${action}`) ?? false
1124
+ const inv = (entry.invalidate as string[] | undefined) ?? []
1125
+ const params = (entry.params as string[] | undefined) ?? []
1126
+ const opId = action === "_call" ? resource : `${resource}.${action}`
1247
1127
  body.push(
1248
- `\t\t\tresource_map.insert(${JSON.stringify(action)}, ServiceEntry { method: ${JSON.stringify(String(entry.method))}, path: ${JSON.stringify(String(entry.path))}, params: ${paramsStr}, sse: ${entry.sse === true}, ws: ${entry.ws === true}, realtime: ${isRealtime}, invalidate: ${invStr} });`,
1128
+ `\t\t\tresource_map.insert(${rustString(action)}, ServiceEntry { method: ${rustString(String(entry.method))}, path: ${rustString(String(entry.path))}, params: &[${params.map(rustString).join(", ")}], sse: ${entry.sse === true}, ws: ${entry.ws === true}, realtime: ${realtimeIds.has(opId)}, invalidate: &[${inv.map(rustString).join(", ")}] });`,
1249
1129
  )
1250
1130
  }
1251
- body.push(`\t\t\tmap.insert(${JSON.stringify(resource)}, resource_map);`)
1131
+ body.push(`\t\t\tmap.insert(${rustString(resource)}, resource_map);`)
1252
1132
  body.push(`\t\t}`)
1253
1133
  }
1254
1134
  body.push(`\t\tmap`)
@@ -1259,72 +1139,52 @@ function buildRustClient(spec: OpenApiSpec, options: RustSDKOptions, ir: ReturnT
1259
1139
  body.push(`\tpub(crate) cfg: crate::runtime::ClientConfig,`)
1260
1140
  body.push(`\tpub(crate) stale: Option<crate::invalidation::StaleTracker>,`)
1261
1141
  body.push(`\tpub(crate) http_client: reqwest::Client,`)
1142
+ body.push(`\tpub(crate) auth: std::sync::Arc<crate::runtime::AuthState>,`)
1262
1143
  body.push(`}`)
1263
1144
  body.push(``)
1264
1145
 
1265
- const auth = detectAuthScheme(spec as Record<string, unknown>)
1266
1146
  body.push(`/// Client is the generated SDK client.`)
1267
1147
  body.push(`pub struct Client {`)
1268
1148
  body.push(`\tpub(crate) inner: std::sync::Arc<ClientInner>,`)
1269
- for (const [seg] of namespacesOf(ir.tree)) {
1270
- const safeName = safeResourceName(seg)
1271
- body.push(`\t${rustSnake(safeName)}: ${rustPascal(safeName)}Resource,`)
1272
- }
1149
+ for (const c of root.children) body.push(`\t${c.accessor}: ${c.node.structName},`)
1273
1150
  body.push(`}`)
1274
1151
  body.push(``)
1275
-
1276
1152
  body.push(`impl Client {`)
1277
1153
  body.push(`\t/// new creates a new SDK client. base_url must be non-empty.`)
1278
1154
  body.push(`\tpub fn new(mut cfg: crate::runtime::ClientConfig) -> Self {`)
1279
1155
  body.push(`\t\tif cfg.base_url.is_empty() {`)
1280
1156
  body.push(`\t\t\tpanic!("honey sdk: ClientConfig.base_url must not be empty");`)
1281
1157
  body.push(`\t\t}`)
1282
- body.push(`\t\t/* http_client: None → build a default reqwest::Client */`)
1283
- body.push(`\t\tlet http_client = if cfg.http_client.is_none() {`)
1284
- body.push(`\t\t\treqwest::Client::new()`)
1285
- body.push(`\t\t} else {`)
1286
- body.push(`\t\t\tcfg.http_client.take().unwrap()`)
1158
+ body.push(`\t\t/* http_client: None → a default client that never redirects to another origin */`)
1159
+ body.push(`\t\tlet http_client = match cfg.http_client.take() {`)
1160
+ body.push(`\t\t\tSome(c) => c,`)
1161
+ body.push(`\t\t\tNone => crate::runtime::default_http_client(),`)
1287
1162
  body.push(`\t\t};`)
1288
1163
  body.push(`\t\tif cfg.auth_header_name.is_none() {`)
1289
- body.push(`\t\t\tcfg.auth_header_name = Some(${JSON.stringify(auth.headerName)}.to_string());`)
1164
+ body.push(`\t\t\tcfg.auth_header_name = Some(${rustString(auth.headerName)}.to_string());`)
1290
1165
  body.push(`\t\t}`)
1291
1166
  body.push(`\t\tif cfg.auth_header_prefix.is_none() {`)
1292
- body.push(`\t\t\tcfg.auth_header_prefix = Some(${JSON.stringify(auth.prefix)}.to_string());`)
1167
+ body.push(`\t\t\tcfg.auth_header_prefix = Some(${rustString(auth.prefix)}.to_string());`)
1293
1168
  body.push(`\t\t}`)
1294
- body.push(`\t\tlet stale = if let Some(ref inv) = cfg.invalidation {`)
1295
- body.push(`\t\t\tif inv.stale_time > 0 {`)
1296
- body.push(`\t\t\t\tSome(crate::invalidation::StaleTracker::new(Some(inv)))`)
1297
- body.push(`\t\t\t} else { None }`)
1298
- body.push(`\t\t} else { None };`)
1299
- body.push(`\t\tlet inner = std::sync::Arc::new(ClientInner { cfg, stale, http_client });`)
1169
+ body.push(...staleInit("crate::invalidation::StaleTracker"))
1170
+ body.push(`\t\tlet auth = std::sync::Arc::new(crate::runtime::AuthState::new(cfg.bearer_token.clone()));`)
1171
+ body.push(`\t\tlet inner = std::sync::Arc::new(ClientInner { cfg, stale, http_client, auth });`)
1300
1172
  body.push(`\t\tClient {`)
1301
- for (const [seg] of namespacesOf(ir.tree)) {
1302
- const safeName = safeResourceName(seg)
1303
- body.push(
1304
- `\t\t\t${rustSnake(safeName)}: crate::resources::${rustSnake(safeName)}::new_${rustSnake(safeName)}_resource(&inner),`,
1305
- )
1173
+ for (const c of root.children) {
1174
+ body.push(`\t\t\t${c.accessor}: crate::resources::${c.node.modName}::new_resource(&inner),`)
1306
1175
  }
1307
1176
  body.push(`\t\t\tinner,`)
1308
1177
  body.push(`\t\t}`)
1309
1178
  body.push(`\t}`)
1310
1179
  body.push(``)
1311
-
1312
- for (const [seg] of namespacesOf(ir.tree)) {
1313
- const safeName = safeResourceName(seg)
1314
- const structName = `${rustPascal(safeName)}Resource`
1315
- body.push(`\t/// ${rustSnake(safeName)} returns a reference to the ${structName}.`)
1316
- body.push(`\tpub fn ${rustSnake(safeName)}(&self) -> &${structName} {`)
1317
- body.push(`\t\t&self.${rustSnake(safeName)}`)
1180
+ for (const c of root.children) {
1181
+ body.push(`\t/// ${c.accessor} returns a reference to the ${c.node.structName}.`)
1182
+ body.push(`\tpub fn ${c.accessor}(&self) -> &${c.node.structName} {`)
1183
+ body.push(`\t\t&self.${c.accessor}`)
1318
1184
  body.push(`\t}`)
1319
1185
  body.push(``)
1320
1186
  }
1321
-
1322
- for (const m of topLevel.sort((a, b) => a.resource.localeCompare(b.resource))) {
1323
- body.push(...emitMethod(m, "self", throwOnError))
1324
- }
1325
-
1326
- /* is_stale — public predicate consumers call to drive refetch decisions.
1327
- * Delegates to the StaleTracker; returns false when invalidation is disabled. */
1187
+ for (const plan of root.plans) body.push(...emitMethod(plan, "self", throwOnError).map((x) => `\t${x}`))
1328
1188
  body.push(`\t/// is_stale reports whether (method, path) is currently within an active stale window.`)
1329
1189
  body.push(`\tpub async fn is_stale(&self, method: &str, path: &str) -> bool {`)
1330
1190
  body.push(`\t\tmatch self.inner.stale {`)
@@ -1332,101 +1192,62 @@ function buildRustClient(spec: OpenApiSpec, options: RustSDKOptions, ir: ReturnT
1332
1192
  body.push(`\t\t\tNone => false,`)
1333
1193
  body.push(`\t\t}`)
1334
1194
  body.push(`\t}`)
1335
- body.push(``)
1336
-
1337
1195
  body.push(`}`)
1338
1196
  body.push(``)
1339
1197
 
1340
- const topLevelOptsEmitted = new Set<string>()
1341
- for (const m of topLevel.sort((a, b) => a.opId.localeCompare(b.opId))) {
1342
- const optsName = optsStructName(m)
1343
- if (topLevelOptsEmitted.has(optsName)) continue
1344
- topLevelOptsEmitted.add(optsName)
1345
- body.push(...emitOptsStruct(m))
1346
- }
1198
+ for (const plan of root.plans) body.push(...emitOptsStruct(plan), ...emitErrorTypes(plan))
1347
1199
 
1348
1200
  /* ── SyncClient ── */
1349
-
1350
- /* root namespaces that have at least one non-SSE/non-WS descendant method */
1351
- const syncNsSegs = namespacesOf(ir.tree)
1352
- .map(([seg]) => seg)
1353
- .filter((seg) => {
1354
- const nsMs = methods.filter((m) => !m.isSSE && !m.isWS && m.namePath[0] === seg)
1355
- return nsMs.length > 0
1356
- })
1357
-
1201
+ const syncChildren = root.children.filter((c) => hasSync(c.node))
1358
1202
  body.push(`pub(crate) struct SyncClientInner {`)
1359
1203
  body.push(`\tpub(crate) cfg: crate::runtime_sync::SyncClientConfig,`)
1360
1204
  body.push(`\tpub(crate) stale: Option<crate::invalidation_sync::StaleTrackerSync>,`)
1361
1205
  body.push(`\tpub(crate) http_client: reqwest::blocking::Client,`)
1206
+ body.push(`\tpub(crate) auth: std::sync::Arc<crate::runtime_sync::SyncAuthState>,`)
1362
1207
  body.push(`}`)
1363
1208
  body.push(``)
1364
-
1365
1209
  body.push(`/// SyncClient is the generated blocking SDK client.`)
1366
1210
  body.push(`/// WARNING: Do not use from inside a tokio async task — reqwest blocking will deadlock.`)
1367
1211
  body.push(`pub struct SyncClient {`)
1368
1212
  body.push(`\tpub(crate) inner: std::sync::Arc<SyncClientInner>,`)
1369
- for (const seg of syncNsSegs) {
1370
- const safeName = safeResourceName(seg)
1371
- body.push(`\t${rustSnake(safeName)}: ${rustPascal(safeName)}ResourceSync,`)
1372
- }
1213
+ for (const c of syncChildren) body.push(`\t${c.accessor}: ${c.node.syncStructName},`)
1373
1214
  body.push(`}`)
1374
1215
  body.push(``)
1375
-
1376
1216
  body.push(`impl SyncClient {`)
1377
1217
  body.push(`\t/// new creates a new blocking SDK client. base_url must be non-empty.`)
1378
1218
  body.push(`\tpub fn new(mut cfg: crate::runtime_sync::SyncClientConfig) -> Self {`)
1379
1219
  body.push(`\t\tif cfg.base_url.is_empty() {`)
1380
1220
  body.push(`\t\t\tpanic!("honey sdk: SyncClientConfig.base_url must not be empty");`)
1381
1221
  body.push(`\t\t}`)
1382
- body.push(`\t\tlet http_client = if cfg.http_client.is_none() {`)
1383
- body.push(`\t\t\treqwest::blocking::Client::new()`)
1384
- body.push(`\t\t} else {`)
1385
- body.push(`\t\t\tcfg.http_client.take().unwrap()`)
1222
+ body.push(`\t\tlet http_client = match cfg.http_client.take() {`)
1223
+ body.push(`\t\t\tSome(c) => c,`)
1224
+ body.push(`\t\t\tNone => crate::runtime_sync::default_http_client_blocking(),`)
1386
1225
  body.push(`\t\t};`)
1387
1226
  body.push(`\t\tif cfg.auth_header_name.is_none() {`)
1388
- body.push(`\t\t\tcfg.auth_header_name = Some(${JSON.stringify(auth.headerName)}.to_string());`)
1227
+ body.push(`\t\t\tcfg.auth_header_name = Some(${rustString(auth.headerName)}.to_string());`)
1389
1228
  body.push(`\t\t}`)
1390
1229
  body.push(`\t\tif cfg.auth_header_prefix.is_none() {`)
1391
- body.push(`\t\t\tcfg.auth_header_prefix = Some(${JSON.stringify(auth.prefix)}.to_string());`)
1230
+ body.push(`\t\t\tcfg.auth_header_prefix = Some(${rustString(auth.prefix)}.to_string());`)
1392
1231
  body.push(`\t\t}`)
1393
- body.push(`\t\tlet stale = if let Some(ref inv) = cfg.invalidation {`)
1394
- body.push(`\t\t\tif inv.stale_time > 0 {`)
1395
- body.push(`\t\t\t\tSome(crate::invalidation_sync::StaleTrackerSync::new(Some(inv)))`)
1396
- body.push(`\t\t\t} else { None }`)
1397
- body.push(`\t\t} else { None };`)
1398
- body.push(`\t\tlet inner = std::sync::Arc::new(SyncClientInner { cfg, stale, http_client });`)
1232
+ body.push(...staleInit("crate::invalidation_sync::StaleTrackerSync"))
1233
+ body.push(`\t\tlet auth = std::sync::Arc::new(crate::runtime_sync::SyncAuthState::new(cfg.bearer_token.clone()));`)
1234
+ body.push(`\t\tlet inner = std::sync::Arc::new(SyncClientInner { cfg, stale, http_client, auth });`)
1399
1235
  body.push(`\t\tSyncClient {`)
1400
- for (const seg of syncNsSegs) {
1401
- const safeName = safeResourceName(seg)
1402
- body.push(
1403
- `\t\t\t${rustSnake(safeName)}: ${rustPascal(safeName)}ResourceSync { client: std::sync::Arc::clone(&inner) },`,
1404
- )
1236
+ for (const c of syncChildren) {
1237
+ body.push(`\t\t\t${c.accessor}: crate::resources::${c.node.modName}::new_resource_sync(&inner),`)
1405
1238
  }
1406
1239
  body.push(`\t\t\tinner,`)
1407
1240
  body.push(`\t\t}`)
1408
1241
  body.push(`\t}`)
1409
1242
  body.push(``)
1410
-
1411
- for (const seg of syncNsSegs) {
1412
- const safeName = safeResourceName(seg)
1413
- const structName = `${rustPascal(safeName)}ResourceSync`
1414
- body.push(`\t/// ${rustSnake(safeName)} returns a reference to the ${structName}.`)
1415
- body.push(`\tpub fn ${rustSnake(safeName)}(&self) -> &${structName} {`)
1416
- body.push(`\t\t&self.${rustSnake(safeName)}`)
1243
+ for (const c of syncChildren) {
1244
+ body.push(`\t/// ${c.accessor} returns a reference to the ${c.node.syncStructName}.`)
1245
+ body.push(`\tpub fn ${c.accessor}(&self) -> &${c.node.syncStructName} {`)
1246
+ body.push(`\t\t&self.${c.accessor}`)
1417
1247
  body.push(`\t}`)
1418
1248
  body.push(``)
1419
1249
  }
1420
-
1421
- /* top-level sync methods (operationIds without resource.action split, e.g. `getUser`)
1422
- mirror the async Client's top-level methods. SSE/WS skipped — no sync variant. */
1423
- const syncTopLevel = topLevel.filter((m) => !m.isSSE && !m.isWS)
1424
- for (const m of syncTopLevel.sort((a, b) => a.resource.localeCompare(b.resource))) {
1425
- body.push(...emitSyncMethod(m, "self", throwOnError))
1426
- }
1427
-
1428
- /* is_stale (sync) — public predicate consumers call to drive refetch decisions.
1429
- * Delegates to the StaleTrackerSync; returns false when invalidation is disabled. */
1250
+ for (const plan of root.plans) body.push(...emitSyncMethod(plan, "self", throwOnError).map((x) => `\t${x}`))
1430
1251
  body.push(`\t/// is_stale reports whether (method, path) is currently within an active stale window.`)
1431
1252
  body.push(`\tpub fn is_stale(&self, method: &str, path: &str) -> bool {`)
1432
1253
  body.push(`\t\tmatch self.inner.stale {`)
@@ -1434,22 +1255,18 @@ function buildRustClient(spec: OpenApiSpec, options: RustSDKOptions, ir: ReturnT
1434
1255
  body.push(`\t\t\tNone => false,`)
1435
1256
  body.push(`\t\t}`)
1436
1257
  body.push(`\t}`)
1437
- body.push(``)
1438
-
1439
1258
  body.push(`}`)
1440
1259
  body.push(``)
1441
-
1442
- const bodyStr = body.join("\n")
1260
+ void plans
1443
1261
 
1444
1262
  const out: string[] = []
1445
1263
  out.push(`/* Code generated by honey. DO NOT EDIT. */`)
1446
1264
  out.push(``)
1447
- out.push(`#![allow(unused_imports, unused_variables, dead_code)]`)
1265
+ out.push(`#![allow(unused_imports, unused_variables, unused_mut, dead_code, deprecated)]`)
1448
1266
  out.push(`use std::sync::Arc;`)
1449
1267
  out.push(`use once_cell;`)
1450
1268
  out.push(`use reqwest;`)
1451
1269
  out.push(`use tokio_tungstenite;`)
1452
- out.push(`use urlencoding;`)
1453
1270
  out.push(`use futures_util::{self, Stream};`)
1454
1271
  out.push(`use crate::errors::Error;`)
1455
1272
  out.push(`use crate::resources::*;`)
@@ -1459,238 +1276,127 @@ function buildRustClient(spec: OpenApiSpec, options: RustSDKOptions, ir: ReturnT
1459
1276
  out.push(`use crate::types::*;`)
1460
1277
  out.push(`use crate::ws::TypedWebSocket;`)
1461
1278
  out.push(``)
1462
- out.push(bodyStr)
1279
+ out.push(body.join("\n"))
1463
1280
  return out.join("\n")
1464
1281
  }
1465
1282
 
1466
- /* ── buildRustResources ── */
1467
-
1468
- /** Emit standard file header imports for a resource .rs file. */
1469
- function emitRustResourceFileHeader(rMethods: MethodInfo[]): string[] {
1470
- const hasSSE = rMethods.some((m) => m.isSSE)
1471
- const syncMethods = rMethods.filter((m) => !m.isSSE && !m.isWS)
1472
- const hasSyncMethods = syncMethods.length > 0
1473
- const lines: string[] = []
1474
- lines.push(`/* Code generated by honey. DO NOT EDIT. */`)
1475
- lines.push(``)
1476
- lines.push(`#![allow(unused_imports, unused_variables, dead_code)]`)
1477
- lines.push(`use std::sync::Arc;`)
1478
- lines.push(`use futures_util::{self, Stream};`)
1479
- lines.push(`use reqwest;`)
1480
- lines.push(`use serde::{Serialize, Deserialize};`)
1481
- lines.push(`use tokio_tungstenite;`)
1482
- lines.push(`use urlencoding;`)
1483
- lines.push(`use crate::client::ClientInner;`)
1484
- if (hasSyncMethods) lines.push(`use crate::client::SyncClientInner;`)
1485
- lines.push(`use crate::errors::Error;`)
1486
- lines.push(`use crate::result::SdkResult;`)
1487
- lines.push(`use crate::runtime::*;`)
1488
- if (hasSyncMethods) lines.push(`use crate::runtime_sync::*;`)
1489
- lines.push(`use crate::types::*;`)
1490
- if (hasSSE) {
1491
- lines.push(`use crate::sse::{SseEvent, parse_sse_stream};`)
1492
- } else {
1493
- lines.push(`use crate::sse::SseEvent;`)
1494
- }
1495
- lines.push(`use crate::ws::TypedWebSocket;`)
1496
- lines.push(``)
1497
- return lines
1498
- }
1499
-
1500
- function buildRustResources(
1501
- spec: OpenApiSpec,
1502
- options: RustSDKOptions,
1503
- ir: ReturnType<typeof toIR>,
1504
- ): Map<string, string> {
1505
- const throwOnError = options.throwOnError ?? true
1506
- const allMethods = collectMethodsFromIR(ir.tree, spec)
1507
-
1283
+ function buildRustResources(root: RustNode, throwOnError: boolean): Map<string, string> {
1508
1284
  const files = new Map<string, string>()
1509
1285
 
1510
- /* ---- src/resources/mod.rs: one `pub mod` per root namespace ---- */
1511
1286
  const modLines: string[] = [`/* Code generated by honey. DO NOT EDIT. */`, ``]
1512
- for (const [seg] of namespacesOf(ir.tree)) {
1513
- modLines.push(`pub mod ${rustSnake(safeResourceName(seg))};`)
1514
- }
1515
- for (const [seg] of namespacesOf(ir.tree)) {
1516
- const safeName = safeResourceName(seg)
1517
- modLines.push(`pub use ${rustSnake(safeName)}::${rustPascal(safeName)}Resource;`)
1518
- const hasSyncDescendants = allMethods.some((m) => !m.isSSE && !m.isWS && m.namePath[0] === seg)
1519
- if (hasSyncDescendants) {
1520
- modLines.push(`pub use ${rustSnake(safeName)}::${rustPascal(safeName)}ResourceSync;`)
1521
- }
1287
+ for (const c of root.children) modLines.push(`pub mod ${c.node.modName};`)
1288
+ for (const c of root.children) {
1289
+ modLines.push(`pub use ${c.node.modName}::${c.node.structName};`)
1290
+ if (hasSync(c.node)) modLines.push(`pub use ${c.node.modName}::${c.node.syncStructName};`)
1522
1291
  }
1523
1292
  modLines.push(``)
1524
1293
  files.set("src/resources/mod.rs", modLines.join("\n"))
1525
1294
 
1526
- /* ---- recursive resource file emitter ---- */
1527
- function emitNsFile(ns: IRNamespace, path: string[]): void {
1528
- const safePath = path.map((s) => safeResourceName(s))
1529
- const structName = `${safePath.map(rustPascal).join("")}Resource`
1530
- const syncStructName = `${structName}Sync`
1531
-
1532
- /* collect methods directly on this ns (not descendants) */
1533
- const directMethods = methodsOf(ns).map(([, op]) => {
1534
- const m = allMethods.find((x) => x.opId === op.id)
1535
- if (!m) throw new Error(`missing MethodInfo for ${op.id}`)
1536
- return m
1537
- })
1538
-
1539
- const childSegs = namespacesOf(ns).map(([seg]) => seg)
1540
- const hasChildren = childSegs.length > 0
1541
-
1542
- const fileLines: string[] = [...emitRustResourceFileHeader(directMethods)]
1543
-
1544
- /* helper: full Pascal struct name for a child at a given path */
1545
- const childStructName = (childSeg: string): string =>
1546
- `${[...path, childSeg].map((s) => rustPascal(safeResourceName(s))).join("")}Resource`
1547
-
1548
- if (hasChildren) {
1549
- /* sub-modules for child namespaces */
1550
- for (const childSeg of childSegs) {
1551
- fileLines.push(`pub mod ${rustSnake(safeResourceName(childSeg))};`)
1552
- }
1553
- /* re-export child resource types for convenience */
1554
- for (const childSeg of childSegs) {
1555
- const childSafe = safeResourceName(childSeg)
1556
- fileLines.push(`pub use ${rustSnake(childSafe)}::${childStructName(childSeg)};`)
1557
- }
1558
- fileLines.push(``)
1295
+ function emitNode(node: RustNode, dir: string[]): void {
1296
+ const sync = hasSync(node)
1297
+ const l: string[] = [...fileHeader(sync)]
1298
+ for (const c of node.children) l.push(`pub mod ${c.node.modName};`)
1299
+ for (const c of node.children) {
1300
+ l.push(`pub use ${c.node.modName}::${c.node.structName};`)
1301
+ if (hasSync(c.node)) l.push(`pub use ${c.node.modName}::${c.node.syncStructName};`)
1559
1302
  }
1303
+ if (node.children.length > 0) l.push(``)
1560
1304
 
1561
- /* async resource struct — includes child namespace accessor fields */
1562
- fileLines.push(`pub struct ${structName} {`)
1563
- fileLines.push(`\tpub(crate) client: std::sync::Arc<crate::client::ClientInner>,`)
1564
- for (const childSeg of childSegs) {
1565
- const childSafe = safeResourceName(childSeg)
1566
- fileLines.push(`\tpub ${rustSnake(childSafe)}: ${childStructName(childSeg)},`)
1567
- }
1568
- fileLines.push(`}`)
1569
- fileLines.push(``)
1570
-
1571
- fileLines.push(`impl ${structName} {`)
1572
- /* accessor methods for child namespaces */
1573
- for (const childSeg of childSegs) {
1574
- const childSafe = safeResourceName(childSeg)
1575
- fileLines.push(`\tpub fn ${rustSnake(childSafe)}(&self) -> &${childStructName(childSeg)} {`)
1576
- fileLines.push(`\t\t&self.${rustSnake(childSafe)}`)
1577
- fileLines.push(`\t}`)
1578
- fileLines.push(``)
1579
- }
1580
- /* direct methods */
1581
- for (const m of directMethods.sort((a, b) => a.methodAction.localeCompare(b.methodAction))) {
1582
- fileLines.push(...emitMethod(m, "self.client", throwOnError).map((l) => `\t${l}`))
1305
+ l.push(`pub struct ${node.structName} {`)
1306
+ l.push(`\tpub(crate) client: std::sync::Arc<crate::client::ClientInner>,`)
1307
+ for (const c of node.children) l.push(`\tpub ${c.accessor}: ${c.node.structName},`)
1308
+ l.push(`}`)
1309
+ l.push(``)
1310
+ l.push(`impl ${node.structName} {`)
1311
+ for (const c of node.children) {
1312
+ l.push(`\tpub fn ${c.accessor}(&self) -> &${c.node.structName} {`)
1313
+ l.push(`\t\t&self.${c.accessor}`)
1314
+ l.push(`\t}`)
1315
+ l.push(``)
1583
1316
  }
1584
- fileLines.push(`}`)
1585
- fileLines.push(``)
1317
+ for (const plan of node.plans) l.push(...emitMethod(plan, "self.client", throwOnError).map((x) => `\t${x}`))
1318
+ l.push(`}`)
1319
+ l.push(``)
1320
+ l.push(`pub(crate) fn new_resource(client: &std::sync::Arc<crate::client::ClientInner>) -> ${node.structName} {`)
1321
+ l.push(`\t${node.structName} {`)
1322
+ l.push(`\t\tclient: std::sync::Arc::clone(client),`)
1323
+ for (const c of node.children) l.push(`\t\t${c.accessor}: ${c.node.modName}::new_resource(client),`)
1324
+ l.push(`\t}`)
1325
+ l.push(`}`)
1326
+ l.push(``)
1586
1327
 
1587
- /* constructor free-fn used by Client::new / parent resource constructors */
1588
- const constructorFn = `new_${safePath.map(rustSnake).join("_")}_resource`
1589
- fileLines.push(
1590
- `pub(crate) fn ${constructorFn}(client: &std::sync::Arc<crate::client::ClientInner>) -> ${structName} {`,
1591
- )
1592
- fileLines.push(`\t${structName} {`)
1593
- fileLines.push(`\t\tclient: std::sync::Arc::clone(client),`)
1594
- for (const childSeg of childSegs) {
1595
- const childSafe = safeResourceName(childSeg)
1596
- const childConstructor = `${rustSnake(childSafe)}::new_${[...safePath.map(rustSnake), rustSnake(childSafe)].join("_")}_resource`
1597
- fileLines.push(`\t\t${rustSnake(childSafe)}: ${childConstructor}(client),`)
1598
- }
1599
- fileLines.push(`\t}`)
1600
- fileLines.push(`}`)
1601
- fileLines.push(``)
1602
-
1603
- /* sync struct — only when direct non-SSE/non-WS methods exist */
1604
- const directSyncMethods = directMethods.filter((m) => !m.isSSE && !m.isWS)
1605
- if (directSyncMethods.length > 0) {
1606
- fileLines.push(`pub struct ${syncStructName} {`)
1607
- fileLines.push(`\tpub(crate) client: std::sync::Arc<crate::client::SyncClientInner>,`)
1608
- fileLines.push(`}`)
1609
- fileLines.push(``)
1610
- fileLines.push(`impl ${syncStructName} {`)
1611
- for (const m of directSyncMethods.sort((a, b) => a.methodAction.localeCompare(b.methodAction))) {
1612
- fileLines.push(...emitSyncMethod(m, "self.client", throwOnError).map((l) => `\t${l}`))
1328
+ if (sync) {
1329
+ const syncKids = node.children.filter((c) => hasSync(c.node))
1330
+ l.push(`pub struct ${node.syncStructName} {`)
1331
+ l.push(`\tpub(crate) client: std::sync::Arc<crate::client::SyncClientInner>,`)
1332
+ for (const c of syncKids) l.push(`\tpub ${c.accessor}: ${c.node.syncStructName},`)
1333
+ l.push(`}`)
1334
+ l.push(``)
1335
+ l.push(`impl ${node.syncStructName} {`)
1336
+ for (const c of syncKids) {
1337
+ l.push(`\tpub fn ${c.accessor}(&self) -> &${c.node.syncStructName} {`)
1338
+ l.push(`\t\t&self.${c.accessor}`)
1339
+ l.push(`\t}`)
1340
+ l.push(``)
1613
1341
  }
1614
- fileLines.push(`}`)
1615
- fileLines.push(``)
1616
- }
1617
-
1618
- /* opts structs */
1619
- const optsEmitted = new Set<string>()
1620
- for (const m of directMethods.sort((a, b) => a.opId.localeCompare(b.opId))) {
1621
- const optsName = optsStructName(m)
1622
- if (optsEmitted.has(optsName)) continue
1623
- optsEmitted.add(optsName)
1624
- fileLines.push(...emitOptsStruct(m))
1625
- }
1626
-
1627
- /* per-op error envelopes */
1628
- const errEmitted = new Set<string>()
1629
- for (const m of directMethods.sort((a, b) => a.opId.localeCompare(b.opId))) {
1630
- if (errEmitted.has(m.opId)) continue
1631
- errEmitted.add(m.opId)
1632
- fileLines.push(...emitOperationErrorTypes(m.opId, m.op))
1342
+ for (const plan of node.plans) l.push(...emitSyncMethod(plan, "self.client", throwOnError).map((x) => `\t${x}`))
1343
+ l.push(`}`)
1344
+ l.push(``)
1345
+ l.push(
1346
+ `pub(crate) fn new_resource_sync(client: &std::sync::Arc<crate::client::SyncClientInner>) -> ${node.syncStructName} {`,
1347
+ )
1348
+ l.push(`\t${node.syncStructName} {`)
1349
+ l.push(`\t\tclient: std::sync::Arc::clone(client),`)
1350
+ for (const c of syncKids) l.push(`\t\t${c.accessor}: ${c.node.modName}::new_resource_sync(client),`)
1351
+ l.push(`\t}`)
1352
+ l.push(`}`)
1353
+ l.push(``)
1633
1354
  }
1634
1355
 
1635
- /* determine file path: namespace with children → dir/mod.rs, leaf → dir.rs */
1636
- const dirPath = safePath.map(rustSnake).join("/")
1637
- const filePath = hasChildren ? `src/resources/${dirPath}/mod.rs` : `src/resources/${dirPath}.rs`
1638
- files.set(filePath, fileLines.join("\n"))
1356
+ for (const plan of node.plans) l.push(...emitOptsStruct(plan), ...emitErrorTypes(plan))
1639
1357
 
1640
- /* recurse into children */
1641
- for (const [childSeg, childNs] of namespacesOf(ns)) {
1642
- emitNsFile(childNs, [...path, childSeg])
1643
- }
1358
+ const here = [...dir, node.modName]
1359
+ const filePath =
1360
+ node.children.length > 0 ? `src/resources/${here.join("/")}/mod.rs` : `src/resources/${here.join("/")}.rs`
1361
+ files.set(filePath, l.join("\n"))
1362
+ for (const c of node.children) emitNode(c.node, here)
1644
1363
  }
1645
-
1646
- for (const [seg, ns] of namespacesOf(ir.tree)) {
1647
- emitNsFile(ns, [seg])
1648
- }
1649
-
1364
+ for (const c of root.children) emitNode(c.node, [])
1650
1365
  return files
1651
1366
  }
1652
1367
 
1653
1368
  export function generateRustSDK(spec: Record<string, unknown>, options: RustSDKOptions = {}): GeneratedRustSDK {
1654
1369
  const crateName = validateCrateName(options.crateName ?? "honey-sdk")
1370
+ const throwOnError = options.throwOnError ?? true
1655
1371
 
1656
- /* toIR validates operationId collisions — throws before any codegen runs */
1657
- const ir = toIR(spec as Parameters<typeof toIR>[0])
1658
-
1659
- const { serviceMap } = collectSDKMethods(spec as Parameters<typeof collectSDKMethods>[0])
1660
-
1661
- /* detect whether any operation uses x-realtime */
1662
- const hasRealtime = collectMethodsFromIR(ir.tree, spec as unknown as OpenApiSpec).some((m) => m.isRealtime)
1372
+ const input = spec as unknown as OpenApiSpecInput
1373
+ const model = buildSdkModel(input)
1374
+ const serviceMap = serviceMapOf(model.ir)
1375
+ const names = buildRustNames(model)
1376
+ const hasRealtime = model.ops.some((op) => op.stream === "realtime")
1377
+ const realtimeIds = new Set(model.ops.filter((op) => op.stream === "realtime").map((op) => op.id))
1663
1378
 
1664
1379
  const files: Record<string, string> = {}
1665
-
1666
- /* copy static runtime files; realtime.rs is conditional; runtime_sync + invalidation_sync always */
1667
1380
  for (const [name, content] of loadRustRuntimeTemplates()) {
1668
- if (name === "realtime.rs") {
1669
- if (hasRealtime) files[`src/${name}`] = content
1670
- } else {
1671
- files[`src/${name}`] = content
1672
- }
1381
+ if (name === "realtime.rs" && !hasRealtime) continue
1382
+ files[`src/${name}`] = content
1673
1383
  }
1674
1384
 
1675
- /* types.rs uses unresolved spec — resolveRefs strips components.schemas */
1676
- files["src/types.rs"] = buildRustTypes(spec as unknown as OpenApiSpec)
1677
- files["src/client.rs"] = buildRustClient(spec as unknown as OpenApiSpec, options, ir)
1385
+ /* schema types claim their names first; operations then hoist into the same table */
1386
+ const schemaSource = buildRustTypes(model, names)
1387
+ const { plans, root } = buildTree(model, names, throwOnError)
1388
+ files["src/client.rs"] = buildRustClient(root, plans, throwOnError, model.ir.auth, serviceMap, realtimeIds)
1389
+ for (const [relPath, content] of buildRustResources(root, throwOnError)) files[relPath] = content
1390
+ files["src/types.rs"] = typesFile(schemaSource, names)
1678
1391
  files["src/lib.rs"] = buildLibRs(hasRealtime)
1679
1392
  files["Cargo.toml"] = buildCargoToml(crateName, {
1680
- version: options.version ?? "0.1.0",
1681
1393
  description: options.description,
1682
1394
  homepage: options.homepage,
1683
- repository: options.repository,
1684
1395
  license: options.license,
1396
+ repository: options.repository,
1397
+ version: options.version ?? "0.1.0",
1685
1398
  })
1686
-
1687
- for (const [relPath, content] of buildRustResources(spec as unknown as OpenApiSpec, options, ir)) {
1688
- files[relPath] = content
1689
- }
1690
-
1691
- if (!files["src/resources/mod.rs"]) {
1692
- files["src/resources/mod.rs"] = `/* Code generated by honey. DO NOT EDIT. */\n`
1693
- }
1399
+ if (!files["src/resources/mod.rs"]) files["src/resources/mod.rs"] = `/* Code generated by honey. DO NOT EDIT. */\n`
1694
1400
 
1695
1401
  return { files, serviceMap }
1696
1402
  }