@lovrozagar/honey 0.6.5 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +410 -149
  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 +776 -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 +1893 -1761
  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 +271 -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 +562 -424
  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
package/src/codegen-go.ts CHANGED
@@ -1,16 +1,34 @@
1
1
  /* Go SDK emitter — generates a complete Go module from an OpenAPI 3.1 spec.
2
- * Mirrors the TS emitter pattern (string-array + join, tab indent).
3
- * Static runtime files (sse.go, ws.go, etc.) are read from ./client-go/ at
4
- * module load time and copied verbatim into the output file map.
2
+ *
3
+ * Input is the SDK request model (codegen-sdk-model.ts), never the raw spec. Every identifier
4
+ * comes from a NameScope seeded with the runtime's own package-level names, and every spec string
5
+ * lands in source through goString / goComment (codegen-lang.ts).
6
+ *
7
+ * Static runtime files (sse.go, ws.go, etc.) are read from ./client-go/ at module load time and
8
+ * copied verbatim into the output file map.
5
9
  */
6
10
 
7
11
  import { spawnSync } from "node:child_process"
8
12
  import { readFileSync } from "node:fs"
9
13
  import { fileURLToPath } from "node:url"
10
- import { collectSDKMethods, extractOpenApiPathParams, isSSEOperation, resolveRefs } from "./codegen.ts"
11
- import { methodsOf, namespacesOf, toIR } from "./codegen-ir.ts"
12
- import type { IRNamespace, IROperation } from "./codegen-ir.ts"
13
- import { GO_KEYWORDS, goPascal, isStringEnum, renderTopLevel, renderUse } from "./go-type-emitter.ts"
14
+ import type { OpenApiSpecInput } from "./codegen.ts"
15
+ import { serviceMapOf } from "./codegen.ts"
16
+ import { methodsOf, namespacesOf, specAuth } from "./codegen-ir.ts"
17
+ import type { IRAuth, IRInfo, IRNamespace, IRSchema } from "./codegen-ir.ts"
18
+ import {
19
+ GO_KEYWORDS,
20
+ GO_PREDECLARED,
21
+ NameScope,
22
+ cmpCodeUnit,
23
+ goComment,
24
+ goExported,
25
+ goLocal,
26
+ goString,
27
+ } from "./codegen-lang.ts"
28
+ import { buildSdkModel, valueCycleSchemas } from "./codegen-sdk-model.ts"
29
+ import type { SdkModel, SdkOp } from "./codegen-sdk-model.ts"
30
+ import { irRenderTopLevel, irRenderUse } from "./go-type-emitter.ts"
31
+ import type { GoTypeNames, RenderUseCtx } from "./go-type-emitter.ts"
14
32
 
15
33
  /* ── types ── */
16
34
 
@@ -30,35 +48,29 @@ export type GeneratedGoSDK = {
30
48
  serviceMap: Record<string, Record<string, unknown>>
31
49
  }
32
50
 
33
- type OpenApiSpec = {
34
- components?: {
35
- schemas?: Record<string, Record<string, unknown>>
36
- }
37
- info: { description?: string; title: string; version: string }
38
- openapi: string
39
- paths: Record<string, Record<string, Record<string, unknown>>>
40
- }
41
-
42
51
  /* ── runtime template loader ── */
43
52
 
44
53
  let runtimeCache: Map<string, string> | null = null
45
54
 
55
+ const RUNTIME_FILES = [
56
+ "result.go",
57
+ "errors.go",
58
+ "runtime.go",
59
+ "invalidation.go",
60
+ "sse.go",
61
+ "ws.go",
62
+ "transport.go",
63
+ "realtime.go",
64
+ "export.go",
65
+ "nullable.go",
66
+ ]
67
+
46
68
  /** Reads static .go runtime files from ./client-go/ and caches them. */
