ts-procedures 7.3.0 → 8.0.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 (325) hide show
  1. package/README.md +65 -3
  2. package/agent_config/claude-code/agents/ts-procedures-architect.md +6 -8
  3. package/agent_config/claude-code/skills/ts-procedures/SKILL.md +30 -33
  4. package/agent_config/claude-code/skills/ts-procedures/anti-patterns.md +104 -53
  5. package/agent_config/claude-code/skills/ts-procedures/api-reference.md +205 -232
  6. package/agent_config/claude-code/skills/ts-procedures/patterns.md +80 -153
  7. package/agent_config/claude-code/skills/ts-procedures-review/SKILL.md +1 -1
  8. package/agent_config/claude-code/skills/ts-procedures-review/checklist.md +4 -5
  9. package/agent_config/claude-code/skills/ts-procedures-scaffold/SKILL.md +4 -7
  10. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono.md +223 -0
  11. package/agent_config/copilot/copilot-instructions.md +34 -48
  12. package/agent_config/cursor/cursorrules +34 -48
  13. package/build/client/call.js +4 -1
  14. package/build/client/call.js.map +1 -1
  15. package/build/client/call.test.js +23 -0
  16. package/build/client/call.test.js.map +1 -1
  17. package/build/client/fetch-adapter.js +3 -1
  18. package/build/client/fetch-adapter.js.map +1 -1
  19. package/build/client/fetch-adapter.test.js +11 -1
  20. package/build/client/fetch-adapter.test.js.map +1 -1
  21. package/build/client/index.test.js +7 -7
  22. package/build/client/index.test.js.map +1 -1
  23. package/build/client/request-builder.d.ts +1 -1
  24. package/build/client/request-builder.js +2 -2
  25. package/build/client/request-builder.js.map +1 -1
  26. package/build/client/stream.js +13 -2
  27. package/build/client/stream.js.map +1 -1
  28. package/build/client/stream.test.js +32 -7
  29. package/build/client/stream.test.js.map +1 -1
  30. package/build/client/typed-error-dispatch.test.js +8 -92
  31. package/build/client/typed-error-dispatch.test.js.map +1 -1
  32. package/build/client/types.d.ts +21 -3
  33. package/build/codegen/bin/cli.js +0 -0
  34. package/build/codegen/e2e.test.js +87 -23
  35. package/build/codegen/e2e.test.js.map +1 -1
  36. package/build/codegen/emit-errors.integration.test.js +1 -1
  37. package/build/codegen/emit-errors.integration.test.js.map +1 -1
  38. package/build/codegen/emit-scope.js +308 -47
  39. package/build/codegen/emit-scope.js.map +1 -1
  40. package/build/codegen/emit-scope.test.js +363 -110
  41. package/build/codegen/emit-scope.test.js.map +1 -1
  42. package/build/codegen/pipeline.test.js +7 -7
  43. package/build/codegen/pipeline.test.js.map +1 -1
  44. package/build/codegen/resolve-envelope.js +1 -1
  45. package/build/codegen/resolve-envelope.js.map +1 -1
  46. package/build/codegen/resolve-envelope.test.js +5 -5
  47. package/build/codegen/resolve-envelope.test.js.map +1 -1
  48. package/build/codegen/targets/_shared/route-slots.d.ts +8 -3
  49. package/build/codegen/targets/_shared/route-slots.js +49 -8
  50. package/build/codegen/targets/_shared/route-slots.js.map +1 -1
  51. package/build/codegen/targets/_shared/route-slots.test.js +99 -26
  52. package/build/codegen/targets/_shared/route-slots.test.js.map +1 -1
  53. package/build/codegen/targets/kotlin/emit-route-kotlin.test.js +88 -17
  54. package/build/codegen/targets/kotlin/emit-route-kotlin.test.js.map +1 -1
  55. package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js +9 -6
  56. package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js.map +1 -1
  57. package/build/codegen/targets/kotlin/integration.test.js +6 -0
  58. package/build/codegen/targets/kotlin/integration.test.js.map +1 -1
  59. package/build/codegen/targets/swift/access-level.test.js +8 -11
  60. package/build/codegen/targets/swift/access-level.test.js.map +1 -1
  61. package/build/codegen/targets/swift/emit-route-swift.test.js +91 -20
  62. package/build/codegen/targets/swift/emit-route-swift.test.js.map +1 -1
  63. package/build/codegen/targets/swift/emit-scope-swift.test.js +12 -9
  64. package/build/codegen/targets/swift/emit-scope-swift.test.js.map +1 -1
  65. package/build/codegen/targets/swift/integration.test.js +6 -0
  66. package/build/codegen/targets/swift/integration.test.js.map +1 -1
  67. package/build/create-http-stream.d.ts +58 -0
  68. package/build/create-http-stream.js +122 -0
  69. package/build/create-http-stream.js.map +1 -0
  70. package/build/create-http-stream.test.js +88 -0
  71. package/build/create-http-stream.test.js.map +1 -0
  72. package/build/create-http.d.ts +49 -0
  73. package/build/create-http.js +108 -0
  74. package/build/create-http.js.map +1 -0
  75. package/build/create-http.test.js +137 -0
  76. package/build/create-http.test.js.map +1 -0
  77. package/build/create-stream.d.ts +35 -0
  78. package/build/create-stream.js +123 -0
  79. package/build/create-stream.js.map +1 -0
  80. package/build/create-stream.test.js +428 -0
  81. package/build/create-stream.test.js.map +1 -0
  82. package/build/create.d.ts +28 -0
  83. package/build/create.js +82 -0
  84. package/build/create.js.map +1 -0
  85. package/build/create.test.js +483 -0
  86. package/build/create.test.js.map +1 -0
  87. package/build/exports.d.ts +2 -0
  88. package/build/implementations/http/astro/index.test.js +20 -12
  89. package/build/implementations/http/astro/index.test.js.map +1 -1
  90. package/build/implementations/http/doc-registry.js +1 -1
  91. package/build/implementations/http/doc-registry.js.map +1 -1
  92. package/build/implementations/http/doc-registry.test.js +36 -5
  93. package/build/implementations/http/doc-registry.test.js.map +1 -1
  94. package/build/implementations/http/error-dispatch.d.ts +76 -0
  95. package/build/implementations/http/error-dispatch.js +77 -0
  96. package/build/implementations/http/error-dispatch.js.map +1 -0
  97. package/build/implementations/http/error-dispatch.test.js +254 -0
  98. package/build/implementations/http/error-dispatch.test.js.map +1 -0
  99. package/build/implementations/http/error-taxonomy.d.ts +5 -5
  100. package/build/implementations/http/hono/docs/http-doc.d.ts +6 -0
  101. package/build/implementations/http/hono/docs/http-doc.js +42 -0
  102. package/build/implementations/http/hono/docs/http-doc.js.map +1 -0
  103. package/build/implementations/http/hono/docs/http-stream-doc.d.ts +6 -0
  104. package/build/implementations/http/hono/docs/http-stream-doc.js +40 -0
  105. package/build/implementations/http/hono/docs/http-stream-doc.js.map +1 -0
  106. package/build/implementations/http/hono/docs/rpc-doc.d.ts +6 -0
  107. package/build/implementations/http/hono/docs/rpc-doc.js +24 -0
  108. package/build/implementations/http/hono/docs/rpc-doc.js.map +1 -0
  109. package/build/implementations/http/hono/docs/stream-doc.d.ts +6 -0
  110. package/build/implementations/http/hono/docs/stream-doc.js +42 -0
  111. package/build/implementations/http/hono/docs/stream-doc.js.map +1 -0
  112. package/build/implementations/http/hono/handlers/http-stream.d.ts +10 -0
  113. package/build/implementations/http/hono/handlers/http-stream.js +123 -0
  114. package/build/implementations/http/hono/handlers/http-stream.js.map +1 -0
  115. package/build/implementations/http/hono/handlers/http-stream.test.js +128 -0
  116. package/build/implementations/http/hono/handlers/http-stream.test.js.map +1 -0
  117. package/build/implementations/http/hono/handlers/http.d.ts +10 -0
  118. package/build/implementations/http/hono/handlers/http.js +115 -0
  119. package/build/implementations/http/hono/handlers/http.js.map +1 -0
  120. package/build/implementations/http/hono/handlers/http.test.js +118 -0
  121. package/build/implementations/http/hono/handlers/http.test.js.map +1 -0
  122. package/build/implementations/http/hono/handlers/rpc.d.ts +11 -0
  123. package/build/implementations/http/hono/handlers/rpc.js +32 -0
  124. package/build/implementations/http/hono/handlers/rpc.js.map +1 -0
  125. package/build/implementations/http/hono/handlers/rpc.test.js +73 -0
  126. package/build/implementations/http/hono/handlers/rpc.test.js.map +1 -0
  127. package/build/implementations/http/hono/handlers/stream.d.ts +23 -0
  128. package/build/implementations/http/hono/handlers/stream.js +147 -0
  129. package/build/implementations/http/hono/handlers/stream.js.map +1 -0
  130. package/build/implementations/http/hono/handlers/stream.test.d.ts +1 -0
  131. package/build/implementations/http/hono/handlers/stream.test.js +177 -0
  132. package/build/implementations/http/hono/handlers/stream.test.js.map +1 -0
  133. package/build/implementations/http/hono/index.d.ts +57 -0
  134. package/build/implementations/http/hono/index.js +149 -0
  135. package/build/implementations/http/hono/index.js.map +1 -0
  136. package/build/implementations/http/hono/index.test.d.ts +1 -0
  137. package/build/implementations/http/hono/index.test.js +274 -0
  138. package/build/implementations/http/hono/index.test.js.map +1 -0
  139. package/build/implementations/http/hono/path.d.ts +17 -0
  140. package/build/implementations/http/hono/path.js +39 -0
  141. package/build/implementations/http/hono/path.js.map +1 -0
  142. package/build/implementations/http/hono/path.test.d.ts +1 -0
  143. package/build/implementations/http/hono/path.test.js +83 -0
  144. package/build/implementations/http/hono/path.test.js.map +1 -0
  145. package/build/implementations/http/hono/types.d.ts +51 -0
  146. package/build/implementations/http/hono/types.js.map +1 -0
  147. package/build/implementations/http/on-request-error.test.js +6 -96
  148. package/build/implementations/http/on-request-error.test.js.map +1 -1
  149. package/build/implementations/http/route-errors.test.js +11 -59
  150. package/build/implementations/http/route-errors.test.js.map +1 -1
  151. package/build/implementations/types.d.ts +43 -9
  152. package/build/index.d.ts +124 -124
  153. package/build/index.js +10 -221
  154. package/build/index.js.map +1 -1
  155. package/build/index.test.js +20 -919
  156. package/build/index.test.js.map +1 -1
  157. package/build/migration.test.d.ts +1 -0
  158. package/build/migration.test.js +34 -0
  159. package/build/migration.test.js.map +1 -0
  160. package/build/schema/compute-schema.d.ts +11 -3
  161. package/build/schema/compute-schema.js +13 -7
  162. package/build/schema/compute-schema.js.map +1 -1
  163. package/build/schema/parser.d.ts +11 -3
  164. package/build/schema/parser.js +49 -9
  165. package/build/schema/parser.js.map +1 -1
  166. package/build/stack-utils.js +8 -0
  167. package/build/stack-utils.js.map +1 -1
  168. package/build/types.d.ts +142 -0
  169. package/build/types.js.map +1 -0
  170. package/docs/astro-adapter.md +5 -5
  171. package/docs/core.md +15 -17
  172. package/docs/http-integrations.md +83 -170
  173. package/docs/streaming.md +3 -60
  174. package/docs/superpowers/plans/2026-05-07-astro-adapter.md +2 -7
  175. package/docs/superpowers/plans/2026-05-08-create-http.md +3355 -0
  176. package/docs/superpowers/plans/2026-05-08-hono-app-builder-convergence.md +3365 -0
  177. package/docs/superpowers/specs/2026-05-07-astro-adapter-design.md +1 -3
  178. package/docs/superpowers/specs/2026-05-08-create-http-design.md +409 -0
  179. package/docs/superpowers/specs/2026-05-08-hono-app-builder-convergence-design.md +411 -0
  180. package/package.json +4 -22
  181. package/src/client/call.test.ts +26 -0
  182. package/src/client/call.ts +4 -1
  183. package/src/client/fetch-adapter.test.ts +14 -1
  184. package/src/client/fetch-adapter.ts +3 -1
  185. package/src/client/index.test.ts +7 -7
  186. package/src/client/request-builder.ts +2 -2
  187. package/src/client/stream.test.ts +39 -7
  188. package/src/client/stream.ts +16 -2
  189. package/src/client/typed-error-dispatch.test.ts +7 -97
  190. package/src/client/types.ts +21 -3
  191. package/src/codegen/__fixtures__/users-envelope.json +119 -38
  192. package/src/codegen/e2e.test.ts +98 -24
  193. package/src/codegen/emit-errors.integration.test.ts +1 -1
  194. package/src/codegen/emit-scope.test.ts +395 -110
  195. package/src/codegen/emit-scope.ts +350 -55
  196. package/src/codegen/pipeline.test.ts +7 -7
  197. package/src/codegen/resolve-envelope.test.ts +5 -5
  198. package/src/codegen/resolve-envelope.ts +1 -1
  199. package/src/codegen/targets/_shared/route-slots.test.ts +109 -26
  200. package/src/codegen/targets/_shared/route-slots.ts +48 -11
  201. package/src/codegen/targets/kotlin/__fixtures__/users-golden.kt +73 -0
  202. package/src/codegen/targets/kotlin/emit-route-kotlin.test.ts +100 -17
  203. package/src/codegen/targets/kotlin/emit-scope-kotlin.test.ts +9 -6
  204. package/src/codegen/targets/kotlin/integration.test.ts +19 -0
  205. package/src/codegen/targets/swift/__fixtures__/users-golden.swift +79 -0
  206. package/src/codegen/targets/swift/access-level.test.ts +8 -11
  207. package/src/codegen/targets/swift/emit-route-swift.test.ts +103 -20
  208. package/src/codegen/targets/swift/emit-scope-swift.test.ts +12 -9
  209. package/src/codegen/targets/swift/integration.test.ts +17 -0
  210. package/src/create-http-stream.test.ts +97 -0
  211. package/src/create-http-stream.ts +191 -0
  212. package/src/create-http.test.ts +163 -0
  213. package/src/create-http.ts +211 -0
  214. package/src/create-stream.test.ts +565 -0
  215. package/src/create-stream.ts +228 -0
  216. package/src/create.test.ts +658 -0
  217. package/src/create.ts +172 -0
  218. package/src/exports.ts +2 -0
  219. package/src/implementations/http/README.md +135 -95
  220. package/src/implementations/http/astro/README.md +4 -5
  221. package/src/implementations/http/astro/index.test.ts +25 -18
  222. package/src/implementations/http/doc-registry.test.ts +42 -5
  223. package/src/implementations/http/doc-registry.ts +1 -1
  224. package/src/implementations/http/error-dispatch.test.ts +283 -0
  225. package/src/implementations/http/error-dispatch.ts +176 -0
  226. package/src/implementations/http/error-taxonomy.ts +5 -5
  227. package/src/implementations/http/hono/docs/http-doc.ts +43 -0
  228. package/src/implementations/http/hono/docs/http-stream-doc.ts +44 -0
  229. package/src/implementations/http/hono/docs/rpc-doc.ts +34 -0
  230. package/src/implementations/http/hono/docs/stream-doc.ts +53 -0
  231. package/src/implementations/http/hono/handlers/http-stream.test.ts +150 -0
  232. package/src/implementations/http/hono/handlers/http-stream.ts +152 -0
  233. package/src/implementations/http/hono/handlers/http.test.ts +130 -0
  234. package/src/implementations/http/hono/handlers/http.ts +147 -0
  235. package/src/implementations/http/hono/handlers/rpc.test.ts +81 -0
  236. package/src/implementations/http/hono/handlers/rpc.ts +54 -0
  237. package/src/implementations/http/hono/handlers/stream.test.ts +198 -0
  238. package/src/implementations/http/hono/handlers/stream.ts +208 -0
  239. package/src/implementations/http/hono/index.test.ts +329 -0
  240. package/src/implementations/http/hono/index.ts +204 -0
  241. package/src/implementations/http/hono/path.test.ts +96 -0
  242. package/src/implementations/http/hono/path.ts +59 -0
  243. package/src/implementations/http/hono/types.ts +93 -0
  244. package/src/implementations/http/on-request-error.test.ts +10 -116
  245. package/src/implementations/http/route-errors.test.ts +11 -77
  246. package/src/implementations/types.ts +44 -9
  247. package/src/index.test.ts +22 -1249
  248. package/src/index.ts +49 -485
  249. package/src/migration.test.ts +48 -0
  250. package/src/schema/compute-schema.ts +26 -12
  251. package/src/schema/parser.ts +62 -12
  252. package/src/stack-utils.ts +8 -0
  253. package/src/types.ts +133 -0
  254. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/express-rpc.md +0 -137
  255. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-api.md +0 -173
  256. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-rpc.md +0 -142
  257. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-stream.md +0 -147
  258. package/build/implementations/http/express-rpc/error-taxonomy.test.js +0 -83
  259. package/build/implementations/http/express-rpc/error-taxonomy.test.js.map +0 -1
  260. package/build/implementations/http/express-rpc/index.d.ts +0 -125
  261. package/build/implementations/http/express-rpc/index.js +0 -216
  262. package/build/implementations/http/express-rpc/index.js.map +0 -1
  263. package/build/implementations/http/express-rpc/index.test.js +0 -684
  264. package/build/implementations/http/express-rpc/index.test.js.map +0 -1
  265. package/build/implementations/http/express-rpc/types.d.ts +0 -11
  266. package/build/implementations/http/express-rpc/types.js.map +0 -1
  267. package/build/implementations/http/hono-api/error-taxonomy.test.js +0 -137
  268. package/build/implementations/http/hono-api/error-taxonomy.test.js.map +0 -1
  269. package/build/implementations/http/hono-api/index.d.ts +0 -151
  270. package/build/implementations/http/hono-api/index.js +0 -344
  271. package/build/implementations/http/hono-api/index.js.map +0 -1
  272. package/build/implementations/http/hono-api/index.test.js +0 -992
  273. package/build/implementations/http/hono-api/index.test.js.map +0 -1
  274. package/build/implementations/http/hono-api/types.d.ts +0 -13
  275. package/build/implementations/http/hono-api/types.js.map +0 -1
  276. package/build/implementations/http/hono-rpc/error-taxonomy.test.js +0 -64
  277. package/build/implementations/http/hono-rpc/error-taxonomy.test.js.map +0 -1
  278. package/build/implementations/http/hono-rpc/index.d.ts +0 -130
  279. package/build/implementations/http/hono-rpc/index.js +0 -209
  280. package/build/implementations/http/hono-rpc/index.js.map +0 -1
  281. package/build/implementations/http/hono-rpc/index.test.js +0 -828
  282. package/build/implementations/http/hono-rpc/index.test.js.map +0 -1
  283. package/build/implementations/http/hono-rpc/types.d.ts +0 -11
  284. package/build/implementations/http/hono-rpc/types.js +0 -2
  285. package/build/implementations/http/hono-rpc/types.js.map +0 -1
  286. package/build/implementations/http/hono-stream/error-taxonomy.test.js +0 -159
  287. package/build/implementations/http/hono-stream/error-taxonomy.test.js.map +0 -1
  288. package/build/implementations/http/hono-stream/index.d.ts +0 -171
  289. package/build/implementations/http/hono-stream/index.js +0 -415
  290. package/build/implementations/http/hono-stream/index.js.map +0 -1
  291. package/build/implementations/http/hono-stream/index.test.js +0 -1383
  292. package/build/implementations/http/hono-stream/index.test.js.map +0 -1
  293. package/build/implementations/http/hono-stream/types.d.ts +0 -15
  294. package/build/implementations/http/hono-stream/types.js +0 -2
  295. package/build/implementations/http/hono-stream/types.js.map +0 -1
  296. package/src/implementations/http/express-rpc/README.md +0 -280
  297. package/src/implementations/http/express-rpc/error-taxonomy.test.ts +0 -103
  298. package/src/implementations/http/express-rpc/index.test.ts +0 -957
  299. package/src/implementations/http/express-rpc/index.ts +0 -327
  300. package/src/implementations/http/express-rpc/types.ts +0 -16
  301. package/src/implementations/http/hono-api/README.md +0 -284
  302. package/src/implementations/http/hono-api/error-taxonomy.test.ts +0 -179
  303. package/src/implementations/http/hono-api/index.test.ts +0 -1341
  304. package/src/implementations/http/hono-api/index.ts +0 -519
  305. package/src/implementations/http/hono-api/types.ts +0 -16
  306. package/src/implementations/http/hono-rpc/README.md +0 -357
  307. package/src/implementations/http/hono-rpc/error-taxonomy.test.ts +0 -82
  308. package/src/implementations/http/hono-rpc/index.test.ts +0 -1107
  309. package/src/implementations/http/hono-rpc/index.ts +0 -320
  310. package/src/implementations/http/hono-rpc/types.ts +0 -16
  311. package/src/implementations/http/hono-stream/README.md +0 -559
  312. package/src/implementations/http/hono-stream/error-taxonomy.test.ts +0 -178
  313. package/src/implementations/http/hono-stream/index.test.ts +0 -1804
  314. package/src/implementations/http/hono-stream/index.ts +0 -622
  315. package/src/implementations/http/hono-stream/types.ts +0 -20
  316. /package/build/{implementations/http/express-rpc/error-taxonomy.test.d.ts → create-http-stream.test.d.ts} +0 -0
  317. /package/build/{implementations/http/express-rpc/index.test.d.ts → create-http.test.d.ts} +0 -0
  318. /package/build/{implementations/http/hono-api/error-taxonomy.test.d.ts → create-stream.test.d.ts} +0 -0
  319. /package/build/{implementations/http/hono-api/index.test.d.ts → create.test.d.ts} +0 -0
  320. /package/build/implementations/http/{hono-rpc/error-taxonomy.test.d.ts → error-dispatch.test.d.ts} +0 -0
  321. /package/build/implementations/http/{hono-rpc/index.test.d.ts → hono/handlers/http-stream.test.d.ts} +0 -0
  322. /package/build/implementations/http/{hono-stream/error-taxonomy.test.d.ts → hono/handlers/http.test.d.ts} +0 -0
  323. /package/build/implementations/http/{hono-stream/index.test.d.ts → hono/handlers/rpc.test.d.ts} +0 -0
  324. /package/build/implementations/http/{express-rpc → hono}/types.js +0 -0
  325. /package/build/{implementations/http/hono-api/types.js → types.js} +0 -0