47
69
  export function loadGoRuntimeTemplates(): Map<string, string> {
48
70
  if (runtimeCache) return runtimeCache
49
71
 
50
- const names = [
51
- "result.go",
52
- "errors.go",
53
- "runtime.go",
54
- "invalidation.go",
55
- "sse.go",
56
- "ws.go",
57
- "transport.go",
58
- "realtime.go",
59
- ]
60
72
  const cache = new Map<string, string>()
61
- for (const name of names) {
73
+ for (const name of RUNTIME_FILES) {
62
74
  const filePath = fileURLToPath(new URL(`./client-go/${name}`, import.meta.url))
63
75
  let content: string
64
76
  try {
@@ -72,17 +84,42 @@ export function loadGoRuntimeTemplates(): Map<string, string> {
72
84
  return cache
73
85
  }
74
86
 
87
+ let runtimeIdentCache: Set<string> | null = null
88
+
89
+ /** Every package-level identifier the runtime files declare; generated names must avoid them all. */
90
+ export function goRuntimeIdentifiers(): Set<string> {
91
+ if (runtimeIdentCache) return runtimeIdentCache
92
+ const out = new Set<string>()
93
+ for (const src of loadGoRuntimeTemplates().values()) {
94
+ let inBlock = false
95
+ for (const line of src.split("\n")) {
96
+ const top = /^(?:func|type|var|const)\s+([A-Za-z_]\w*)/.exec(line)
97
+ if (top) out.add(top[1])
98
+ if (/^(?:var|const|type)\s*\($/.test(line)) {
99
+ inBlock = true
100
+ continue
101
+ }
102
+ if (inBlock) {
103
+ if (line.startsWith(")")) inBlock = false
104
+ else {
105
+ const m = /^\t([A-Za-z_]\w*)/.exec(line)
106
+ if (m) out.add(m[1])
107
+ }
108
+ }
109
+ }
110
+ }
111
+ runtimeIdentCache = out
112
+ return out
113
+ }
114
+
75
115
  /* ── helpers ── */
76
116
 
117
+ /** @deprecated kept for callers outside this file; use goLocal / goExported. */
77
118
  export function safeResourceName(name: string): string {
78
119
  const lower = name.toLowerCase()
79
120
  return GO_KEYWORDS.has(lower) ? `${name}_` : name
80
121
  }
81
122
 
82
- function nestedStructName(path: string[]): string {
83
- return `${path.map(goPascal).join("")}Resource`
84
- }
85
-
86
123
  /**
87
124
  * Derive auth header name + prefix from OpenAPI securitySchemes.
88
125
  * Defaults to Authorization + "Bearer " when no scheme is declared, preserving
@@ -90,222 +127,98 @@ function nestedStructName(path: string[]): string {
90
127
  * embeds a "Format: <Prefix> {token}" hint (common pattern for APIs like Anyrow
91
128
  * that use `Authorization: ApiKey <token>`).
92
129
  */
93
- export function detectAuthScheme(spec: Record<string, unknown>): { headerName: string; prefix: string } {
94
- const components = (spec.components ?? {}) as Record<string, unknown>
95
- const schemes = (components.securitySchemes ?? {}) as Record<string, Record<string, unknown>>
96
- const keys = Object.keys(schemes).sort()
97
- if (keys.length === 0) return { headerName: "Authorization", prefix: "Bearer " }
98
- const scheme = schemes[keys[0]]
99
- const type = String(scheme.type ?? "")
100
- if (type === "http") {
101
- const httpScheme = String(scheme.scheme ?? "").toLowerCase()
102
- if (httpScheme === "basic") return { headerName: "Authorization", prefix: "Basic " }
103
- return { headerName: "Authorization", prefix: "Bearer " }
104
- }
105
- if (type === "apiKey" && scheme.in === "header") {
106
- const headerName = String(scheme.name ?? "Authorization")
107
- const desc = String(scheme.description ?? "")
108
- const match = /Format:\s*(\S+)\s+\{/i.exec(desc)
109
- if (match) return { headerName, prefix: `${match[1]} ` }
110
- return { headerName, prefix: "" }
111
- }
112
- return { headerName: "Authorization", prefix: "Bearer " }
130
+ /** The auth header and prefix the spec's first security scheme asks for (see `IR.auth`). */
131
+ export function detectAuthScheme(spec: Record<string, unknown>): IRAuth {
132
+ return specAuth(spec as unknown as OpenApiSpecInput)
113
133
  }
114
134
 
115
135
  function goImportBlock(stdlib: string[], thirdParty: string[]): string {
116
136
  const l: string[] = []
117
137
  l.push(`import (`)
118
- for (const p of [...stdlib].sort()) l.push(`\t"${p}"`)
138
+ for (const p of [...new Set(stdlib)].sort(cmpCodeUnit)) l.push(`\t"${p}"`)
119
139
  if (thirdParty.length > 0) {
120
140
  l.push(``)
121
- for (const p of [...thirdParty].sort()) l.push(`\t"${p}"`)
141
+ for (const p of [...thirdParty].sort(cmpCodeUnit)) l.push(`\t"${p}"`)
122
142
  }
123
143
  l.push(`)`)
124
144
  return l.join("\n")
125
145
  }
126
146
 
127
- /** Converts /users/{id} → string expression pieces for URL construction. */
128
- function pathToGoExpr(path: string): string {
129
- const params = extractOpenApiPathParams(path)
130
- if (params.length === 0) return JSON.stringify(path)
131
- let expr = JSON.stringify(path)
132
- for (const p of params) {
133
- expr = expr.replace(`{${p}}`, `" + url.PathEscape(${p}) + "`)
134
- }
135
- /* clean up empty string concatenation at edges */
136
- expr = expr.replace(/"" \+ /g, "").replace(/ \+ ""/g, "")
137
- return expr
138
- }
139
-
140
- /** Extracts the Go response type for an operation from its 2xx responses. */
141
- function goResponseType(op: Record<string, unknown>): string {
142
- const responses = op.responses as Record<string, Record<string, unknown>> | undefined
143
- if (!responses) return "json.RawMessage"
144
-
145
- for (const [status, response] of Object.entries(responses)) {
146
- const code = Number.parseInt(status, 10)
147
- if (Number.isNaN(code) || code < 200 || code >= 300) continue
148
- if (code === 204) return ""
149
- const content = response.content as Record<string, Record<string, unknown>> | undefined
150
- if (!content) return ""
151
- const jsonSchema = content["application/json"]?.schema as Record<string, unknown> | undefined
152
- if (!jsonSchema) return "json.RawMessage"
153
- if (jsonSchema.$ref) {
154
- return goPascal((jsonSchema.$ref as string).split("/").pop() ?? "")
155
- }
156
- if (jsonSchema.type === "array" && jsonSchema.items) {
157
- const items = jsonSchema.items as Record<string, unknown>
158
- if (items.$ref) {
159
- return `[]${goPascal((items.$ref as string).split("/").pop() ?? "")}`
160
- }
161
- return `[]${renderUse(items, { decls: new Map(), fieldName: "item", parentName: "Body" })}`
162
- }
163
- if (jsonSchema.type === "string") return "string"
164
- if (jsonSchema.type === "integer") return "int64"
165
- if (jsonSchema.type === "number") return "float64"
166
- if (jsonSchema.type === "boolean") return "bool"
167
- return "json.RawMessage"
168
- }
169
- return "json.RawMessage"
170
- }
171
-
172
- /** Extracts the Go input type (request body schema name) for an operation. */
173
- function goBodyType(op: Record<string, unknown>): string | null {
174
- const requestBody = op.requestBody as Record<string, unknown> | undefined
175
- if (!requestBody) return null
176
- const content = requestBody.content as Record<string, Record<string, unknown>> | undefined
177
- if (!content) return null
178
- const jsonContent = content["application/json"]
179
- if (!jsonContent?.schema) return null
180
- const schema = jsonContent.schema as Record<string, unknown>
181
- if (schema.$ref) {
182
- return goPascal((schema.$ref as string).split("/").pop() ?? "")
183
- }
184
- return "json.RawMessage"
185
- }
147
+ /* ── naming ── */
186
148
 
187
- /** True iff operation declares `application/octet-stream` request body. */
188
- function hasOctetStreamBody(op: Record<string, unknown>): boolean {
189
- const requestBody = op.requestBody as Record<string, unknown> | undefined
190
- const content = requestBody?.content as Record<string, unknown> | undefined
191
- return content !== undefined && "application/octet-stream" in content
149
+ /** Package-level names for one generated SDK. */
150
+ export type GoNames = {
151
+ scope: NameScope
152
+ types: GoTypeNames
153
+ hoistedDecls: Map<string, string>
192
154
  }
193
155
 
194
- type QueryParam = {
195
- name: string
196
- required: boolean
197
- schema: Record<string, unknown>
156
+ export function buildGoNames(model: SdkModel): GoNames {
157
+ const scope = new NameScope([...goRuntimeIdentifiers(), ...GO_PREDECLARED, "Client", "NewClient", "serviceEntry"])
158
+ const schemaNames = new Map<string, string>()
159
+ /* schemas first: a schema keeps its own name unless the runtime owns it */
160
+ for (const name of model.schemaNames) {
161
+ const base = goExported(name)
162
+ schemaNames.set(name, scope.has(base) ? scope.claim(`${base}Model`) : scope.claim(base))
163
+ }
164
+ const hoistKeys = new Map<string, string>()
165
+ const types: GoTypeNames = {
166
+ claimConst: (base) => scope.claim(base),
167
+ cyclic: valueCycleSchemas(model.ir.schemas),
168
+ hoist: (key, base) => {
169
+ const hit = hoistKeys.get(key)
170
+ if (hit) return hit
171
+ const name = scope.claim(base)
172
+ hoistKeys.set(key, name)
173
+ return name
174
+ },
175
+ ref: (name) => schemaNames.get(name) ?? goExported(name),
176
+ resolve: model.resolve,
177
+ }
178
+ return { hoistedDecls: new Map(), scope, types }
198
179
  }
199
180
 
200
- /**
201
- * Produces a Go expression that converts a query-param value to `string`,
202
- * suitable as the 2nd argument of `url.Values.Set`. Nullable fields should
203
- * already be dereferenced by the caller — `varExpr` is the concrete value.
204
- */
205
- function qpStringExpr(varExpr: string, schema: Record<string, unknown>): string {
206
- if (isStringEnum(schema)) return `string(${varExpr})`
207
- const t = schema.type
208
- if (t === "integer") return `strconv.FormatInt(${varExpr}, 10)`
209
- if (t === "number") return `strconv.FormatFloat(${varExpr}, 'f', -1, 64)`
210
- if (t === "boolean") return `strconv.FormatBool(${varExpr})`
211
- return varExpr
212
- }
181
+ /* ── buildGoTypes ── */
213
182
 
214
- function queryParamsNeedStrconv(methods: MethodInfo[]): boolean {
215
- for (const m of methods) {
216
- for (const qp of m.queryParams) {
217
- const t = qp.schema.type
218
- if (t === "integer" || t === "number" || t === "boolean") return true
219
- }
183
+ function buildGoTypes(model: SdkModel, names: GoNames): string {
184
+ const body: string[] = []
185
+ const decls = names.hoistedDecls
186
+ for (const name of model.schemaNames) {
187
+ const ir = model.ir.schemas[name]
188
+ const goName = names.types.ref(name)
189
+ const description = Object.hasOwn(model.ir.schemaDescriptions, name) ? model.ir.schemaDescriptions[name] : ""
190
+ if (description) body.push(...goComment(description))
191
+ body.push(irRenderTopLevel(goName, ir, decls, undefined, names.types))
192
+ body.push(``)
220
193
  }
221
- return false
194
+ return body.join("\n")
222
195
  }
223
196
 
224
- function getQueryParams(op: Record<string, unknown>): QueryParam[] {
225
- const parameters = op.parameters as Array<Record<string, unknown>> | undefined
226
- if (!parameters) return []
227
- return parameters
228
- .filter((p) => p.in === "query")
229
- .map((p) => ({
230
- name: String(p.name),
231
- required: p.required === true,
232
- schema: (p.schema as Record<string, unknown>) ?? { type: "string" },
233
- }))
234
- .sort((a, b) => a.name.localeCompare(b.name))
197
+ function hoistedDeclsSource(decls: Map<string, string>): string {
198
+ const out: string[] = []
199
+ for (const name of [...decls.keys()].sort(cmpCodeUnit)) {
200
+ const decl = decls.get(name)
201
+ if (!decl) continue
202
+ out.push(decl)
203
+ out.push(``)
204
+ }
205
+ return out.join("\n")
235
206
  }
236
207
 
237
- /* ── buildGoTypes ── */
238
-
239
- /* Schema names defined in the Go runtime files — skip them to avoid redeclaration. */
240
- const GO_RUNTIME_TYPES = new Set([
241
- "APIError",
242
- "BadGatewayError",
243
- "BadRequestError",
244
- "Config",
245
- "ConflictError",
246
- "ConnectionState",
247
- "ForbiddenError",
248
- "GatewayTimeoutError",
249
- "InternalServerError",
250
- "InvalidationConfig",
251
- "LongpollTransport",
252
- "NotFoundError",
253
- "RateLimitError",
254
- "RequestMeta",
255
- "ResumableConnection",
256
- "SDKResult",
257
- "SSEEvent",
258
- "ServiceUnavailableError",
259
- "SseTransport",
260
- "StaleTracker",
261
- "StatusError",
262
- "StatusErrorMeta",
263
- "Transport",
264
- "TransportConn",
265
- "TransportKind",
266
- "TransportOpts",
267
- "TypedWebSocket",
268
- "UnauthorizedError",
269
- "UnprocessableEntityError",
270
- "WsTransport",
271
- ])
272
-
273
- function buildGoTypes(spec: OpenApiSpec): string {
274
- const schemas = spec.components?.schemas ?? {}
208
+ /** types.go: component schemas, then every hoisted declaration (call after the client is planned). */
209
+ function typesFile(schemaTypes: string, names: GoNames): string {
210
+ const body = `${schemaTypes}\n${hoistedDeclsSource(names.hoistedDecls)}`
275
211
  const l: string[] = []
276
212
  /* blank line after generator header detaches it from package-doc on pkg.go.dev */
277
213
  l.push(`// Code generated by honey. DO NOT EDIT.`)
278
214
  l.push(``)
279
215
  l.push(`package sdk`)
280
216
  l.push(``)
281
-
282
- const sortedSchemas = Object.entries(schemas)
283
- .filter(([name]) => !GO_RUNTIME_TYPES.has(name))
284
- .sort(([a], [b]) => a.localeCompare(b))
285
- const body: string[] = []
286
- const decls = new Map<string, string>()
287
- for (const [name, schema] of sortedSchemas) {
288
- const goType = renderTopLevel(name, schema, decls)
289
- body.push(goType)
290
- body.push(``)
291
- }
292
- /* append hoisted enum decls (deduped by name) after top-level types */
293
- const hoistedNames = [...decls.keys()].sort((a, b) => a.localeCompare(b))
294
- for (const hname of hoistedNames) {
295
- body.push(decls.get(hname) as string)
296
- body.push(``)
297
- }
298
-
299
- /* emit encoding/json import only when generated type bodies actually use it
300
- * (json.RawMessage, json.Unmarshal, etc). Keeps types.go compilable whether
301
- * schemas produced additionalProperties helpers or not. */
302
- const bodyStr = body.join("\n")
303
- if (bodyStr.includes("json.RawMessage") || bodyStr.includes("json.Unmarshal") || bodyStr.includes("json.Marshal")) {
217
+ if (/\bjson\./.test(body)) {
304
218
  l.push(`import "encoding/json"`)
305
219
  l.push(``)
306
220
  }
307
- l.push(bodyStr)
308
-
221
+ l.push(body)
309
222
  return l.join("\n")
310
223
  }
311
224
 
@@ -313,15 +226,14 @@ function buildGoTypes(spec: OpenApiSpec): string {
313
226
 
314
227
  function buildGoMod(modulePath?: string): string {
315
228
  const mod = modulePath ?? "example.com/sdk"
316
- return [`module ${mod}`, ``, `go 1.23`, ``, `require nhooyr.io/websocket v1.8.17`, ``].join("\n")
229
+ return [`module ${mod}`, ``, `go 1.24`, ``, `require nhooyr.io/websocket v1.8.17`, ``].join("\n")
317
230
  }
318
231
 
319
232
  /* ── buildGoDoc ── */
320
233
 
321
- function buildGoDoc(spec: OpenApiSpec): string {
322
- const title = spec.info.title
323
- const desc = spec.info.description
324
- const descSuffix = desc ? ` ${desc}` : ""
234
+ function buildGoDoc(info: IRInfo): string {
235
+ const title = info.title ?? "API"
236
+ const desc = info.description ?? ""
325
237
  /* Generator directive on its own line, separated from package-doc by blank
326
238
  * line so pkg.go.dev does not aggregate "Code generated..." into Overview.
327
239
  * The `// Package sdk ...` line IS attached to `package sdk` (no blank line
@@ -329,7 +241,7 @@ function buildGoDoc(spec: OpenApiSpec): string {
329
241
  const l: string[] = []
330
242
  l.push(`// Code generated by honey. DO NOT EDIT.`)
331
243
  l.push(``)
332
- l.push(`// Package sdk is an auto-generated client for ${title}.${descSuffix}`)
244
+ l.push(...goComment(`Package sdk is an auto-generated client for ${title}.${desc ? ` ${desc}` : ""}`))
333
245
  l.push(`package sdk`)
334
246
  l.push(``)
335
247
  return l.join("\n")
@@ -337,471 +249,389 @@ function buildGoDoc(spec: OpenApiSpec): string {
337
249
 
338
250
  /* ── buildGoClient ── */
339
251
 
340
- type MethodInfo = {
341
- /* first segment of operationId — used as invalidationMap key */
342
- resource: string
343
- /* segments[1..] joined with "." — used as invalidationMap action key */
344
- action: string
345
- /* all segments except last — namespace path for struct name generation */
346
- namePath: string[]
347
- /* last segment of operationId — used for the Go method name */
348
- methodAction: string
349
- bodyIsStream: boolean
350
- bodyType: string | null
351
- description: string
352
- httpMethod: string
353
- isIdempotent: boolean
354
- isRealtime: boolean
355
- isSSE: boolean
356
- isWS: boolean
357
- op: Record<string, unknown>
358
- opId: string
359
- path: string
360
- pathParams: string[]
361
- queryParams: QueryParam[]
362
- responseType: string
363
- summary: string
364
- /* Override for the Pascal-cased method/opts name. Set when a top-level
365
- * operation's resource name collides with an existing resource bucket,
366
- * so the emitted `Client` doesn't have a same-named field + method. */
367
- methodNameOverride?: string
368
- }
369
-
370
- /** Build a MethodInfo from an IROperation + the raw spec op object. */
371
- function methodInfoFromIROp(irOp: IROperation, rawOp: Record<string, unknown>): MethodInfo {
372
- const segments = irOp.id.split(".")
373
- const isTopLevel = segments.length === 1
374
- const resource = segments[0] ?? irOp.id
375
- const action = isTopLevel ? "_call" : segments.slice(1).join(".")
376
- const namePath = segments.slice(0, -1)
377
- const methodAction = segments[segments.length - 1] ?? irOp.id
378
- const isRealtime = rawOp["x-realtime"] === true
379
- return {
380
- action,
381
- bodyIsStream: hasOctetStreamBody(rawOp),
382
- bodyType: goBodyType(rawOp),
383
- description: String(rawOp.description ?? ""),
384
- httpMethod: irOp.method,
385
- isIdempotent: rawOp["x-idempotency-key"] === true,
386
- isRealtime,
387
- isSSE: !isRealtime && isSSEOperation(rawOp),
388
- isWS: !isRealtime && rawOp["x-websocket"] === true,
389
- methodAction,
390
- namePath,
391
- op: rawOp,
392
- opId: irOp.id,
393
- path: irOp.path,
394
- pathParams: irOp.params.path.map((p) => p.name),
395
- queryParams: getQueryParams(rawOp),
396
- resource,
397
- responseType: goResponseType(rawOp),
398
- summary: String(rawOp.summary ?? ""),
399
- }
400
- }
401
-
402
- /** Walk the IR tree and collect all MethodInfos, pairing with raw spec ops. */
403
- function collectMethodsFromIR(ns: IRNamespace, spec: OpenApiSpec): MethodInfo[] {
404
- const rawOpById = new Map<string, Record<string, unknown>>()
405
- for (const [, pathMethods] of Object.entries(spec.paths ?? {})) {
406
- for (const [, operation] of Object.entries(pathMethods)) {
407
- const op = operation as Record<string, unknown>
408
- const id = op.operationId as string | undefined
409
- if (id) rawOpById.set(id, op)
410
- }
411
- }
412
-
413
- const results: MethodInfo[] = []
414
-
415
- function walk(cur: IRNamespace): void {
416
- for (const [, entry] of methodsOf(cur)) {
417
- const raw = rawOpById.get(entry.id) ?? {}
418
- results.push(methodInfoFromIROp(entry, raw))
419
- }
420
- for (const [, childNs] of namespacesOf(cur)) {
421
- walk(childNs)
422
- }
423
- }
424
-
425
- walk(ns)
426
- return results
252
+ type GoOpPlan = {
253
+ op: SdkOp
254
+ methodName: string
255
+ optsName: string
256
+ /** opts struct field per query/header param */
257
+ optFields: Array<{ goName: string; type: string; param: { name: string }; where: "query" | "header" }>
258
+ /** local identifier per path param */
259
+ pathLocals: Array<{ local: string; name: string }>
260
+ bodyParam?: { local: string; type: string }
261
+ multipartName?: string
262
+ multipartFields?: Array<{
263
+ goName: string
264
+ type: string
265
+ key: string
266
+ file: boolean
267
+ list: boolean
268
+ required: boolean
269
+ }>
270
+ /** Go type returned on success, "" for none */
271
+ resultType: string
427
272
  }
428
273
 
429
- function buildGoClient(spec: OpenApiSpec, options: GoSDKOptions): string {
430
- const throwOnError = options.throwOnError ?? true
431
-
432
- /* toIR validates operationId collisions — throws before any codegen runs */
433
- const ir = toIR(spec as Parameters<typeof toIR>[0])
434
- const methods = collectMethodsFromIR(ir.tree, spec)
435
-
436
- /* top-level single-segment ops (action === "_call") emit as methods on *Client */
437
- const topLevel: MethodInfo[] = methods.filter((m) => m.action === "_call")
438
- /* collect root namespace names to detect field/method name collision */
439
- const rootNsNames = new Set(namespacesOf(ir.tree).map(([seg]) => safeResourceName(seg)))
440
-
441
- /* top-level op whose resource name collides with a root namespace field needs a
442
- * unique method name — Go forbids struct field + method sharing a name.
443
- * Suffix with "Call": extract + extract.table → Extract field + ExtractCall method. */
444
- for (const m of topLevel) {
445
- if (rootNsNames.has(safeResourceName(m.resource))) {
446
- m.methodNameOverride = `${goPascal(m.resource)}Call`
447
- }
448
- }
449
-
450
- const hasWS = methods.some((m) => m.isWS)
451
-
452
- /* collect invalidation data for map */
453
- const serviceMap = collectSDKMethods(spec as Parameters<typeof collectSDKMethods>[0]).serviceMap
454
-
455
- const body: string[] = []
456
-
457
- /* serviceEntry struct */
458
- body.push(`type serviceEntry struct {`)
459
- body.push(`\tMethod string`)
460
- body.push(`\tPath string`)
461
- body.push(`\tParams []string`)
462
- body.push(`\tSSE bool`)
463
- body.push(`\tWS bool`)
464
- body.push(`\tInvalidate []string`)
465
- body.push(`}`)
466
- body.push(``)
467
-
468
- /* invalidationMap */
469
- body.push(`var invalidationMap = map[string]map[string]serviceEntry{`)
470
- for (const [resource, actions] of Object.entries(serviceMap).sort(([a], [b]) => a.localeCompare(b))) {
471
- body.push(`\t"${resource}": {`)
472
- for (const [action, entry] of Object.entries(actions as Record<string, Record<string, unknown>>).sort(([a], [b]) =>
473
- a.localeCompare(b),
474
- )) {
475
- const inv = entry.invalidate as string[] | undefined
476
- const invStr = inv && inv.length > 0 ? `[]string{${inv.map((s) => JSON.stringify(s)).join(", ")}}` : `nil`
477
- const params = entry.params as string[] | undefined
478
- const paramsStr =
479
- params && params.length > 0 ? `[]string{${params.map((s) => JSON.stringify(s)).join(", ")}}` : `nil`
480
- body.push(
481
- `\t\t"${action}": {Method: ${JSON.stringify(String(entry.method))}, Path: ${JSON.stringify(String(entry.path))}, Params: ${paramsStr}, SSE: ${entry.sse === true}, WS: ${entry.ws === true}, Invalidate: ${invStr}},`,
482
- )
274
+ const OPTS_RESERVED = [
275
+ "Headers",
276
+ "IdempotencyKey",
277
+ "LastEventID",
278
+ "ReconnectToken",
279
+ "Protocols",
280
+ "MaxReconnectAttempts",
281
+ "ReconnectDelayMs",
282
+ ]
283
+
284
+ /** Locals every emitted method body uses. Path params must not shadow them. */
285
+ const METHOD_LOCALS = [
286
+ "ctx",
287
+ "opts",
288
+ "q",
289
+ "path",
290
+ "err",
291
+ "result",
292
+ "out",
293
+ "resp",
294
+ "req",
295
+ "reqBody",
296
+ "callHeaders",
297
+ "pathParams",
298
+ "concretePath",
299
+ "selector",
300
+ "requestMeta",
301
+ "stale",
302
+ "cfg",
303
+ "yield",
304
+ "ev",
305
+ "conn",
306
+ "dialOpts",
307
+ "wsURL",
308
+ "transports",
309
+ "topts",
310
+ "parts",
311
+ "c",
312
+ "r",
313
+ ]
314
+
315
+ function planOp(op: SdkOp, methodName: string, optsName: string, names: GoNames): GoOpPlan {
316
+ const plan: GoOpPlan = { methodName, op, optFields: [], optsName, pathLocals: [], resultType: "" }
317
+
318
+ const locals = new NameScope(METHOD_LOCALS)
319
+ const bodyLocal =
320
+ op.body && op.stream !== "ws" && op.stream !== "realtime"
321
+ ? locals.claim(op.body.kind === "form" ? "form" : "body")
322
+ : ""
323
+ for (const p of op.pathParams) plan.pathLocals.push({ local: locals.claim(goLocal(p.name)), name: p.name })
324
+
325
+ if (op.stream !== "realtime") {
326
+ const fieldScope = new NameScope(OPTS_RESERVED)
327
+ const decls = names.hoistedDecls
328
+ const params: Array<[{ name: string; schema: IRSchema; required?: boolean }, "query" | "header"]> = [
329
+ ...op.query.map((p): [typeof p, "query"] => [p, "query"]),
330
+ ...op.headers.map((p): [typeof p, "header"] => [p, "header"]),
331
+ ]
332
+ for (const [p, where] of params) {
333
+ if (op.stream === "sse" && where === "query" && /^last[-_]?event[-_]?id$/i.test(p.name)) continue
334
+ const goName = fieldScope.claim(goExported(p.name))
335
+ const ctx: RenderUseCtx = { decls, fieldName: goName, names: names.types, parentName: optsName }
336
+ const t = irRenderUse(p.schema, ctx)
337
+ const nilable = t.startsWith("*") || t.startsWith("[]") || t.startsWith("map[") || t === "json.RawMessage"
338
+ plan.optFields.push({ goName, param: p, type: p.required || nilable ? t : `*${t}`, where })
483
339
  }
484
- body.push(`\t},`)
485
340
  }
486
- body.push(`}`)
487
- body.push(``)
488
341
 
489
- /* ---- Client struct: cfg + stale + one field per root namespace ---- */
490
- body.push(`// Client is the generated SDK client.`)
491
- body.push(`type Client struct {`)
492
- body.push(`\tcfg Config`)
493
- body.push(`\tstale *StaleTracker`)
494
- for (const [seg] of namespacesOf(ir.tree)) {
495
- const safeName = safeResourceName(seg)
496
- body.push(`\t${goPascal(safeName)} *${nestedStructName([safeName])}`)
497
- }
498
- body.push(`}`)
499
- body.push(``)
500
-
501
- /* ---- NewClient: init + cascade nested struct init ---- */
502
- const auth = detectAuthScheme(spec)
503
- body.push(`// NewClient creates a new SDK client. BaseURL must be non-empty.`)
504
- body.push(`func NewClient(cfg Config) *Client {`)
505
- body.push(`\tif cfg.BaseURL == "" {`)
506
- body.push(`\t\tpanic("honey sdk: Config.BaseURL must not be empty")`)
507
- body.push(`\t}`)
508
- body.push(`\tif cfg.HTTPClient == nil {`)
509
- body.push(`\t\tcfg.HTTPClient = &http.Client{Timeout: 30 * time.Second}`)
510
- body.push(`\t}`)
511
- body.push(`\tif cfg.AuthHeaderName == "" {`)
512
- body.push(`\t\tcfg.AuthHeaderName = ${JSON.stringify(auth.headerName)}`)
513
- body.push(`\t}`)
514
- body.push(`\tif cfg.AuthHeaderPrefix == "" {`)
515
- body.push(`\t\tcfg.AuthHeaderPrefix = ${JSON.stringify(auth.prefix)}`)
516
- body.push(`\t}`)
517
- body.push(`\tc := &Client{cfg: cfg}`)
518
- body.push(`\tc.stale = NewStaleTracker(cfg.Invalidation)`)
519
- /* emit a helper fn per root namespace that inits itself + its children,
520
- * then assign: c.Checkout = newCheckoutResource(c) */
521
- for (const [seg] of namespacesOf(ir.tree)) {
522
- const safeName = safeResourceName(seg)
523
- body.push(`\tc.${goPascal(safeName)} = new${nestedStructName([safeName])}(c)`)
524
- }
525
- body.push(`\treturn c`)
526
- body.push(`}`)
527
- body.push(``)
528
-
529
- /* IsStale predicate */
530
- body.push(`// IsStale reports whether (method, path) currently sits inside an active stale window.`)
531
- body.push(`func (c *Client) IsStale(method, path string) bool {`)
532
- body.push(`\tif c.stale == nil { return false }`)
533
- body.push(`\treturn c.stale.IsStale(method, path)`)
534
- body.push(`}`)
535
- body.push(``)
536
-
537
- /* top-level single-segment methods on *Client */
538
- for (const m of topLevel.sort((a, b) => a.resource.localeCompare(b.resource))) {
539
- body.push(...emitMethod(m, "c", "*Client", throwOnError))
540
- }
541
-
542
- /* ---- recursive resource struct + method emission ---- */
543
- function emitResourceStructs(ns: IRNamespace, path: string[]): void {
544
- const structName = nestedStructName(path)
545
- const recvName = path[path.length - 1]?.[0]?.toLowerCase() ?? "r"
546
-
547
- /* struct definition: client pointer + one field per child namespace */
548
- body.push(`type ${structName} struct {`)
549
- body.push(`\tclient *Client`)
550
- for (const [childSeg] of namespacesOf(ns)) {
551
- const safeSeg = safeResourceName(childSeg)
552
- body.push(`\t${goPascal(safeSeg)} *${nestedStructName([...path, safeSeg])}`)
553
- }
554
- body.push(`}`)
555
- body.push(``)
556
-
557
- /* constructor helper: newXResource(c) sets client + child fields */
558
- body.push(`func new${structName}(c *Client) *${structName} {`)
559
- body.push(`\tr := &${structName}{client: c}`)
560
- for (const [childSeg] of namespacesOf(ns)) {
561
- const safeSeg = safeResourceName(childSeg)
562
- body.push(`\tr.${goPascal(safeSeg)} = new${nestedStructName([...path, safeSeg])}(c)`)
563
- }
564
- body.push(`\treturn r`)
565
- body.push(`}`)
566
- body.push(``)
567
-
568
- /* direct methods on this struct */
569
- for (const [, op] of methodsOf(ns)) {
570
- const raw =
571
- methods.find((m) => m.opId === op.id) ??
572
- (() => {
573
- throw new Error(`missing MethodInfo for ${op.id}`)
574
- })()
575
- body.push(...emitMethod(raw, recvName, `*${structName}`, throwOnError))
576
- }
577
-
578
- /* recurse into child namespaces */
579
- for (const [childSeg, childNs] of namespacesOf(ns)) {
580
- emitResourceStructs(childNs, [...path, safeResourceName(childSeg)])
342
+ const body = op.body
343
+ if (body && op.stream !== "ws" && op.stream !== "realtime") {
344
+ const local = bodyLocal
345
+ switch (body.kind) {
346
+ case "json": {
347
+ const resolved = body.schema
348
+ let t: string
349
+ if (resolved.kind === "ref") t = names.types.ref(resolved.name)
350
+ else if (resolved.kind === "object" && resolved.fields.length > 0) {
351
+ const name = names.types.hoist(`${op.id}.body`, `${optsName.replace(/Opts$/, "")}Body`)
352
+ if (!names.hoistedDecls.has(name)) {
353
+ names.hoistedDecls.set(name, "")
354
+ names.hoistedDecls.set(name, irRenderTopLevel(name, resolved, names.hoistedDecls, undefined, names.types))
355
+ }
356
+ t = name
357
+ } else {
358
+ t = irRenderUse(resolved, {
359
+ decls: names.hoistedDecls,
360
+ fieldName: "Body",
361
+ names: names.types,
362
+ parentName: optsName.replace(/Opts$/, ""),
363
+ })
364
+ }
365
+ plan.bodyParam = { local, type: t }
366
+ break
367
+ }
368
+ case "form":
369
+ plan.bodyParam = { local, type: "url.Values" }
370
+ break
371
+ case "binary":
372
+ plan.bodyParam = { local, type: "io.Reader" }
373
+ break
374
+ case "text":
375
+ plan.bodyParam = { local, type: "string" }
376
+ break
377
+ case "multipart": {
378
+ const name = names.types.hoist(`${op.id}.multipart`, `${optsName.replace(/Opts$/, "")}Body`)
379
+ const fieldScope = new NameScope()
380
+ plan.multipartName = name
381
+ plan.multipartFields = body.parts.map((part) => {
382
+ const goName = fieldScope.claim(goExported(part.name))
383
+ const schema = part.schema ? names.types.resolve(part.schema) : { kind: "unknown" as const }
384
+ const list = schema.kind === "array"
385
+ if (part.type === "file") {
386
+ return { file: true, goName, key: part.name, list, required: true, type: list ? "[]FilePart" : "FilePart" }
387
+ }
388
+ const item = list && schema.kind === "array" ? schema.items : schema
389
+ let t = "string"
390
+ if (item.kind === "scalar") {
391
+ t =
392
+ item.type === "integer"
393
+ ? "int64"
394
+ : item.type === "number"
395
+ ? "float64"
396
+ : item.type === "boolean"
397
+ ? "bool"
398
+ : "string"
399
+ }
400
+ return { file: false, goName, key: part.name, list, required: false, type: list ? `[]${t}` : `*${t}` }
401
+ })
402
+ plan.bodyParam = { local, type: name }
403
+ break
404
+ }
581
405
  }
582
406
  }
583
407
 
584
- for (const [seg, ns] of namespacesOf(ir.tree)) {
585
- emitResourceStructs(ns, [safeResourceName(seg)])
586
- }
587
-
588
- /* opts structs — emit once per unique opts name, sorted by opId */
589
- const optsEmitted = new Set<string>()
590
- for (const m of methods.sort((a, b) => a.opId.localeCompare(b.opId))) {
591
- const optsName = optsStructName(m)
592
- if (optsEmitted.has(optsName)) continue
593
- optsEmitted.add(optsName)
594
- body.push(...emitOptsStruct(m))
595
- }
596
-
597
- const bodyStr = body.join("\n")
598
-
599
- const hasSSE = methods.some((m) => m.isSSE)
600
- const stdlib = ["context", "encoding/json", "fmt", "io", "net/http", "net/url", "strings", "sync", "time"]
601
- if (hasSSE) stdlib.push("iter")
602
- if (queryParamsNeedStrconv(methods)) stdlib.push("strconv")
603
- const thirdParty = hasWS ? ["nhooyr.io/websocket"] : []
604
-
605
- /* emit a `var _ = <symbol>` suppression only for stdlib imports the body
606
- * does not already reference, so unused-import compile errors are avoided
607
- * without polluting generated code with dead suppressions. */
608
- const suppressions: { line: string; symbol: string }[] = [
609
- { line: `var _ json.RawMessage`, symbol: "json." },
610
- { line: `var _ = fmt.Sprintf`, symbol: "fmt." },
611
- { line: `var _ = io.Discard`, symbol: "io." },
612
- { line: `var _ *url.URL`, symbol: "url." },
613
- { line: `var _ = strings.NewReader`, symbol: "strings." },
614
- { line: `var _ = sync.Mutex{}`, symbol: "sync." },
615
- { line: `var _ = time.Second`, symbol: "time." },
616
- ]
617
- const suppressionLines = suppressions.filter((s) => !bodyStr.includes(s.symbol)).map((s) => s.line)
618
-
619
- const out: string[] = []
620
- /* blank line after generator header detaches it from package-doc on pkg.go.dev */
621
- out.push(`// Code generated by honey. DO NOT EDIT.`)
622
- out.push(``)
623
- out.push(`package sdk`)
624
- out.push(``)
625
- out.push(goImportBlock([...new Set(stdlib)].sort(), thirdParty))
626
- out.push(``)
627
- if (suppressionLines.length > 0) {
628
- out.push(...suppressionLines)
629
- out.push(``)
408
+ if (op.stream === null) {
409
+ const s = op.success
410
+ if (s.kind === "json") {
411
+ if (!s.schema) plan.resultType = "json.RawMessage"
412
+ else if (s.schema.kind === "ref") plan.resultType = names.types.ref(s.schema.name)
413
+ else if (s.schema.kind === "object" && s.schema.fields.length > 0) {
414
+ const name = names.types.hoist(`${op.id}.response`, `${optsName.replace(/Opts$/, "")}Response`)
415
+ if (!names.hoistedDecls.has(name)) {
416
+ names.hoistedDecls.set(name, "")
417
+ names.hoistedDecls.set(name, irRenderTopLevel(name, s.schema, names.hoistedDecls, undefined, names.types))
418
+ }
419
+ plan.resultType = name
420
+ } else {
421
+ const t = irRenderUse(s.schema, {
422
+ decls: names.hoistedDecls,
423
+ fieldName: "Response",
424
+ names: names.types,
425
+ parentName: optsName.replace(/Opts$/, ""),
426
+ })
427
+ plan.resultType = t.startsWith("*") ? t.slice(1) : t
428
+ }
429
+ } else if (s.kind === "text") plan.resultType = "string"
430
+ else if (s.kind === "binary") plan.resultType = "[]byte"
630
431
  }
631
- out.push(bodyStr)
632
- return out.join("\n")
633
- }
634
-
635
- function optsStructName(m: MethodInfo): string {
636
- if (m.methodNameOverride) return `${m.methodNameOverride}Opts`
637
- /* For top-level single-segment ops: namePath=[], methodAction=opId → e.g. GetUserOpts
638
- * For multi-segment: namePath=[checkout,sessions], methodAction=create → CheckoutSessionsCreateOpts */
639
- const prefix = m.namePath.map(goPascal).join("")
640
- const suffix = goPascal(m.methodAction)
641
- return `${prefix}${suffix}Opts`
432
+ return plan
642
433
  }
643
434
 
644
- function emitOptsStruct(m: MethodInfo): string[] {
435
+ function emitOptsStruct(plan: GoOpPlan): string[] {
645
436
  const l: string[] = []
646
- const optsName = optsStructName(m)
647
- if (m.isRealtime) {
648
- /* realtime ops get a fixed opts shape — no generated query fields;
649
- * transports receive ReconnectToken / LastEventID via TransportOpts. */
437
+ const { op, optsName } = plan
438
+ l.push(`// ${optsName} holds the optional inputs of ${plan.methodName}.`)
439
+ if (op.stream === "realtime") {
440
+ /* realtime ops get a fixed opts shape — transports receive
441
+ * ReconnectToken / LastEventID via TransportOpts. */
650
442
  l.push(`type ${optsName} struct {`)
651
443
  l.push(`\tReconnectToken string`)
652
444
  l.push(`\tLastEventID string`)
653
445
  l.push(`\tProtocols []string`)
446
+ l.push(`\t// MaxReconnectAttempts caps reconnects after a drop. Zero means 5; negative means unlimited.`)
654
447
  l.push(`\tMaxReconnectAttempts int`)
448
+ l.push(`\t// ReconnectDelayMs is the first backoff delay. Zero means 1000.`)
655
449
  l.push(`\tReconnectDelayMs int`)
656
450
  l.push(`\tHeaders map[string]string`)
657
451
  l.push(`}`)
658
452
  l.push(``)
659
453
  return l
660
454
  }
661
- const decls = new Map<string, string>()
662
- const fields: string[] = []
663
- for (const qp of m.queryParams) {
664
- const fieldName = goPascal(qp.name)
665
- const goType = renderUse(qp.schema, {
666
- decls,
667
- fieldName: qp.name,
668
- parentName: optsName,
669
- })
670
- const ptr = qp.required ? goType : `*${goType}`
671
- fields.push(`\t${fieldName} ${ptr}`)
672
- }
673
- /* hoisted query-param enums emit before the opts struct so refs resolve */
674
- for (const decl of decls.values()) {
675
- l.push(decl)
676
- l.push(``)
677
- }
678
455
  l.push(`type ${optsName} struct {`)
679
- l.push(...fields)
680
- if (m.isSSE) {
681
- l.push(`\tLastEventID string`)
682
- }
683
- if (m.isWS) {
456
+ for (const f of plan.optFields) l.push(`\t${f.goName} ${f.type}`)
457
+ if (op.stream === "sse") l.push(`\tLastEventID string`)
458
+ if (op.stream === "ws") {
684
459
  l.push(`\tReconnectToken string`)
685
460
  l.push(`\tProtocols []string`)
686
461
  }
687
- if (m.isIdempotent && !m.isRealtime && !m.isSSE && !m.isWS) {
688
- l.push(`\tIdempotencyKey string`)
689
- }
462
+ if (op.idempotent && op.stream === null) l.push(`\tIdempotencyKey string`)
690
463
  l.push(`\tHeaders map[string]string`)
691
464
  l.push(`}`)
692
465
  l.push(``)
693
466
  return l
694
467
  }
695
468
 
696
- function emitMethod(m: MethodInfo, recv: string, recvType: string, throwOnError: boolean): string[] {
469
+ function emitMultipartStruct(plan: GoOpPlan): string[] {
470
+ if (!plan.multipartName || !plan.multipartFields) return []
697
471
  const l: string[] = []
698
- /* top-level single-segment ops (action === "_call") use resource as the method name */
699
- const actionPascal = m.methodNameOverride ?? goPascal(m.action === "_call" ? m.resource : m.methodAction)
700
- const optsName = optsStructName(m)
701
- const httpMethod = m.httpMethod
702
- const pathStr = m.path
703
-
704
- /* godoc */
705
- if (m.summary) {
706
- l.push(`// ${actionPascal} — ${m.summary}`)
707
- if (m.description) l.push(`// ${m.description}`)
472
+ l.push(`// ${plan.multipartName} is the multipart/form-data body of ${plan.methodName}.`)
473
+ l.push(`type ${plan.multipartName} struct {`)
474
+ for (const f of plan.multipartFields) l.push(`\t${f.goName} ${f.type}`)
475
+ l.push(`}`)
476
+ l.push(``)
477
+ return l
478
+ }
479
+
480
+ function pathExpr(plan: GoOpPlan): string {
481
+ if (plan.pathLocals.length === 0) return ""
482
+ return `map[string]string{${plan.pathLocals.map((p) => `${goString(p.name)}: ${p.local}`).join(", ")}}`
483
+ }
484
+
485
+ /** Lines that compute `path` (and return on error through `fail`). */
486
+ function emitPath(plan: GoOpPlan, fail: string, indent: string): string[] {
487
+ const l: string[] = []
488
+ if (plan.pathLocals.length === 0) {
489
+ l.push(`${indent}path := ${goString(plan.op.path)}`)
490
+ return l
708
491
  }
492
+ l.push(`${indent}path, err := expandPath(${goString(plan.op.path)}, ${pathExpr(plan)})`)
493
+ l.push(`${indent}if err != nil {`)
494
+ l.push(`${indent}\t${fail}`)
495
+ l.push(`${indent}}`)
496
+ return l
497
+ }
709
498
 
710
- /* signature */
711
- const params: string[] = [`ctx context.Context`]
712
- for (const pp of m.pathParams) params.push(`${pp} string`)
713
- if (m.bodyIsStream) params.push(`body io.Reader`)
714
- else if (m.bodyType) params.push(`body ${m.bodyType}`)
499
+ /** Lines that build `q` and `callHeaders` from opts. */
500
+ function emitQueryAndHeaders(plan: GoOpPlan, indent: string, baseHeaders: string): string[] {
501
+ const l: string[] = []
502
+ l.push(`${indent}var q queryList`)
503
+ l.push(`${indent}callHeaders := ${baseHeaders}`)
504
+ l.push(`${indent}if opts != nil {`)
505
+ for (const f of plan.optFields) {
506
+ if (f.where === "query") l.push(`${indent}\tq.set(${goString(f.param.name)}, opts.${f.goName})`)
507
+ }
508
+ l.push(`${indent}\tfor k, v := range opts.Headers {`)
509
+ l.push(`${indent}\t\tcallHeaders[k] = v`)
510
+ l.push(`${indent}\t}`)
511
+ for (const f of plan.optFields) {
512
+ if (f.where === "header")
513
+ l.push(`${indent}\tsetHeaderValue(callHeaders, ${goString(f.param.name)}, opts.${f.goName})`)
514
+ }
515
+ l.push(`${indent}}`)
516
+ return l
517
+ }
715
518
 
716
- let ret: string
717
- if (m.isRealtime) {
718
- params.push(`opts *${optsName}`)
719
- ret = `*ResumableConnection`
720
- } else if (m.isSSE) {
721
- params.push(`opts *${optsName}`)
722
- ret = `iter.Seq2[SSEEvent, error]`
723
- } else if (m.isWS) {
724
- params.push(`opts *${optsName}`)
725
- ret = `(*TypedWebSocket, error)`
726
- } else if (throwOnError) {
727
- params.push(`opts *${optsName}`)
728
- ret = m.responseType !== "" ? `(*${m.responseType}, error)` : `error`
729
- } else {
730
- params.push(`opts *${optsName}`)
731
- ret = m.responseType !== "" ? `(SDKResult[${m.responseType}], error)` : `error`
519
+ /** Expression for the request body, plus setup lines. */
520
+ function emitBody(plan: GoOpPlan, fail: string, indent: string): { lines: string[]; expr: string } {
521
+ const body = plan.op.body
522
+ const local = plan.bodyParam?.local
523
+ if (!body || !local) return { expr: "noBody()", lines: [] }
524
+ switch (body.kind) {
525
+ case "json":
526
+ return { expr: `jsonBody(${local})`, lines: [] }
527
+ case "form":
528
+ return { expr: `formBody(${local})`, lines: [] }
529
+ case "text":
530
+ return { expr: `rawBody([]byte(${local}), ${goString(body.contentType)})`, lines: [] }
531
+ case "binary":
532
+ return { expr: `readerBody(${local}, ${goString(body.contentType)})`, lines: [] }
533
+ case "multipart": {
534
+ const l: string[] = []
535
+ l.push(`${indent}var parts []multipartField`)
536
+ for (const f of plan.multipartFields ?? []) {
537
+ if (f.file) {
538
+ if (f.list) {
539
+ l.push(`${indent}for _, file := range ${local}.${f.goName} {`)
540
+ l.push(`${indent}\tparts = append(parts, fileField(${goString(f.key)}, file))`)
541
+ l.push(`${indent}}`)
542
+ } else {
543
+ l.push(`${indent}parts = append(parts, fileField(${goString(f.key)}, ${local}.${f.goName}))`)
544
+ }
545
+ } else if (f.list) {
546
+ l.push(`${indent}for _, v := range ${local}.${f.goName} {`)
547
+ l.push(`${indent}\tparts = append(parts, textField(${goString(f.key)}, v))`)
548
+ l.push(`${indent}}`)
549
+ } else {
550
+ l.push(`${indent}if ${local}.${f.goName} != nil {`)
551
+ l.push(`${indent}\tparts = append(parts, textField(${goString(f.key)}, *${local}.${f.goName}))`)
552
+ l.push(`${indent}}`)
553
+ }
554
+ }
555
+ l.push(`${indent}reqBody, err := multipartBody(parts)`)
556
+ l.push(`${indent}if err != nil {`)
557
+ l.push(`${indent}\t${fail}`)
558
+ l.push(`${indent}}`)
559
+ return { expr: "reqBody", lines: l }
560
+ }
561
+ }
562
+ }
563
+
564
+ function emitDoc(plan: GoOpPlan): string[] {
565
+ const l: string[] = []
566
+ const { op } = plan
567
+ const text = [op.summary, op.description].filter((s) => s !== "").join("\n")
568
+ if (text) l.push(...goComment(`${plan.methodName} — ${text}`))
569
+ if (op.deprecated) {
570
+ if (text) l.push(`//`)
571
+ l.push(`// Deprecated: the API marks this operation deprecated.`)
732
572
  }
573
+ return l
574
+ }
733
575
 
734
- l.push(`func (${recv} ${recvType}) ${actionPascal}(${params.join(", ")}) ${ret} {`)
576
+ function emitMethod(plan: GoOpPlan, recv: string, recvType: string, throwOnError: boolean): string[] {
577
+ const l: string[] = []
578
+ const { op, optsName } = plan
579
+ const cfg = recvType === "*Client" ? `${recv}.cfg` : `${recv}.client.cfg`
580
+ const stale = recvType === "*Client" ? `${recv}.stale` : `${recv}.client.stale`
735
581
 
736
- const httpMethodStr = httpMethod.toUpperCase()
582
+ l.push(...emitDoc(plan))
737
583
 
738
- /* client accessor */
739
- const cfgAccess = recvType === "*Client" ? `${recv}.cfg` : `${recv}.client.cfg`
740
- const staleAccess = recvType === "*Client" ? `${recv}.stale` : `${recv}.client.stale`
584
+ const params: string[] = [`ctx context.Context`]
585
+ for (const p of plan.pathLocals) params.push(`${p.local} string`)
586
+ if (plan.bodyParam) params.push(`${plan.bodyParam.local} ${plan.bodyParam.type}`)
587
+ params.push(`opts *${optsName}`)
741
588
 
742
- if (m.isRealtime) {
743
- const urlExpr = pathToGoExpr(pathStr)
744
- /* ctx is accepted for API symmetry with sibling methods but not used
745
- * here — consumer binds a ctx at ResumableConnection.Connect(ctx) time. */
589
+ if (op.stream === "realtime") {
590
+ l.push(`func (${recv} ${recvType}) ${plan.methodName}(${params.join(", ")}) (*ResumableConnection, error) {`)
591
+ /* ctx bounds nothing here: ResumableConnection.Connect(ctx) takes its own */
746
592
  l.push(`\t_ = ctx`)
747
- l.push(`\ttransports := []Transport{&WsTransport{}, &SseTransport{}, &LongpollTransport{}}`)
748
- l.push(`\ttopts := &TransportOpts{}`)
593
+ l.push(...emitPath(plan, "return nil, err", "\t"))
594
+ l.push(`\trtURL, err := buildURL(${cfg}.BaseURL, path, nil)`)
595
+ l.push(`\tif err != nil {`)
596
+ l.push(`\t\treturn nil, err`)
597
+ l.push(`\t}`)
598
+ l.push(`\tvar extra map[string]string`)
599
+ l.push(`\ttopts := &TransportOpts{HTTPClient: streamClient(${cfg})}`)
749
600
  l.push(`\tif opts != nil {`)
601
+ l.push(`\t\textra = opts.Headers`)
750
602
  l.push(`\t\ttopts.ReconnectToken = opts.ReconnectToken`)
751
603
  l.push(`\t\ttopts.LastEventID = opts.LastEventID`)
752
604
  l.push(`\t\ttopts.Protocols = opts.Protocols`)
753
605
  l.push(`\t\ttopts.MaxReconnectAttempts = opts.MaxReconnectAttempts`)
754
606
  l.push(`\t\ttopts.ReconnectDelayMs = opts.ReconnectDelayMs`)
755
- l.push(`\t\tif len(opts.Headers) > 0 {`)
756
- l.push(`\t\t\ttopts.Headers = http.Header{}`)
757
- l.push(`\t\t\tfor k, v := range opts.Headers { topts.Headers.Set(k, v) }`)
758
- l.push(`\t\t}`)
759
607
  l.push(`\t}`)
760
- l.push(`\treturn NewResumableConnection(${cfgAccess}.BaseURL + ${urlExpr}, transports, topts)`)
608
+ l.push(`\ttopts.Headers = authHeaders(${cfg}, extra)`)
609
+ l.push(`\ttransports := []Transport{&WsTransport{}, &SseTransport{}, &LongpollTransport{}}`)
610
+ l.push(`\treturn NewResumableConnection(rtURL, transports, topts), nil`)
761
611
  l.push(`}`)
762
612
  l.push(``)
763
613
  return l
764
614
  }
765
615
 
766
- if (m.isSSE) {
767
- const sseQueryParams = m.queryParams.filter((qp) => qp.name !== "last-event-id")
616
+ if (op.stream === "sse") {
617
+ l.push(`func (${recv} ${recvType}) ${plan.methodName}(${params.join(", ")}) iter.Seq2[SSEEvent, error] {`)
768
618
  l.push(`\treturn func(yield func(SSEEvent, error) bool) {`)
769
- if (sseQueryParams.length > 0) {
770
- l.push(`\t\tq := url.Values{}`)
771
- for (const qp of sseQueryParams) {
772
- const fieldName = goPascal(qp.name)
773
- if (qp.required) {
774
- l.push(
775
- `\t\tif opts != nil { q.Set(${JSON.stringify(qp.name)}, ${qpStringExpr(`opts.${fieldName}`, qp.schema)}) }`,
776
- )
777
- } else {
778
- l.push(
779
- `\t\tif opts != nil && opts.${fieldName} != nil { q.Set(${JSON.stringify(qp.name)}, ${qpStringExpr(`*opts.${fieldName}`, qp.schema)}) }`,
780
- )
781
- }
782
- }
783
- }
784
- const urlExpr = pathToGoExpr(pathStr)
785
- const sseURLExpr =
786
- sseQueryParams.length > 0 ? `${cfgAccess}.BaseURL+${urlExpr}+"?"+q.Encode()` : `${cfgAccess}.BaseURL+${urlExpr}`
787
- l.push(`\t\tcallHeaders := map[string]string{"Accept": "text/event-stream"}`)
619
+ l.push(...emitPath(plan, "yield(SSEEvent{}, err)\n\t\t\treturn", "\t\t"))
620
+ l.push(...emitQueryAndHeaders(plan, "\t\t", `map[string]string{"Accept": "text/event-stream"}`))
788
621
  l.push(`\t\tif opts != nil && opts.LastEventID != "" {`)
789
622
  l.push(`\t\t\tcallHeaders["Last-Event-ID"] = opts.LastEventID`)
790
623
  l.push(`\t\t}`)
791
- l.push(`\t\tif opts != nil {`)
792
- l.push(`\t\t\tfor k, v := range opts.Headers { callHeaders[k] = v }`)
624
+ const body = emitBody(plan, "yield(SSEEvent{}, err)\n\t\t\treturn", "\t\t")
625
+ l.push(...body.lines)
626
+ l.push(`\t\tresp, err := openStream(ctx, ${cfg}, ${goString(op.method)}, path, q, ${body.expr}, callHeaders)`)
627
+ l.push(`\t\tif err != nil {`)
628
+ l.push(`\t\t\tyield(SSEEvent{}, err)`)
629
+ l.push(`\t\t\treturn`)
793
630
  l.push(`\t\t}`)
794
- l.push(`\t\treq, err := http.NewRequestWithContext(ctx, "GET", ${sseURLExpr}, nil)`)
795
- l.push(`\t\tif err != nil { yield(SSEEvent{}, err); return }`)
796
- l.push(`\t\tfor k, v := range ${cfgAccess}.Headers { req.Header.Set(k, v) }`)
797
- l.push(`\t\tfor k, v := range callHeaders { req.Header.Set(k, v) }`)
798
- l.push(
799
- `\t\tif ${cfgAccess}.BearerToken != "" { req.Header.Set(${cfgAccess}.AuthHeaderName, ${cfgAccess}.AuthHeaderPrefix+${cfgAccess}.BearerToken) }`,
800
- )
801
- l.push(`\t\tresp, err := ${cfgAccess}.HTTPClient.Do(req)`)
802
- l.push(`\t\tif err != nil { yield(SSEEvent{}, err); return }`)
803
631
  l.push(`\t\tfor ev, err := range parseSSEStream(ctx, resp) {`)
804
- l.push(`\t\t\tif !yield(ev, err) { return }`)
632
+ l.push(`\t\t\tif !yield(ev, err) {`)
633
+ l.push(`\t\t\t\treturn`)
634
+ l.push(`\t\t\t}`)
805
635
  l.push(`\t\t}`)
806
636
  l.push(`\t}`)
807
637
  l.push(`}`)
@@ -809,180 +639,290 @@ function emitMethod(m: MethodInfo, recv: string, recvType: string, throwOnError:
809
639
  return l
810
640
  }
811
641
 
812
- if (m.isWS) {
813
- const urlExpr = pathToGoExpr(pathStr)
814
- l.push(`\tq := url.Values{}`)
815
- for (const qp of m.queryParams) {
816
- if (qp.name === "reconnect_token") continue
817
- const fieldName = goPascal(qp.name)
818
- if (qp.required) {
819
- l.push(
820
- `\tif opts != nil { q.Set(${JSON.stringify(qp.name)}, ${qpStringExpr(`opts.${fieldName}`, qp.schema)}) }`,
821
- )
822
- } else {
823
- l.push(
824
- `\tif opts != nil && opts.${fieldName} != nil { q.Set(${JSON.stringify(qp.name)}, ${qpStringExpr(`*opts.${fieldName}`, qp.schema)}) }`,
825
- )
826
- }
827
- }
828
- l.push(`\tif opts != nil && opts.ReconnectToken != "" { q.Set("reconnect_token", opts.ReconnectToken) }`)
829
- l.push(`\twsURL := ${cfgAccess}.BaseURL + ${urlExpr}`)
830
- l.push(`\tif len(q) > 0 { wsURL += "?" + q.Encode() }`)
831
- l.push(`\tdialOpts := &websocket.DialOptions{}`)
832
- l.push(`\tif opts != nil && len(opts.Protocols) > 0 { dialOpts.Subprotocols = opts.Protocols }`)
833
- l.push(`\tif opts != nil && len(opts.Headers) > 0 {`)
834
- l.push(`\t\tdialOpts.HTTPHeader = http.Header{}`)
835
- l.push(`\t\tfor k, v := range opts.Headers { dialOpts.HTTPHeader.Set(k, v) }`)
642
+ if (op.stream === "ws") {
643
+ l.push(`func (${recv} ${recvType}) ${plan.methodName}(${params.join(", ")}) (*TypedWebSocket, error) {`)
644
+ l.push(...emitPath(plan, "return nil, err", "\t"))
645
+ l.push(...emitQueryAndHeaders(plan, "\t", "map[string]string{}"))
646
+ l.push(`\tif opts != nil && opts.ReconnectToken != "" {`)
647
+ l.push(`\t\tq.set("reconnect_token", opts.ReconnectToken)`)
648
+ l.push(`\t}`)
649
+ l.push(`\twsURL, err := buildURL(${cfg}.BaseURL, path, q)`)
650
+ l.push(`\tif err != nil {`)
651
+ l.push(`\t\treturn nil, err`)
652
+ l.push(`\t}`)
653
+ l.push(
654
+ `\tdialOpts := &websocket.DialOptions{HTTPClient: streamClient(${cfg}), HTTPHeader: authHeaders(${cfg}, callHeaders)}`,
655
+ )
656
+ l.push(`\tif opts != nil && len(opts.Protocols) > 0 {`)
657
+ l.push(`\t\tdialOpts.Subprotocols = opts.Protocols`)
658
+ l.push(`\t}`)
659
+ l.push(`\tconn, _, err := websocket.Dial(ctx, toWsURL(wsURL), dialOpts)`)
660
+ l.push(`\tif err != nil {`)
661
+ l.push(`\t\treturn nil, err`)
836
662
  l.push(`\t}`)
837
- l.push(`\tconn, _, err := websocket.Dial(ctx, wsURL, dialOpts)`)
838
- l.push(`\tif err != nil { return nil, err }`)
839
663
  l.push(`\treturn newTypedWebSocket(conn), nil`)
840
664
  l.push(`}`)
841
665
  l.push(``)
842
666
  return l
843
667
  }
844
668
 
845
- /* HTTP method */
846
- l.push(`\tq := url.Values{}`)
847
- for (const qp of m.queryParams) {
848
- const fieldName = goPascal(qp.name)
849
- if (qp.required) {
850
- l.push(`\tif opts != nil { q.Set(${JSON.stringify(qp.name)}, ${qpStringExpr(`opts.${fieldName}`, qp.schema)}) }`)
851
- } else {
852
- l.push(
853
- `\tif opts != nil && opts.${fieldName} != nil { q.Set(${JSON.stringify(qp.name)}, ${qpStringExpr(`*opts.${fieldName}`, qp.schema)}) }`,
854
- )
855
- }
856
- }
857
-
858
- const urlExpr = pathToGoExpr(pathStr)
859
- const bodyArg = !m.bodyIsStream && m.bodyType ? "body" : "nil"
860
- const bodyReaderArg = m.bodyIsStream ? "body" : "nil"
861
- const contentTypeArg = m.bodyIsStream ? `"application/octet-stream"` : `""`
862
-
863
- l.push(`\tvar callHeaders map[string]string`)
864
- l.push(`\tif opts != nil { callHeaders = opts.Headers }`)
865
-
866
- /* idempotency-key auto-injection: case-insensitive existing-header check
867
- * matches caller's intent if they supplied any casing. opts==nil branch
868
- * still emits a key so the contract holds (every dispatch carries a key
869
- * for ops marked x-idempotency-key). */
870
- if (m.isIdempotent && !m.isRealtime && !m.isSSE && !m.isWS) {
871
- l.push(`\tif opts != nil {`)
872
- l.push(`\t\t_idemExisting := ""`)
873
- l.push(`\t\tfor _k, _v := range callHeaders {`)
874
- l.push(`\t\t\tif strings.EqualFold(_k, "Idempotency-Key") { _idemExisting = _v; break }`)
875
- l.push(`\t\t}`)
876
- l.push(`\t\tif _idemExisting == "" {`)
877
- l.push(`\t\t\tif callHeaders == nil { callHeaders = map[string]string{} }`)
878
- l.push(`\t\t\tif opts.IdempotencyKey != "" {`)
879
- l.push(`\t\t\t\tcallHeaders["Idempotency-Key"] = opts.IdempotencyKey`)
880
- l.push(`\t\t\t} else {`)
881
- l.push(`\t\t\t\tcallHeaders["Idempotency-Key"] = newUUIDv4()`)
882
- l.push(`\t\t\t}`)
669
+ const resultType = plan.resultType
670
+ let ret: string
671
+ let failRet: string
672
+ if (resultType === "") {
673
+ ret = "error"
674
+ failRet = "return err"
675
+ } else if (throwOnError) {
676
+ ret = `(*${resultType}, error)`
677
+ failRet = "return nil, err"
678
+ } else {
679
+ ret = `(SDKResult[${resultType}], error)`
680
+ failRet = `return SDKResult[${resultType}]{}, err`
681
+ }
682
+ l.push(`func (${recv} ${recvType}) ${plan.methodName}(${params.join(", ")}) ${ret} {`)
683
+ l.push(`\tvar err error`)
684
+ l.push(...emitPath(plan, failRet, "\t"))
685
+ l.push(...emitQueryAndHeaders(plan, "\t", "map[string]string{}"))
686
+
687
+ /* idempotency-key auto-injection: an explicit header (any casing) wins, then
688
+ * opts.IdempotencyKey, then a fresh UUID — every dispatch carries a key. */
689
+ if (op.idempotent) {
690
+ l.push(`\tif !hasHeader(callHeaders, "Idempotency-Key") {`)
691
+ l.push(`\t\tkey := newUUIDv4()`)
692
+ l.push(`\t\tif opts != nil && opts.IdempotencyKey != "" {`)
693
+ l.push(`\t\t\tkey = opts.IdempotencyKey`)
883
694
  l.push(`\t\t}`)
884
- l.push(`\t} else {`)
885
- l.push(`\t\tcallHeaders = map[string]string{"Idempotency-Key": newUUIDv4()}`)
695
+ l.push(`\t\tcallHeaders["Idempotency-Key"] = key`)
886
696
  l.push(`\t}`)
887
697
  }
888
698
 
889
- /* invalidation pre-flight — build path-params map, interpolate to concrete
890
- * selector, snapshot RequestMeta from the tracker. Emitted unconditionally
891
- * so consumer can flip cfg.Invalidation on/off without regen; tracker's
892
- * BuildRequestMeta short-circuits to nil when disabled. */
893
- const hasInvalidate = Array.isArray(m.op["x-invalidate"]) && (m.op["x-invalidate"] as unknown[]).length > 0
894
- const pathParamsMapExpr =
895
- m.pathParams.length > 0
896
- ? `map[string]string{${m.pathParams.map((p) => `${JSON.stringify(p)}: ${p}`).join(", ")}}`
897
- : `map[string]string(nil)`
898
- if (hasInvalidate) {
899
- l.push(`\t_pathParamsMap := ${pathParamsMapExpr}`)
900
- l.push(`\t_concretePath, _ := interpolatePath(${JSON.stringify(pathStr)}, _pathParamsMap)`)
901
- l.push(`\t_concreteSelector := ${JSON.stringify(`${httpMethodStr} `)} + _concretePath`)
902
- l.push(`\tvar _requestMeta *RequestMeta`)
903
- l.push(
904
- `\tif ${staleAccess} != nil { _requestMeta = ${staleAccess}.BuildRequestMeta(_concreteSelector, _concretePath, ${JSON.stringify(httpMethodStr)}) }`,
905
- )
906
- }
699
+ const body = emitBody(plan, failRet, "\t")
700
+ l.push(...body.lines)
701
+
702
+ /* invalidation pre-flight for every call: reads see and clear staleness too */
703
+ const pathParamsExpr = plan.pathLocals.length > 0 ? pathExpr(plan) : "map[string]string(nil)"
704
+ l.push(`\tpathParams := ${pathParamsExpr}`)
705
+ l.push(`\tselector := ${goString(`${op.method} `)} + path`)
706
+ l.push(`\tvar requestMeta *RequestMeta`)
707
+ l.push(`\tif ${stale} != nil {`)
708
+ l.push(`\t\trequestMeta = ${stale}.BuildRequestMeta(selector, path, ${goString(op.method)})`)
709
+ l.push(`\t}`)
907
710
 
908
- const resultBind = m.responseType === "" ? "_, err" : "result, err"
909
- const operationStr = JSON.stringify(`${httpMethodStr} ${pathStr}`)
910
- const requestMetaArg = hasInvalidate ? "_requestMeta" : "nil"
711
+ const resultBind = resultType === "" ? "_, err =" : "result, err :="
911
712
  l.push(
912
- `\t${resultBind} := doRequest(ctx, ${cfgAccess}, "${httpMethodStr}", ${urlExpr}, q, ${bodyArg}, ${bodyReaderArg}, ${contentTypeArg}, callHeaders, ${operationStr}, ${requestMetaArg})`,
713
+ `\t${resultBind} doRequest(ctx, ${cfg}, ${goString(op.method)}, path, q, ${body.expr}, callHeaders, ${goString(`${op.method} ${op.path}`)}, requestMeta)`,
913
714
  )
914
715
  l.push(`\tif err != nil {`)
915
-
916
- if (throwOnError) {
917
- if (m.responseType !== "") {
918
- l.push(`\t\treturn nil, err`)
919
- } else {
920
- l.push(`\t\treturn err`)
921
- }
716
+ if (resultType !== "" && !throwOnError) {
717
+ l.push(`\t\tvar apiErr APIError`)
718
+ l.push(`\t\tif errors.As(err, &apiErr) {`)
719
+ l.push(`\t\t\treturn SDKResult[${resultType}]{Err: apiErr, Status: apiErr.Status()}, nil`)
720
+ l.push(`\t\t}`)
721
+ l.push(`\t\treturn SDKResult[${resultType}]{}, err`)
922
722
  } else {
923
- if (m.responseType !== "") {
924
- /* unwrap typed APIError if doRequest produced one, else synthesize
925
- * a StatusError so callers always see a typed Err without panicking
926
- * on ctx cancel / network errors. */
927
- l.push(`\t\tif apiErr, ok := err.(APIError); ok {`)
928
- l.push(`\t\t\treturn SDKResult[${m.responseType}]{Err: apiErr, Status: 0, Response: nil}, nil`)
929
- l.push(`\t\t}`)
930
- l.push(
931
- `\t\treturn SDKResult[${m.responseType}]{Err: &StatusError{StatusCode: 0, Message: err.Error()}, Status: 0, Response: nil}, nil`,
932
- )
933
- } else {
934
- l.push(`\t\treturn err`)
935
- }
723
+ l.push(`\t\t${failRet}`)
936
724
  }
937
725
  l.push(`\t}`)
938
726
 
939
- /* invalidation post-flight — expand templated x-invalidate targets with the
940
- * call's concrete params (TS parity: GET /users/{id} → GET /users/abc),
941
- * mark them stale, then clear keys that were already stale before this
942
- * mutation completed (seq-bounded to avoid clobbering newer invalidations). */
943
- if (hasInvalidate) {
944
- l.push(`\tif ${staleAccess} != nil {`)
945
- l.push(
946
- `\t\t${staleAccess}.MarkStale(invalidationMap["${m.resource}"]["${m.action}"].Invalidate, _pathParamsMap, _concreteSelector)`,
947
- )
948
- l.push(`\t\tif _requestMeta != nil && _requestMeta.IsStale {`)
949
- l.push(
950
- `\t\t\t${staleAccess}.ClearStale(_concreteSelector, _concretePath, ${JSON.stringify(httpMethodStr)}, _requestMeta.SeqSnapshot)`,
951
- )
952
- l.push(`\t\t}`)
953
- l.push(`\t}`)
727
+ /* invalidation post-flight: mark declared targets stale (templated ones expanded
728
+ * with this call's params), then clear this key if the call read through a stale window */
729
+ l.push(`\tif ${stale} != nil {`)
730
+ if (op.invalidates.length > 0) {
731
+ l.push(`\t\t${stale}.MarkStale([]string{${op.invalidates.map(goString).join(", ")}}, pathParams, selector)`)
732
+ } else {
733
+ l.push(`\t\t_ = pathParams`)
954
734
  }
735
+ l.push(`\t\tif requestMeta != nil && requestMeta.IsStale {`)
736
+ l.push(`\t\t\t${stale}.ClearStale(selector, path, ${goString(op.method)}, requestMeta.SeqSnapshot)`)
737
+ l.push(`\t\t}`)
738
+ l.push(`\t}`)
955
739
 
956
- if (m.responseType === "") {
740
+ if (resultType === "") {
957
741
  l.push(`\treturn nil`)
958
742
  l.push(`}`)
959
743
  l.push(``)
960
744
  return l
961
745
  }
962
746
 
963
- if (throwOnError) {
964
- l.push(`\tvar out ${m.responseType}`)
965
- l.push(`\tif len(result.body) > 0 {`)
966
- l.push(`\t\tif err := json.Unmarshal(result.body, &out); err != nil { return nil, err }`)
967
- l.push(`\t}`)
968
- l.push(`\treturn &out, nil`)
747
+ l.push(`\tvar out ${resultType}`)
748
+ if (op.success.kind === "text") {
749
+ l.push(`\tout = string(result.body)`)
750
+ } else if (op.success.kind === "binary") {
751
+ l.push(`\tout = result.body`)
969
752
  } else {
970
- l.push(`\tvar out ${m.responseType}`)
971
753
  l.push(`\tif len(result.body) > 0 {`)
972
754
  l.push(`\t\tif err := json.Unmarshal(result.body, &out); err != nil {`)
973
- l.push(
974
- `\t\t\treturn SDKResult[${m.responseType}]{Err: &StatusError{StatusCode: result.status, Body: result.body, Response: result.resp, Message: err.Error()}, Status: result.status, Response: result.resp}, nil`,
975
- )
755
+ if (throwOnError) {
756
+ l.push(`\t\t\treturn nil, fmt.Errorf("honey: decode response: %w", err)`)
757
+ } else {
758
+ l.push(
759
+ `\t\t\treturn SDKResult[${resultType}]{Err: &StatusError{StatusCode: result.status, Body: result.body, Response: result.resp, Message: err.Error()}, Status: result.status, Response: result.resp}, nil`,
760
+ )
761
+ }
976
762
  l.push(`\t\t}`)
977
763
  l.push(`\t}`)
978
- l.push(`\treturn SDKResult[${m.responseType}]{Data: &out, Status: result.status, Response: result.resp}, nil`)
979
764
  }
980
-
765
+ if (throwOnError) l.push(`\treturn &out, nil`)
766
+ else l.push(`\treturn SDKResult[${resultType}]{Data: &out, Status: result.status, Response: result.resp}, nil`)
981
767
  l.push(`}`)
982
768
  l.push(``)
983
769
  return l
984
770
  }
985
771
 
772
+ type ResourceNode = {
773
+ structName: string
774
+ ns: IRNamespace
775
+ path: string[]
776
+ children: Array<{ field: string; node: ResourceNode }>
777
+ methods: GoOpPlan[]
778
+ }
779
+
780
+ function buildGoClient(
781
+ model: SdkModel,
782
+ names: GoNames,
783
+ options: GoSDKOptions,
784
+ auth: { headerName: string; prefix: string },
785
+ ): string {
786
+ const throwOnError = options.throwOnError ?? true
787
+
788
+ /* plan resources top-down so names are claimed in a stable order */
789
+ const allPlans: GoOpPlan[] = []
790
+ function planNs(
791
+ ns: IRNamespace,
792
+ path: string[],
793
+ reserved: string[],
794
+ ): { children: ResourceNode["children"]; methods: GoOpPlan[] } {
795
+ const members = new NameScope(reserved)
796
+ const children: ResourceNode["children"] = []
797
+ for (const [seg, childNs] of namespacesOf(ns)) {
798
+ const field = members.claim(goExported(seg))
799
+ const childPath = [...path, seg]
800
+ const structName = names.scope.claim(`${childPath.map(goExported).join("")}Resource`)
801
+ children.push({ field, node: { children: [], methods: [], ns: childNs, path: childPath, structName } })
802
+ }
803
+ const methods: GoOpPlan[] = []
804
+ for (const [seg, irOp] of methodsOf(ns)) {
805
+ const op = model.opsById.get(irOp.id)
806
+ if (!op) continue
807
+ const base = goExported(seg)
808
+ /* Go forbids a field and a method with one name: `extract` + `extract.table` → ExtractCall */
809
+ const methodName = members.has(base) ? members.claim(`${base}Call`) : members.claim(base)
810
+ const optsName = names.scope.claim(`${[...path, seg].map(goExported).join("")}Opts`)
811
+ const plan = planOp(op, methodName, optsName, names)
812
+ methods.push(plan)
813
+ allPlans.push(plan)
814
+ }
815
+ for (const child of children) {
816
+ const sub = planNs(child.node.ns, child.node.path, ["client"])
817
+ child.node.children = sub.children
818
+ child.node.methods = sub.methods
819
+ }
820
+ return { children, methods }
821
+ }
822
+ const root = planNs(model.ir.tree, [], ["cfg", "stale", "IsStale"])
823
+
824
+ const body: string[] = []
825
+ /* ---- Client struct: cfg + stale + one field per root namespace ---- */
826
+ body.push(`// Client is the generated SDK client.`)
827
+ body.push(`type Client struct {`)
828
+ body.push(`\tcfg Config`)
829
+ body.push(`\tstale *StaleTracker`)
830
+ for (const child of root.children) body.push(`\t${child.field} *${child.node.structName}`)
831
+ body.push(`}`)
832
+ body.push(``)
833
+ body.push(emitClientBody(root, allPlans, throwOnError, auth))
834
+ return body.join("\n")
835
+ }
836
+
837
+ function emitClientBody(
838
+ root: { children: ResourceNode["children"]; methods: GoOpPlan[] },
839
+ allPlans: GoOpPlan[],
840
+ throwOnError: boolean,
841
+ auth: { headerName: string; prefix: string },
842
+ ): string {
843
+ const body: string[] = []
844
+ /* ---- NewClient: init + cascade nested struct init ---- */
845
+ body.push(`// NewClient creates a new SDK client. BaseURL must be non-empty.`)
846
+ body.push(`func NewClient(cfg Config) *Client {`)
847
+ body.push(`\tif cfg.BaseURL == "" {`)
848
+ body.push(`\t\tpanic("honey sdk: Config.BaseURL must not be empty")`)
849
+ body.push(`\t}`)
850
+ body.push(`\tif cfg.AuthHeaderName == "" {`)
851
+ body.push(`\t\tcfg.AuthHeaderName = ${goString(auth.headerName)}`)
852
+ body.push(`\t}`)
853
+ body.push(`\tif cfg.AuthHeaderPrefix == "" {`)
854
+ body.push(`\t\tcfg.AuthHeaderPrefix = ${goString(auth.prefix)}`)
855
+ body.push(`\t}`)
856
+ body.push(`\tcfg = prepareConfig(cfg)`)
857
+ body.push(`\tc := &Client{cfg: cfg}`)
858
+ body.push(`\tc.stale = NewStaleTracker(cfg.Invalidation)`)
859
+ for (const child of root.children) body.push(`\tc.${child.field} = new${child.node.structName}(c)`)
860
+ body.push(`\treturn c`)
861
+ body.push(`}`)
862
+ body.push(``)
863
+
864
+ /* IsStale predicate */
865
+ body.push(`// IsStale reports whether (method, path) currently sits inside an active stale window.`)
866
+ body.push(`func (c *Client) IsStale(method, path string) bool {`)
867
+ body.push(`\tif c.stale == nil {`)
868
+ body.push(`\t\treturn false`)
869
+ body.push(`\t}`)
870
+ body.push(`\treturn c.stale.IsStale(method, path)`)
871
+ body.push(`}`)
872
+ body.push(``)
873
+
874
+ for (const plan of root.methods) body.push(...emitMethod(plan, "c", "*Client", throwOnError))
875
+
876
+ function emitResource(node: ResourceNode): void {
877
+ body.push(`// ${node.structName} groups the ${goString(node.path.join("."))} operations.`)
878
+ body.push(`type ${node.structName} struct {`)
879
+ body.push(`\tclient *Client`)
880
+ for (const child of node.children) body.push(`\t${child.field} *${child.node.structName}`)
881
+ body.push(`}`)
882
+ body.push(``)
883
+ body.push(`func new${node.structName}(c *Client) *${node.structName} {`)
884
+ body.push(`\tr := &${node.structName}{client: c}`)
885
+ for (const child of node.children) body.push(`\tr.${child.field} = new${child.node.structName}(c)`)
886
+ body.push(`\treturn r`)
887
+ body.push(`}`)
888
+ body.push(``)
889
+ for (const plan of node.methods) body.push(...emitMethod(plan, "r", `*${node.structName}`, throwOnError))
890
+ for (const child of node.children) emitResource(child.node)
891
+ }
892
+ for (const child of root.children) emitResource(child.node)
893
+
894
+ for (const plan of [...allPlans].sort((a, b) => cmpCodeUnit(a.optsName, b.optsName))) {
895
+ body.push(...emitMultipartStruct(plan))
896
+ body.push(...emitOptsStruct(plan))
897
+ }
898
+ return body.join("\n")
899
+ }
900
+
901
+ function clientFile(model: SdkModel, names: GoNames, options: GoSDKOptions): string {
902
+ const bodyStr = buildGoClient(model, names, options, model.ir.auth)
903
+
904
+ const stdlib = ["context"]
905
+ const uses = (re: RegExp) => re.test(bodyStr)
906
+ if (uses(/\bjson\./)) stdlib.push("encoding/json")
907
+ if (uses(/\berrors\./)) stdlib.push("errors")
908
+ if (uses(/\bfmt\./)) stdlib.push("fmt")
909
+ if (uses(/\bio\./)) stdlib.push("io")
910
+ if (uses(/\biter\./)) stdlib.push("iter")
911
+ if (uses(/\burl\./)) stdlib.push("net/url")
912
+ const thirdParty = uses(/\bwebsocket\./) ? ["nhooyr.io/websocket"] : []
913
+
914
+ const out: string[] = []
915
+ /* blank line after generator header detaches it from package-doc on pkg.go.dev */
916
+ out.push(`// Code generated by honey. DO NOT EDIT.`)
917
+ out.push(``)
918
+ out.push(`package sdk`)
919
+ out.push(``)
920
+ out.push(goImportBlock(stdlib, thirdParty))
921
+ out.push(``)
922
+ out.push(bodyStr)
923
+ return out.join("\n")
924
+ }
925
+
986
926
  /* ── public entrypoint ── */
987
927
 
988
928
  /**
@@ -990,9 +930,10 @@ function emitMethod(m: MethodInfo, recv: string, recvType: string, throwOnError:
990
930
  * Returns a file map (filename → content) ready to write to disk as a Go module.
991
931
  */
992
932
  export function generateGoSDK(spec: Record<string, unknown>, options: GoSDKOptions = {}): GeneratedGoSDK {
993
- const resolved = resolveRefs(spec as Parameters<typeof resolveRefs>[0]) as unknown as OpenApiSpec
994
-
995
- const { serviceMap } = collectSDKMethods(spec as Parameters<typeof resolveRefs>[0])
933
+ const input = spec as unknown as OpenApiSpecInput
934
+ const model = buildSdkModel(input)
935
+ const names = buildGoNames(model)
936
+ const serviceMap = serviceMapOf(model.ir)
996
937
 
997
938
  const files: Record<string, string> = {}
998
939
 
@@ -1001,13 +942,12 @@ export function generateGoSDK(spec: Record<string, unknown>, options: GoSDKOptio
1001
942
  files[name] = content
1002
943
  }
1003
944
 
1004
- /* types.go uses unresolved spec because resolveRefs strips components.schemas. */
1005
- files["types.go"] = buildGoTypes(spec as unknown as OpenApiSpec)
1006
- /* client uses unresolved spec so request/response $refs survive for type
1007
- * resolution into named Go types (resolveRefs inlines and drops names). */
1008
- files["client.go"] = buildGoClient(spec as unknown as OpenApiSpec, options)
945
+ /* schema types claim their names first; the client then hoists into the same table */
946
+ const schemaTypes = buildGoTypes(model, names)
947
+ files["client.go"] = clientFile(model, names, options)
948
+ files["types.go"] = typesFile(schemaTypes, names)
1009
949
  files["go.mod"] = buildGoMod(options.modulePath)
1010
- files["doc.go"] = buildGoDoc(resolved)
950
+ files["doc.go"] = buildGoDoc(model.ir.info)
1011
951
 
1012
952
  /* Opt-in post-process. Consumers publishing to pkg.go.dev pass `gofmt: true`
1013
953
  * so struct fields align and single-line `if` blocks expand. No-op when