@@ -2,54 +2,137 @@ import { describe, expect, it } from 'vitest'
2
2
  import { extractRouteSlots } from './route-slots.js'
3
3
  import type { AnyHttpRouteDoc } from '../../../implementations/types.js'
4
4
 
5
- const r = (schema: Record<string, unknown> | undefined): AnyHttpRouteDoc =>
6
- ({ name: 'X', kind: 'api', method: 'GET', fullPath: '/x', schema } as unknown as AnyHttpRouteDoc)
5
+ // Helpers for constructing minimal route docs per kind
6
+ function apiRoute(jsonSchema: Record<string, unknown>): AnyHttpRouteDoc {
7
+ return { name: 'X', kind: 'api', method: 'get', path: '/x', fullPath: '/x', jsonSchema } as unknown as AnyHttpRouteDoc
8
+ }
7
9
 
8
- describe('extractRouteSlots', () => {
9
- it('returns no slots when schema is undefined', () => {
10
- expect(extractRouteSlots(r(undefined))).toEqual([])
11
- })
10
+ function httpStreamRoute(jsonSchema: Record<string, unknown>): AnyHttpRouteDoc {
11
+ return { name: 'X', kind: 'http-stream', method: 'get', path: '/x', fullPath: '/x', streamMode: 'sse', jsonSchema } as unknown as AnyHttpRouteDoc
12
+ }
13
+
14
+ function rpcRoute(jsonSchema: Record<string, unknown>): AnyHttpRouteDoc {
15
+ return { name: 'X', kind: 'rpc', method: 'post', path: '/x', scope: 'x', version: 1, jsonSchema } as unknown as AnyHttpRouteDoc
16
+ }
12
17
 
13
- it('returns no slots when schema is empty', () => {
14
- expect(extractRouteSlots(r({}))).toEqual([])
18
+ function streamRoute(jsonSchema: Record<string, unknown>): AnyHttpRouteDoc {
19
+ return { name: 'X', kind: 'stream', path: '/x', scope: 'x', version: 1, methods: ['get'], streamMode: 'sse', jsonSchema } as unknown as AnyHttpRouteDoc
20
+ }
21
+
22
+ describe('extractRouteSlots — api kind', () => {
23
+ it('returns no slots when jsonSchema is empty', () => {
24
+ expect(extractRouteSlots(apiRoute({}))).toEqual([])
15
25
  })
16
26
 
17
- it('returns the deterministic slot order: PathParams, Query, Body, Response', () => {
27
+ it('returns the deterministic slot order: PathParams, Query, Body, Headers, Response, ResponseHeaders', () => {
18
28
  const slots = extractRouteSlots(
19
- r({
20
- input: {
21
- body: { type: 'object', x: 'body' },
22
- query: { type: 'object', x: 'query' },
23
- pathParams: { type: 'object', x: 'path' },
29
+ apiRoute({
30
+ req: {
31
+ body: { type: 'object', tag: 'body' },
32
+ query: { type: 'object', tag: 'query' },
33
+ pathParams: { type: 'object', tag: 'path' },
34
+ headers: { type: 'object', tag: 'headers' },
35
+ },
36
+ res: {
37
+ body: { type: 'object', tag: 'response' },
38
+ headers: { type: 'object', tag: 'resHeaders' },
24
39
  },
25
- returnType: { type: 'object', x: 'response' },
26
40
  }),
27
41
  )
28
- expect(slots.map((s) => s.rootName)).toEqual(['PathParams', 'Query', 'Body', 'Response'])
42
+ expect(slots.map((s) => s.rootName)).toEqual(['PathParams', 'Query', 'Body', 'Headers', 'Response', 'ResponseHeaders'])
29
43
  })
30
44
 
31
45
  it('omits slots whose source is null or undefined', () => {
32
46
  const slots = extractRouteSlots(
33
- r({
34
- input: { pathParams: { type: 'object' }, query: null, body: undefined },
35
- // returnType missing
47
+ apiRoute({
48
+ req: { pathParams: { type: 'object' }, query: null, body: undefined },
36
49
  }),
37
50
  )
38
51
  expect(slots.map((s) => s.rootName)).toEqual(['PathParams'])
39
52
  })
40
53
 
41
- it('attaches the source schema verbatim to each slot', () => {
54
+ it('attaches source schemas verbatim', () => {
42
55
  const path = { type: 'object', tag: 'p' }
43
- const ret = { type: 'object', tag: 'r' }
44
- const slots = extractRouteSlots(r({ input: { pathParams: path }, returnType: ret }))
56
+ const body = { type: 'object', tag: 'b' }
57
+ const slots = extractRouteSlots(apiRoute({ req: { pathParams: path, body }, res: {} }))
45
58
  expect(slots).toEqual([
46
59
  { rootName: 'PathParams', source: path },
47
- { rootName: 'Response', source: ret },
60
+ { rootName: 'Body', source: body },
61
+ ])
62
+ })
63
+
64
+ it('reads responseHeaders from res.headers', () => {
65
+ const resHeaders = { type: 'object', tag: 'rh' }
66
+ const slots = extractRouteSlots(apiRoute({ res: { headers: resHeaders } }))
67
+ expect(slots).toEqual([{ rootName: 'ResponseHeaders', source: resHeaders }])
68
+ })
69
+ })
70
+
71
+ describe('extractRouteSlots — http-stream kind', () => {
72
+ it('returns no slots when jsonSchema is empty', () => {
73
+ expect(extractRouteSlots(httpStreamRoute({}))).toEqual([])
74
+ })
75
+
76
+ it('returns the deterministic slot order: PathParams, Query, Body, Headers, ResponseHeaders, Yield, ReturnType', () => {
77
+ const slots = extractRouteSlots(
78
+ httpStreamRoute({
79
+ req: {
80
+ pathParams: { type: 'object', tag: 'path' },
81
+ query: { type: 'object', tag: 'query' },
82
+ body: { type: 'object', tag: 'body' },
83
+ headers: { type: 'object', tag: 'headers' },
84
+ },
85
+ res: { headers: { type: 'object', tag: 'resHeaders' } },
86
+ yield: { type: 'object', tag: 'yield' },
87
+ returnType: { type: 'object', tag: 'return' },
88
+ }),
89
+ )
90
+ expect(slots.map((s) => s.rootName)).toEqual([
91
+ 'PathParams', 'Query', 'Body', 'Headers', 'ResponseHeaders', 'Yield', 'ReturnType',
48
92
  ])
49
93
  })
50
94
 
51
- it('returns no slots for stream/no-schema routes', () => {
52
- const stream = { name: 'S', kind: 'stream', method: 'GET', path: '/s' } as unknown as AnyHttpRouteDoc
53
- expect(extractRouteSlots(stream)).toEqual([])
95
+ it('attaches yield and returnType sources correctly', () => {
96
+ const yieldSchema = { type: 'object', tag: 'y' }
97
+ const returnSchema = { type: 'object', tag: 'r' }
98
+ const slots = extractRouteSlots(httpStreamRoute({ yield: yieldSchema, returnType: returnSchema }))
99
+ expect(slots).toEqual([
100
+ { rootName: 'Yield', source: yieldSchema },
101
+ { rootName: 'ReturnType', source: returnSchema },
102
+ ])
103
+ })
104
+ })
105
+
106
+ describe('extractRouteSlots — rpc kind', () => {
107
+ it('returns no slots when jsonSchema is empty', () => {
108
+ expect(extractRouteSlots(rpcRoute({}))).toEqual([])
109
+ })
110
+
111
+ it('maps body and response slots', () => {
112
+ const body = { type: 'object', tag: 'b' }
113
+ const response = { type: 'object', tag: 'r' }
114
+ const slots = extractRouteSlots(rpcRoute({ body, response }))
115
+ expect(slots).toEqual([
116
+ { rootName: 'Body', source: body },
117
+ { rootName: 'Response', source: response },
118
+ ])
119
+ })
120
+ })
121
+
122
+ describe('extractRouteSlots — stream kind', () => {
123
+ it('returns no slots when jsonSchema is empty', () => {
124
+ expect(extractRouteSlots(streamRoute({}))).toEqual([])
125
+ })
126
+
127
+ it('maps params→Body, yieldType→Yield, returnType→ReturnType', () => {
128
+ const params = { type: 'object', tag: 'p' }
129
+ const yieldType = { type: 'object', tag: 'y' }
130
+ const returnType = { type: 'object', tag: 'r' }
131
+ const slots = extractRouteSlots(streamRoute({ params, yieldType, returnType }))
132
+ expect(slots).toEqual([
133
+ { rootName: 'Body', source: params },
134
+ { rootName: 'Yield', source: yieldType },
135
+ { rootName: 'ReturnType', source: returnType },
136
+ ])
54
137
  })
55
138
  })
@@ -1,9 +1,8 @@
1
1
  import type { AnyHttpRouteDoc } from '../../../implementations/types.js'
2
2
 
3
3
  /**
4
- * A "slot" is one of the deterministic schema sources a route exposes:
5
- * `pathParams`, `query`, `body` (under `schema.input`) and the response
6
- * (`schema.returnType`). Targets emit one type per non-null slot.
4
+ * A "slot" is one of the deterministic schema sources a route exposes.
5
+ * Targets emit one type per non-null slot.
7
6
  */
8
7
  export interface RouteSlot {
9
8
  /** Stable identifier used as the emitted type's `rootTypeName`. */
@@ -15,18 +14,56 @@ export interface RouteSlot {
15
14
  /**
16
15
  * Returns the deterministic ordered slot list for a route, filtered to slots
17
16
  * with non-null sources. Order is fixed at the module level for stable output.
17
+ *
18
+ * Kind dispatch:
19
+ * - `'api'` — reads from `jsonSchema.req` / `jsonSchema.res` (new envelope shape)
20
+ * - `'http-stream'` — reads from `jsonSchema.req` / `jsonSchema.res` / `jsonSchema.yield` / `jsonSchema.returnType`
21
+ * - `'rpc'` — reads from `jsonSchema.body` / `jsonSchema.response`
22
+ * - `'stream'` — reads from `jsonSchema.params` / `jsonSchema.yieldType` / `jsonSchema.returnType`
18
23
  */
19
24
  export function extractRouteSlots(route: AnyHttpRouteDoc): RouteSlot[] {
20
- const schema = (route as { schema?: Record<string, unknown> }).schema ?? {}
21
- const input = (schema.input ?? {}) as Record<string, unknown>
25
+ const kind = route.kind
22
26
 
23
27
  // Order is load-bearing: targets emit slots in this sequence.
24
- const slots: RouteSlot[] = [
25
- { rootName: 'PathParams', source: input.pathParams },
26
- { rootName: 'Query', source: input.query },
27
- { rootName: 'Body', source: input.body },
28
- { rootName: 'Response', source: schema.returnType },
29
- ]
28
+ let slots: RouteSlot[]
29
+
30
+ if (kind === 'api') {
31
+ const req = route.jsonSchema.req ?? {}
32
+ const res = route.jsonSchema.res ?? {}
33
+ slots = [
34
+ { rootName: 'PathParams', source: req.pathParams },
35
+ { rootName: 'Query', source: req.query },
36
+ { rootName: 'Body', source: req.body },
37
+ { rootName: 'Headers', source: req.headers },
38
+ { rootName: 'Response', source: res.body },
39
+ { rootName: 'ResponseHeaders', source: res.headers },
40
+ ]
41
+ } else if (kind === 'http-stream') {
42
+ const req = route.jsonSchema.req ?? {}
43
+ const res = route.jsonSchema.res ?? {}
44
+ slots = [
45
+ { rootName: 'PathParams', source: req.pathParams },
46
+ { rootName: 'Query', source: req.query },
47
+ { rootName: 'Body', source: req.body },
48
+ { rootName: 'Headers', source: req.headers },
49
+ { rootName: 'ResponseHeaders', source: res.headers },
50
+ { rootName: 'Yield', source: route.jsonSchema.yield },
51
+ { rootName: 'ReturnType', source: route.jsonSchema.returnType },
52
+ ]
53
+ } else if (kind === 'rpc') {
54
+ slots = [
55
+ { rootName: 'Body', source: route.jsonSchema.body },
56
+ { rootName: 'Response', source: route.jsonSchema.response },
57
+ ]
58
+ } else if (kind === 'stream') {
59
+ slots = [
60
+ { rootName: 'Body', source: route.jsonSchema.params },
61
+ { rootName: 'Yield', source: route.jsonSchema.yieldType },
62
+ { rootName: 'ReturnType', source: route.jsonSchema.returnType },
63
+ ]
64
+ } else {
65
+ slots = []
66
+ }
30
67
 
31
68
  return slots.filter((s) => s.source != null)
32
69
  }
@@ -118,4 +118,77 @@ object Users {
118
118
  )
119
119
  }
120
120
  }
121
+
122
+ object DownloadUser {
123
+ const val method = "GET"
124
+ const val pathTemplate = "/users/{id}/download"
125
+ fun path(p: PathParams): String = "/users/${p.id}/download"
126
+
127
+ @Serializable
128
+ data class PathParams(
129
+ val id: String,
130
+ )
131
+
132
+ @Serializable
133
+ data class Response(
134
+ val id: String,
135
+ val name: String,
136
+ @SerialName("created-at") @Contextual val createdAt: java.time.Instant,
137
+ val address: Address,
138
+ ) {
139
+ @Serializable
140
+ data class Address(
141
+ val street: String,
142
+ val city: String,
143
+ )
144
+ }
145
+
146
+ @Serializable
147
+ data class ResponseHeaders(
148
+ @SerialName("x-download-token") val xDownloadToken: String,
149
+ @SerialName("content-disposition") val contentDisposition: String? = null,
150
+ )
151
+ }
152
+
153
+ object WatchUsers {
154
+ const val method = "GET"
155
+ const val pathTemplate = "/users/watch"
156
+ const val path = "/users/watch"
157
+
158
+ @Serializable
159
+ data class Query(
160
+ val status: Status? = null,
161
+ val limit: Long? = null,
162
+ ) {
163
+ @Serializable
164
+ enum class Status {
165
+ @SerialName("active") ACTIVE,
166
+ @SerialName("inactive") INACTIVE,
167
+ }
168
+ }
169
+
170
+ @Serializable
171
+ data class ResponseHeaders(
172
+ @SerialName("x-download-token") val xDownloadToken: String,
173
+ @SerialName("content-disposition") val contentDisposition: String? = null,
174
+ )
175
+
176
+ @Serializable
177
+ data class Yield(
178
+ val id: String,
179
+ val event: Event,
180
+ ) {
181
+ @Serializable
182
+ enum class Event {
183
+ @SerialName("created") CREATED,
184
+ @SerialName("updated") UPDATED,
185
+ @SerialName("deleted") DELETED,
186
+ }
187
+ }
188
+
189
+ @Serializable
190
+ data class ReturnType(
191
+ val count: Long,
192
+ )
193
+ }
121
194
  }
@@ -34,11 +34,9 @@ describe('emitKotlinRoute', () => {
34
34
  name: 'GetUser',
35
35
  method: 'GET',
36
36
  fullPath: '/users/:id',
37
- schema: {
38
- input: {
39
- pathParams: { type: 'object' },
40
- },
41
- returnType: { type: 'object' },
37
+ jsonSchema: {
38
+ req: { pathParams: { type: 'object' } },
39
+ res: { body: { type: 'object' } },
42
40
  },
43
41
  errors: [],
44
42
  } as unknown as AnyHttpRouteDoc
@@ -64,7 +62,10 @@ describe('emitKotlinRoute', () => {
64
62
  name: 'CreateUser',
65
63
  method: 'POST',
66
64
  fullPath: '/users',
67
- schema: { input: { body: { type: 'object' } }, returnType: { type: 'object' } },
65
+ jsonSchema: {
66
+ req: { body: { type: 'object' } },
67
+ res: { body: { type: 'object' } },
68
+ },
68
69
  errors: [],
69
70
  } as unknown as AnyHttpRouteDoc
70
71
 
@@ -84,7 +85,10 @@ describe('emitKotlinRoute', () => {
84
85
  name: 'GetUser',
85
86
  method: 'GET',
86
87
  fullPath: '/users/:id',
87
- schema: { input: { pathParams: { type: 'object' } }, returnType: { type: 'object' } },
88
+ jsonSchema: {
89
+ req: { pathParams: { type: 'object' } },
90
+ res: { body: { type: 'object' } },
91
+ },
88
92
  errors: ['NotFound'],
89
93
  } as unknown as AnyHttpRouteDoc
90
94
 
@@ -103,14 +107,14 @@ describe('emitKotlinRoute', () => {
103
107
  it('silently skips error keys with no schema in the envelope map', () => {
104
108
  const route = {
105
109
  kind: 'api', name: 'GetUser', method: 'GET', fullPath: '/users',
106
- schema: {}, errors: ['UnknownTaxonomyKey'],
110
+ jsonSchema: {}, errors: ['UnknownTaxonomyKey'],
107
111
  } as unknown as AnyHttpRouteDoc
108
112
  const result = emitKotlinRoute(route, createStubKotlinEmitter({}), new Map())
109
113
  expect(result.code).not.toContain('object Errors {')
110
114
  })
111
115
 
112
116
  it('returns skipped:true for stream routes', () => {
113
- const route = { kind: 'stream', name: 'WatchUsers', method: 'GET', path: '/users/stream', schema: {}, errors: [] } as unknown as AnyHttpRouteDoc
117
+ const route = { kind: 'stream', name: 'WatchUsers', method: 'GET', path: '/users/stream', jsonSchema: {}, errors: [] } as unknown as AnyHttpRouteDoc
114
118
  const result = emitKotlinRoute(route, createStubKotlinEmitter({}), noErrors)
115
119
  expect(result.code).toBe('')
116
120
  expect(result.skipped).toBe(true)
@@ -122,9 +126,9 @@ describe('emitKotlinRoute', () => {
122
126
  name: 'GetUser',
123
127
  method: 'GET',
124
128
  fullPath: '/users/:id',
125
- schema: {
126
- input: { pathParams: { type: 'object' } },
127
- returnType: { type: 'object' },
129
+ jsonSchema: {
130
+ req: { pathParams: { type: 'object' } },
131
+ res: { body: { type: 'object' } },
128
132
  },
129
133
  errors: ['NotFound'],
130
134
  } as unknown as AnyHttpRouteDoc
@@ -155,13 +159,13 @@ describe('emitKotlinRoute', () => {
155
159
  name: 'X',
156
160
  method: 'POST',
157
161
  fullPath: '/x/:id',
158
- schema: {
159
- input: {
162
+ jsonSchema: {
163
+ req: {
160
164
  pathParams: { type: 'object' },
161
165
  query: { type: 'object' },
162
166
  body: { type: 'object' },
163
167
  },
164
- returnType: { type: 'object' },
168
+ res: { body: { type: 'object' } },
165
169
  },
166
170
  errors: ['Z'],
167
171
  } as unknown as AnyHttpRouteDoc
@@ -187,7 +191,7 @@ describe('emitKotlinRoute', () => {
187
191
  name: 'X',
188
192
  method: 'GET',
189
193
  fullPath: '/x',
190
- schema: { returnType: { type: 'object' } },
194
+ jsonSchema: { res: { body: { type: 'object' } } },
191
195
  errors: [],
192
196
  } as unknown as AnyHttpRouteDoc
193
197
 
@@ -207,13 +211,92 @@ describe('emitKotlinRoute', () => {
207
211
  expect(calls[0]!.opts.uncountableWords).toEqual(['data', 'metadata'])
208
212
  })
209
213
 
214
+ it('emits ResponseHeaders slot when res.headers is declared on an api route', () => {
215
+ const route = {
216
+ kind: 'api',
217
+ name: 'DownloadUser',
218
+ method: 'GET',
219
+ fullPath: '/users/:id/download',
220
+ jsonSchema: {
221
+ req: { pathParams: { type: 'object' } },
222
+ res: {
223
+ body: { type: 'object' },
224
+ headers: { type: 'object' },
225
+ },
226
+ },
227
+ errors: [],
228
+ } as unknown as AnyHttpRouteDoc
229
+
230
+ const { emitter, calls } = makeSpyEmitter({
231
+ PathParams: ok('@Serializable data class PathParams(val id: String)', 'PathParams'),
232
+ Response: ok('@Serializable data class Response(val url: String)', 'Response'),
233
+ ResponseHeaders: ok(
234
+ '@Serializable\ndata class ResponseHeaders(\n @SerialName("x-download-token") val xDownloadToken: String,\n)',
235
+ 'ResponseHeaders',
236
+ ),
237
+ })
238
+
239
+ const result = emitKotlinRoute(route, emitter, noErrors)
240
+ expect(calls.map((c) => c.opts.rootTypeName)).toEqual(['PathParams', 'Response', 'ResponseHeaders'])
241
+ expect(result.code).toContain('@SerialName("x-download-token") val xDownloadToken: String')
242
+ })
243
+
244
+ it('does not emit ResponseHeaders when res.headers is absent', () => {
245
+ const route = {
246
+ kind: 'api',
247
+ name: 'GetUser',
248
+ method: 'GET',
249
+ fullPath: '/users/:id',
250
+ jsonSchema: {
251
+ req: { pathParams: { type: 'object' } },
252
+ res: { body: { type: 'object' } },
253
+ },
254
+ errors: [],
255
+ } as unknown as AnyHttpRouteDoc
256
+
257
+ const { emitter, calls } = makeSpyEmitter({
258
+ PathParams: ok('@Serializable data class PathParams(val id: String)', 'PathParams'),
259
+ Response: ok('@Serializable data class Response(val id: String)', 'Response'),
260
+ })
261
+
262
+ emitKotlinRoute(route, emitter, noErrors)
263
+ expect(calls.map((c) => c.opts.rootTypeName)).not.toContain('ResponseHeaders')
264
+ })
265
+
266
+ it('processes http-stream routes (not skipped) and emits Yield/ReturnType slots', () => {
267
+ const route = {
268
+ kind: 'http-stream',
269
+ name: 'TailLogs',
270
+ method: 'GET',
271
+ fullPath: '/logs/tail',
272
+ jsonSchema: {
273
+ req: { query: { type: 'object' } },
274
+ yield: { type: 'object' },
275
+ returnType: { type: 'object' },
276
+ },
277
+ errors: [],
278
+ } as unknown as AnyHttpRouteDoc
279
+
280
+ const { emitter, calls } = makeSpyEmitter({
281
+ Query: ok('@Serializable data class Query(val filter: String? = null)', 'Query'),
282
+ Yield: ok('@Serializable data class Yield(val line: String)', 'Yield'),
283
+ ReturnType: ok('@Serializable data class ReturnType(val count: Long)', 'ReturnType'),
284
+ })
285
+
286
+ const result = emitKotlinRoute(route, emitter, noErrors)
287
+ expect(result.skipped).toBeFalsy()
288
+ expect(calls.map((c) => c.opts.rootTypeName)).toEqual(['Query', 'Yield', 'ReturnType'])
289
+ expect(result.code).toContain('const val method = "GET"')
290
+ expect(result.code).toContain('const val path = "/logs/tail"')
291
+ })
292
+
210
293
  it('does not include passthrough keys when caller omits them', () => {
211
294
  const route = {
212
295
  kind: 'api',
213
296
  name: 'X',
214
297
  method: 'GET',
215
298
  fullPath: '/x',
216
- schema: { returnType: { type: 'object' } },
299
+ jsonSchema: { res: { body: { type: 'object' } } },
217
300
  errors: [],
218
301
  } as unknown as AnyHttpRouteDoc
219
302
 
@@ -18,7 +18,10 @@ describe('emitKotlinScope', () => {
18
18
  name: 'GetUser',
19
19
  method: 'GET',
20
20
  fullPath: '/users/:id',
21
- schema: { input: { pathParams: { type: 'object' } }, returnType: { type: 'object' } },
21
+ jsonSchema: {
22
+ req: { pathParams: { type: 'object' } },
23
+ res: { body: { type: 'object' } },
24
+ },
22
25
  errors: [],
23
26
  } as unknown as AnyHttpRouteDoc
24
27
 
@@ -40,8 +43,8 @@ describe('emitKotlinScope', () => {
40
43
  })
41
44
 
42
45
  it('joins multiple routes inside one scope object', () => {
43
- const route1 = { kind: 'api', name: 'GetUser', method: 'GET', fullPath: '/users/:id', schema: {}, errors: [] } as unknown as AnyHttpRouteDoc
44
- const route2 = { kind: 'api', name: 'CreateUser', method: 'POST', fullPath: '/users', schema: {}, errors: [] } as unknown as AnyHttpRouteDoc
46
+ const route1 = { kind: 'api', name: 'GetUser', method: 'GET', fullPath: '/users/:id', jsonSchema: {}, errors: [] } as unknown as AnyHttpRouteDoc
47
+ const route2 = { kind: 'api', name: 'CreateUser', method: 'POST', fullPath: '/users', jsonSchema: {}, errors: [] } as unknown as AnyHttpRouteDoc
45
48
  const group: ScopeGroup = { scopeKey: 'users', camelCase: 'users', routes: [route1, route2] }
46
49
  const emitter = createStubKotlinEmitter({})
47
50
 
@@ -60,7 +63,7 @@ describe('emitKotlinScope', () => {
60
63
  })
61
64
 
62
65
  it('threads all 5 passthrough opts to every emitter call', () => {
63
- const route = { kind: 'api', name: 'X', method: 'GET', fullPath: '/x', schema: { returnType: { type: 'object' } }, errors: [] } as unknown as AnyHttpRouteDoc
66
+ const route = { kind: 'api', name: 'X', method: 'GET', fullPath: '/x', jsonSchema: { res: { body: { type: 'object' } } }, errors: [] } as unknown as AnyHttpRouteDoc
64
67
  const group: ScopeGroup = { scopeKey: 'x', camelCase: 'x', routes: [route] }
65
68
 
66
69
  const calls: KotlinEmitOptions[] = []
@@ -96,8 +99,8 @@ describe('emitKotlinScope', () => {
96
99
  })
97
100
 
98
101
  it('collects skipped stream-route names', () => {
99
- const stream = { kind: 'stream', name: 'WatchUsers', method: 'GET', path: '/u/stream', schema: {}, errors: [] } as unknown as AnyHttpRouteDoc
100
- const api = { kind: 'api', name: 'GetUser', method: 'GET', fullPath: '/u', schema: { returnType: { type: 'object' } }, errors: [] } as unknown as AnyHttpRouteDoc
102
+ const stream = { kind: 'stream', name: 'WatchUsers', method: 'GET', path: '/u/stream', jsonSchema: {}, errors: [] } as unknown as AnyHttpRouteDoc
103
+ const api = { kind: 'api', name: 'GetUser', method: 'GET', fullPath: '/u', jsonSchema: { res: { body: { type: 'object' } } }, errors: [] } as unknown as AnyHttpRouteDoc
101
104
  const group: ScopeGroup = { scopeKey: 'u', camelCase: 'u', routes: [stream, api] }
102
105
 
103
106
  const emitter = createStubKotlinEmitter({
@@ -61,6 +61,25 @@ describe('kotlin codegen — integration', () => {
61
61
  'Query',
62
62
  ['kotlinx.serialization.Serializable', 'kotlinx.serialization.SerialName'],
63
63
  ),
64
+
65
+ // DownloadUser
66
+ ResponseHeaders: ok(
67
+ '@Serializable\ndata class ResponseHeaders(\n @SerialName("x-download-token") val xDownloadToken: String,\n @SerialName("content-disposition") val contentDisposition: String? = null,\n)',
68
+ 'ResponseHeaders',
69
+ ['kotlinx.serialization.Serializable', 'kotlinx.serialization.SerialName'],
70
+ ),
71
+
72
+ // WatchUsers (http-stream) — reuses Query and ResponseHeaders stubs from above;
73
+ // adds Yield and ReturnType which are unique to the stream route.
74
+ Yield: ok(
75
+ '@Serializable\ndata class Yield(\n val id: String,\n val event: Event,\n) {\n @Serializable\n enum class Event {\n @SerialName("created") CREATED,\n @SerialName("updated") UPDATED,\n @SerialName("deleted") DELETED,\n }\n}',
76
+ 'Yield',
77
+ ['kotlinx.serialization.Serializable', 'kotlinx.serialization.SerialName'],
78
+ ),
79
+ ReturnType: ok(
80
+ '@Serializable\ndata class ReturnType(\n val count: Long,\n)',
81
+ 'ReturnType',
82
+ ),
64
83
  })
65
84
 
66
85
  const files = await runPipeline({
@@ -120,4 +120,83 @@ public enum Users {
120
120
  }
121
121
  }
122
122
  }
123
+
124
+ public enum DownloadUser {
125
+ public static let method = "GET"
126
+ public static let pathTemplate = "/users/{id}/download"
127
+ public static func path(_ p: PathParams) -> String { return "/users/\(p.id)/download" }
128
+
129
+ public struct PathParams: Codable {
130
+ public let id: String
131
+ }
132
+
133
+ public struct Response: Codable {
134
+ public let id: String
135
+ public let name: String
136
+ public let createdAt: Date
137
+ public let address: Address
138
+
139
+ enum CodingKeys: String, CodingKey {
140
+ case id, name
141
+ case createdAt = "created-at"
142
+ case address
143
+ }
144
+
145
+ public struct Address: Codable {
146
+ public let street: String
147
+ public let city: String
148
+ }
149
+ }
150
+
151
+ public struct ResponseHeaders: Codable {
152
+ public let xDownloadToken: String
153
+ public let contentDisposition: String?
154
+
155
+ enum CodingKeys: String, CodingKey {
156
+ case xDownloadToken = "x-download-token"
157
+ case contentDisposition = "content-disposition"
158
+ }
159
+ }
160
+ }
161
+
162
+ public enum WatchUsers {
163
+ public static let method = "GET"
164
+ public static let pathTemplate = "/users/watch"
165
+ public static let path = "/users/watch"
166
+
167
+ public struct Query: Codable {
168
+ public let status: Status?
169
+ public let limit: Int64?
170
+
171
+ public enum Status: String, Codable {
172
+ case active
173
+ case inactive
174
+ }
175
+ }
176
+
177
+ public struct ResponseHeaders: Codable {
178
+ public let xDownloadToken: String
179
+ public let contentDisposition: String?
180
+
181
+ enum CodingKeys: String, CodingKey {
182
+ case xDownloadToken = "x-download-token"
183
+ case contentDisposition = "content-disposition"
184
+ }
185
+ }
186
+
187
+ public struct Yield: Codable {
188
+ public let id: String
189
+ public let event: Event
190
+
191
+ public enum Event: String, Codable {
192
+ case created
193
+ case updated
194
+ case deleted
195
+ }
196
+ }
197
+
198
+ public struct ReturnType: Codable {
199
+ public let count: Int64
200
+ }
201
+ }
123
202
  }