@typespec/http-server-js 0.58.0-alpha.10-dev.3

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 (215) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/LICENSE +21 -0
  3. package/README.md +183 -0
  4. package/build-helpers.ts +170 -0
  5. package/dist/generated-defs/helpers/header.d.ts +4 -0
  6. package/dist/generated-defs/helpers/header.d.ts.map +1 -0
  7. package/dist/generated-defs/helpers/header.js +76 -0
  8. package/dist/generated-defs/helpers/header.js.map +1 -0
  9. package/dist/generated-defs/helpers/http.d.ts +4 -0
  10. package/dist/generated-defs/helpers/http.d.ts.map +1 -0
  11. package/dist/generated-defs/helpers/http.js +134 -0
  12. package/dist/generated-defs/helpers/http.js.map +1 -0
  13. package/dist/generated-defs/helpers/index.d.ts +4 -0
  14. package/dist/generated-defs/helpers/index.d.ts.map +1 -0
  15. package/dist/generated-defs/helpers/index.js +21 -0
  16. package/dist/generated-defs/helpers/index.js.map +1 -0
  17. package/dist/generated-defs/helpers/multipart.d.ts +4 -0
  18. package/dist/generated-defs/helpers/multipart.d.ts.map +1 -0
  19. package/dist/generated-defs/helpers/multipart.js +249 -0
  20. package/dist/generated-defs/helpers/multipart.js.map +1 -0
  21. package/dist/generated-defs/helpers/router.d.ts +4 -0
  22. package/dist/generated-defs/helpers/router.d.ts.map +1 -0
  23. package/dist/generated-defs/helpers/router.js +259 -0
  24. package/dist/generated-defs/helpers/router.js.map +1 -0
  25. package/dist/src/common/declaration.d.ts +13 -0
  26. package/dist/src/common/declaration.d.ts.map +1 -0
  27. package/dist/src/common/declaration.js +45 -0
  28. package/dist/src/common/declaration.js.map +1 -0
  29. package/dist/src/common/documentation.d.ts +12 -0
  30. package/dist/src/common/documentation.d.ts.map +1 -0
  31. package/dist/src/common/documentation.js +21 -0
  32. package/dist/src/common/documentation.js.map +1 -0
  33. package/dist/src/common/enum.d.ts +10 -0
  34. package/dist/src/common/enum.d.ts.map +1 -0
  35. package/dist/src/common/enum.js +21 -0
  36. package/dist/src/common/enum.js.map +1 -0
  37. package/dist/src/common/interface.d.ts +50 -0
  38. package/dist/src/common/interface.d.ts.map +1 -0
  39. package/dist/src/common/interface.js +194 -0
  40. package/dist/src/common/interface.js.map +1 -0
  41. package/dist/src/common/model.d.ts +26 -0
  42. package/dist/src/common/model.d.ts.map +1 -0
  43. package/dist/src/common/model.js +115 -0
  44. package/dist/src/common/model.js.map +1 -0
  45. package/dist/src/common/namespace.d.ts +38 -0
  46. package/dist/src/common/namespace.d.ts.map +1 -0
  47. package/dist/src/common/namespace.js +184 -0
  48. package/dist/src/common/namespace.js.map +1 -0
  49. package/dist/src/common/reference.d.ts +46 -0
  50. package/dist/src/common/reference.d.ts.map +1 -0
  51. package/dist/src/common/reference.js +243 -0
  52. package/dist/src/common/reference.js.map +1 -0
  53. package/dist/src/common/scalar.d.ts +50 -0
  54. package/dist/src/common/scalar.d.ts.map +1 -0
  55. package/dist/src/common/scalar.js +144 -0
  56. package/dist/src/common/scalar.js.map +1 -0
  57. package/dist/src/common/serialization/index.d.ts +11 -0
  58. package/dist/src/common/serialization/index.d.ts.map +1 -0
  59. package/dist/src/common/serialization/index.js +72 -0
  60. package/dist/src/common/serialization/index.js.map +1 -0
  61. package/dist/src/common/serialization/json.d.ts +6 -0
  62. package/dist/src/common/serialization/json.d.ts.map +1 -0
  63. package/dist/src/common/serialization/json.js +341 -0
  64. package/dist/src/common/serialization/json.js.map +1 -0
  65. package/dist/src/common/union.d.ts +23 -0
  66. package/dist/src/common/union.d.ts.map +1 -0
  67. package/dist/src/common/union.js +57 -0
  68. package/dist/src/common/union.js.map +1 -0
  69. package/dist/src/ctx.d.ts +242 -0
  70. package/dist/src/ctx.d.ts.map +1 -0
  71. package/dist/src/ctx.js +211 -0
  72. package/dist/src/ctx.js.map +1 -0
  73. package/dist/src/helpers/header.d.ts +14 -0
  74. package/dist/src/helpers/header.d.ts.map +1 -0
  75. package/dist/src/helpers/header.js +38 -0
  76. package/dist/src/helpers/header.js.map +1 -0
  77. package/dist/src/helpers/http.d.ts +70 -0
  78. package/dist/src/helpers/http.d.ts.map +1 -0
  79. package/dist/src/helpers/http.js +86 -0
  80. package/dist/src/helpers/http.js.map +1 -0
  81. package/dist/src/helpers/multipart.d.ts +26 -0
  82. package/dist/src/helpers/multipart.d.ts.map +1 -0
  83. package/dist/src/helpers/multipart.js +182 -0
  84. package/dist/src/helpers/multipart.js.map +1 -0
  85. package/dist/src/helpers/router.d.ts +176 -0
  86. package/dist/src/helpers/router.d.ts.map +1 -0
  87. package/dist/src/helpers/router.js +55 -0
  88. package/dist/src/helpers/router.js.map +1 -0
  89. package/dist/src/http/index.d.ts +24 -0
  90. package/dist/src/http/index.d.ts.map +1 -0
  91. package/dist/src/http/index.js +52 -0
  92. package/dist/src/http/index.js.map +1 -0
  93. package/dist/src/http/server/index.d.ts +11 -0
  94. package/dist/src/http/server/index.d.ts.map +1 -0
  95. package/dist/src/http/server/index.js +413 -0
  96. package/dist/src/http/server/index.js.map +1 -0
  97. package/dist/src/http/server/multipart.d.ts +16 -0
  98. package/dist/src/http/server/multipart.d.ts.map +1 -0
  99. package/dist/src/http/server/multipart.js +214 -0
  100. package/dist/src/http/server/multipart.js.map +1 -0
  101. package/dist/src/http/server/router.d.ts +15 -0
  102. package/dist/src/http/server/router.d.ts.map +1 -0
  103. package/dist/src/http/server/router.js +459 -0
  104. package/dist/src/http/server/router.js.map +1 -0
  105. package/dist/src/index.d.ts +5 -0
  106. package/dist/src/index.d.ts.map +1 -0
  107. package/dist/src/index.js +38 -0
  108. package/dist/src/index.js.map +1 -0
  109. package/dist/src/lib.d.ts +141 -0
  110. package/dist/src/lib.d.ts.map +1 -0
  111. package/dist/src/lib.js +116 -0
  112. package/dist/src/lib.js.map +1 -0
  113. package/dist/src/scripts/scaffold/bin.d.mts +14 -0
  114. package/dist/src/scripts/scaffold/bin.d.mts.map +1 -0
  115. package/dist/src/scripts/scaffold/bin.mjs +559 -0
  116. package/dist/src/scripts/scaffold/bin.mjs.map +1 -0
  117. package/dist/src/testing/index.d.ts +3 -0
  118. package/dist/src/testing/index.d.ts.map +1 -0
  119. package/dist/src/testing/index.js +6 -0
  120. package/dist/src/testing/index.js.map +1 -0
  121. package/dist/src/util/case.d.ts +81 -0
  122. package/dist/src/util/case.d.ts.map +1 -0
  123. package/dist/src/util/case.js +111 -0
  124. package/dist/src/util/case.js.map +1 -0
  125. package/dist/src/util/differentiate.d.ts +251 -0
  126. package/dist/src/util/differentiate.d.ts.map +1 -0
  127. package/dist/src/util/differentiate.js +580 -0
  128. package/dist/src/util/differentiate.js.map +1 -0
  129. package/dist/src/util/error.d.ts +13 -0
  130. package/dist/src/util/error.d.ts.map +1 -0
  131. package/dist/src/util/error.js +25 -0
  132. package/dist/src/util/error.js.map +1 -0
  133. package/dist/src/util/extends.d.ts +10 -0
  134. package/dist/src/util/extends.d.ts.map +1 -0
  135. package/dist/src/util/extends.js +31 -0
  136. package/dist/src/util/extends.js.map +1 -0
  137. package/dist/src/util/iter.d.ts +39 -0
  138. package/dist/src/util/iter.d.ts.map +1 -0
  139. package/dist/src/util/iter.js +72 -0
  140. package/dist/src/util/iter.js.map +1 -0
  141. package/dist/src/util/keywords.d.ts +10 -0
  142. package/dist/src/util/keywords.d.ts.map +1 -0
  143. package/dist/src/util/keywords.js +85 -0
  144. package/dist/src/util/keywords.js.map +1 -0
  145. package/dist/src/util/name.d.ts +12 -0
  146. package/dist/src/util/name.d.ts.map +1 -0
  147. package/dist/src/util/name.js +26 -0
  148. package/dist/src/util/name.js.map +1 -0
  149. package/dist/src/util/once-queue.d.ts +24 -0
  150. package/dist/src/util/once-queue.d.ts.map +1 -0
  151. package/dist/src/util/once-queue.js +34 -0
  152. package/dist/src/util/once-queue.js.map +1 -0
  153. package/dist/src/util/openapi3.d.ts +23 -0
  154. package/dist/src/util/openapi3.d.ts.map +1 -0
  155. package/dist/src/util/openapi3.js +40 -0
  156. package/dist/src/util/openapi3.js.map +1 -0
  157. package/dist/src/util/pluralism.d.ts +23 -0
  158. package/dist/src/util/pluralism.d.ts.map +1 -0
  159. package/dist/src/util/pluralism.js +36 -0
  160. package/dist/src/util/pluralism.js.map +1 -0
  161. package/dist/src/util/scope.d.ts +85 -0
  162. package/dist/src/util/scope.d.ts.map +1 -0
  163. package/dist/src/util/scope.js +111 -0
  164. package/dist/src/util/scope.js.map +1 -0
  165. package/dist/src/write.d.ts +23 -0
  166. package/dist/src/write.d.ts.map +1 -0
  167. package/dist/src/write.js +62 -0
  168. package/dist/src/write.js.map +1 -0
  169. package/generated-defs/helpers/header.ts +83 -0
  170. package/generated-defs/helpers/http.ts +141 -0
  171. package/generated-defs/helpers/index.ts +27 -0
  172. package/generated-defs/helpers/multipart.ts +256 -0
  173. package/generated-defs/helpers/router.ts +266 -0
  174. package/package.json +71 -0
  175. package/src/common/declaration.ts +52 -0
  176. package/src/common/documentation.ts +26 -0
  177. package/src/common/enum.ts +28 -0
  178. package/src/common/interface.ts +264 -0
  179. package/src/common/model.ts +160 -0
  180. package/src/common/namespace.ts +243 -0
  181. package/src/common/reference.ts +319 -0
  182. package/src/common/scalar.ts +173 -0
  183. package/src/common/serialization/index.ts +124 -0
  184. package/src/common/serialization/json.ts +444 -0
  185. package/src/common/union.ts +76 -0
  186. package/src/ctx.ts +497 -0
  187. package/src/helpers/header.ts +55 -0
  188. package/src/helpers/http.ts +113 -0
  189. package/src/helpers/multipart.ts +228 -0
  190. package/src/helpers/router.ts +238 -0
  191. package/src/http/index.ts +81 -0
  192. package/src/http/server/index.ts +548 -0
  193. package/src/http/server/multipart.ts +272 -0
  194. package/src/http/server/router.ts +686 -0
  195. package/src/index.ts +56 -0
  196. package/src/lib.ts +130 -0
  197. package/src/scripts/scaffold/bin.mts +781 -0
  198. package/src/testing/index.ts +10 -0
  199. package/src/util/case.ts +182 -0
  200. package/src/util/differentiate.ts +957 -0
  201. package/src/util/error.ts +28 -0
  202. package/src/util/extends.ts +43 -0
  203. package/src/util/iter.ts +85 -0
  204. package/src/util/keywords.ts +90 -0
  205. package/src/util/name.ts +33 -0
  206. package/src/util/once-queue.ts +55 -0
  207. package/src/util/openapi3.ts +53 -0
  208. package/src/util/pluralism.ts +37 -0
  209. package/src/util/scope.ts +211 -0
  210. package/src/write.ts +88 -0
  211. package/temp/tsconfig.tsbuildinfo +1 -0
  212. package/test/header.test.ts +26 -0
  213. package/test/multipart.test.ts +169 -0
  214. package/tsconfig.json +10 -0
  215. package/vitest.config.ts +4 -0
@@ -0,0 +1,686 @@
1
+ // Copyright (c) Microsoft Corporation
2
+ // Licensed under the MIT license.
3
+
4
+ import { Operation, Type } from "@typespec/compiler";
5
+ import {
6
+ HttpOperation,
7
+ HttpService,
8
+ HttpVerb,
9
+ OperationContainer,
10
+ getHttpOperation,
11
+ } from "@typespec/http";
12
+ import {
13
+ createOrGetModuleForNamespace,
14
+ emitNamespaceInterfaceReference,
15
+ } from "../../common/namespace.js";
16
+ import { emitTypeReference } from "../../common/reference.js";
17
+ import { Module, createModule } from "../../ctx.js";
18
+ import { ReCase, parseCase } from "../../util/case.js";
19
+ import { bifilter, indent } from "../../util/iter.js";
20
+ import { keywordSafe } from "../../util/keywords.js";
21
+ import { HttpContext } from "../index.js";
22
+
23
+ import { module as headerHelpers } from "../../../generated-defs/helpers/header.js";
24
+ import { module as routerHelper } from "../../../generated-defs/helpers/router.js";
25
+ import { parseHeaderValueParameters } from "../../helpers/header.js";
26
+ import { reportDiagnostic } from "../../lib.js";
27
+ import { UnimplementedError } from "../../util/error.js";
28
+
29
+ /**
30
+ * Emit a router for the HTTP operations defined in a given service.
31
+ *
32
+ * The generated router will use optimal prefix matching to dispatch requests to the appropriate underlying
33
+ * implementation using the raw server.
34
+ *
35
+ * @param ctx - The emitter context.
36
+ * @param service - The HTTP service to emit a router for.
37
+ * @param serverRawModule - The module that contains the raw server implementation.
38
+ */
39
+ export function emitRouter(ctx: HttpContext, service: HttpService, serverRawModule: Module) {
40
+ const routerModule = createModule("router", ctx.httpModule);
41
+
42
+ const routeTree = createRouteTree(ctx, service);
43
+
44
+ routerModule.imports.push({
45
+ binder: "* as http",
46
+ from: "node:http",
47
+ });
48
+
49
+ routerModule.imports.push({
50
+ binder: "* as serverRaw",
51
+ from: serverRawModule,
52
+ });
53
+
54
+ routerModule.imports.push({
55
+ binder: ["parseHeaderValueParameters"],
56
+ from: headerHelpers,
57
+ });
58
+
59
+ routerModule.declarations.push([...emitRouterDefinition(ctx, service, routeTree, routerModule)]);
60
+ }
61
+
62
+ /**
63
+ * Writes the code for a router of a given service.
64
+ *
65
+ * @param ctx - The emitter context.
66
+ * @param service - The HTTP service to emit a router for.
67
+ * @param routeTree - The service's route tree.
68
+ * @param module - The module we're writing to.
69
+ */
70
+ function* emitRouterDefinition(
71
+ ctx: HttpContext,
72
+ service: HttpService,
73
+ routeTree: RouteTree,
74
+ module: Module,
75
+ ): Iterable<string> {
76
+ const routerName = parseCase(service.namespace.name).pascalCase + "Router";
77
+
78
+ const uniqueContainers = new Set(service.operations.map((operation) => operation.container));
79
+
80
+ const backends = new Map<OperationContainer, [ReCase, string]>();
81
+
82
+ for (const container of uniqueContainers) {
83
+ const param = parseCase(container.name);
84
+
85
+ const traitConstraint =
86
+ container.kind === "Namespace"
87
+ ? emitNamespaceInterfaceReference(ctx, container, module)
88
+ : emitTypeReference(ctx, container, container, module);
89
+
90
+ module.imports.push({
91
+ binder: [param.pascalCase],
92
+ from: createOrGetModuleForNamespace(ctx, container.namespace!),
93
+ });
94
+
95
+ backends.set(container, [param, traitConstraint]);
96
+ }
97
+
98
+ module.imports.push({
99
+ binder: ["RouterOptions", "createPolicyChain", "createPolicyChainForRoute", "HttpContext"],
100
+ from: routerHelper,
101
+ });
102
+
103
+ yield `export interface ${routerName} {`;
104
+ yield ` /**`;
105
+ yield ` * Dispatches the request to the appropriate service based on the request path.`;
106
+ yield ` *`;
107
+ yield ` * This member function may be used directly as a handler for a Node HTTP server.`;
108
+ yield ` *`;
109
+ yield ` * @param request - The incoming HTTP request.`;
110
+ yield ` * @param response - The outgoing HTTP response.`;
111
+ yield ` */`;
112
+ yield ` dispatch(request: http.IncomingMessage, response: http.ServerResponse): void;`;
113
+
114
+ if (ctx.options.express) {
115
+ yield "";
116
+ yield ` /**`;
117
+ yield ` * An Express middleware function that dispatches the request to the appropriate service based on the request path.`;
118
+ yield ` *`;
119
+ yield ` * This member function may be used directly as an application-level middleware function in an Express app.`;
120
+ yield ` *`;
121
+ yield ` * If the router does not match a route, it will call the \`next\` middleware registered with the application,`;
122
+ yield ` * so it is sensible to insert this middleware at the beginning of the middleware stack.`;
123
+ yield ` *`;
124
+ yield ` * @param req - The incoming HTTP request.`;
125
+ yield ` * @param res - The outgoing HTTP response.`;
126
+ yield ` * @param next - The next middleware function in the stack.`;
127
+ yield ` */`;
128
+ yield ` expressMiddleware(req: http.IncomingMessage, res: http.ServerResponse, next: () => void): void;`;
129
+ }
130
+
131
+ yield "}";
132
+ yield "";
133
+
134
+ yield `export function create${routerName}(`;
135
+
136
+ for (const [param] of backends.values()) {
137
+ yield ` ${param.camelCase}: ${param.pascalCase},`;
138
+ }
139
+
140
+ yield ` options: RouterOptions<{`;
141
+ for (const [param] of backends.values()) {
142
+ yield ` ${param.camelCase}: ${param.pascalCase}<HttpContext>,`;
143
+ }
144
+ yield ` }> = {}`;
145
+ yield `): ${routerName} {`;
146
+
147
+ const [onRequestNotFound, onInvalidRequest, onInternalError] = [
148
+ "onRequestNotFound",
149
+ "onInvalidRequest",
150
+ "onInternalError",
151
+ ].map(ctx.gensym);
152
+
153
+ // Router error case handlers
154
+ yield ` const ${onRequestNotFound} = options.onRequestNotFound ?? ((ctx) => {`;
155
+ yield ` ctx.response.statusCode = 404;`;
156
+ yield ` ctx.response.setHeader("Content-Type", "text/plain");`;
157
+ yield ` ctx.response.end("Not Found");`;
158
+ yield ` });`;
159
+ yield "";
160
+ yield ` const ${onInvalidRequest} = options.onInvalidRequest ?? ((ctx, route, error) => {`;
161
+ yield ` ctx.response.statusCode = 400;`;
162
+ yield ` ctx.response.setHeader("Content-Type", "application/json");`;
163
+ yield ` ctx.response.end(JSON.stringify({ error }));`;
164
+ yield ` });`;
165
+ yield "";
166
+ yield ` const ${onInternalError} = options.onInternalError ?? ((ctx, error) => {`;
167
+ yield ` ctx.response.statusCode = 500;`;
168
+ yield ` ctx.response.setHeader("Content-Type", "text/plain");`;
169
+ yield ` ctx.response.end("Internal server error.");`;
170
+ yield ` });`;
171
+ yield "";
172
+
173
+ const routePolicies = ctx.gensym("routePolicies");
174
+ const routeHandlers = ctx.gensym("routeHandlers");
175
+
176
+ yield ` const ${routePolicies} = options.routePolicies ?? {};`;
177
+ yield "";
178
+ yield ` const ${routeHandlers} = {`;
179
+
180
+ // Policy chains for each operation
181
+ for (const operation of service.operations) {
182
+ const operationName = parseCase(operation.operation.name);
183
+ const containerName = parseCase(operation.container.name);
184
+
185
+ yield ` ${containerName.snakeCase}_${operationName.snakeCase}: createPolicyChainForRoute(`;
186
+ yield ` "${containerName.camelCase + operationName.pascalCase + "Dispatch"}",`;
187
+ yield ` ${routePolicies},`;
188
+ yield ` "${containerName.camelCase}",`;
189
+ yield ` "${operationName.camelCase}",`;
190
+ yield ` serverRaw.${containerName.snakeCase}_${operationName.snakeCase},`;
191
+ yield ` ),`;
192
+ }
193
+
194
+ yield ` } as const;`;
195
+ yield "";
196
+
197
+ // Core routing function definition
198
+ yield ` const dispatch = createPolicyChain("${routerName}Dispatch", options.policies ?? [], async function(ctx, request, response) {`;
199
+ yield ` const url = new URL(request.url!, \`http://\${request.headers.host}\`);`;
200
+ yield ` let path = url.pathname;`;
201
+ yield "";
202
+
203
+ yield* indent(indent(emitRouteHandler(ctx, routeHandlers, routeTree, backends, module)));
204
+
205
+ yield "";
206
+
207
+ yield ` return ctx.errorHandlers.onRequestNotFound(ctx);`;
208
+ yield ` });`;
209
+ yield "";
210
+
211
+ const errorHandlers = ctx.gensym("errorHandlers");
212
+
213
+ yield ` const ${errorHandlers} = {`;
214
+ yield ` onRequestNotFound: ${onRequestNotFound},`;
215
+ yield ` onInvalidRequest: ${onInvalidRequest},`;
216
+ yield ` onInternalError: ${onInternalError},`;
217
+ yield ` };`;
218
+
219
+ yield ` return {`;
220
+ yield ` dispatch(request, response) {`;
221
+ yield ` const ctx = { request, response, errorHandlers: ${errorHandlers} };`;
222
+ yield ` return dispatch(ctx, request, response).catch((e) => ${onInternalError}(ctx, e));`;
223
+ yield ` },`;
224
+
225
+ if (ctx.options.express) {
226
+ yield ` expressMiddleware: function (request, response, next) {`;
227
+ yield ` const ctx = { request, response, errorHandlers: ${errorHandlers} };`;
228
+ yield ` void dispatch(`;
229
+ yield ` { request, response, errorHandlers: {`;
230
+ yield ` ...${errorHandlers},`;
231
+ yield ` onRequestNotFound: function () { next() }`;
232
+ yield ` }},`;
233
+ yield ` request,`;
234
+ yield ` response`;
235
+ yield ` ).catch((e) => ${onInternalError}(ctx, e));`;
236
+ yield ` },`;
237
+ }
238
+
239
+ yield " }";
240
+ yield "}";
241
+ }
242
+
243
+ /**
244
+ * Writes handling code for a single route tree node.
245
+ *
246
+ * @param ctx - The emitter context.
247
+ * @param routeTree - The route tree node to write handling code for.
248
+ * @param backends - The map of backends for operations.
249
+ * @param module - The module we're writing to.
250
+ */
251
+ function* emitRouteHandler(
252
+ ctx: HttpContext,
253
+ routeHandlers: string,
254
+ routeTree: RouteTree,
255
+ backends: Map<OperationContainer, [ReCase, string]>,
256
+ module: Module,
257
+ ): Iterable<string> {
258
+ const mustTerminate = routeTree.edges.length === 0 && !routeTree.bind;
259
+
260
+ const onRouteNotFound = "ctx.errorHandlers.onRequestNotFound";
261
+
262
+ yield `if (path.length === 0) {`;
263
+ if (routeTree.operations.size > 0) {
264
+ yield* indent(emitRouteOperationDispatch(ctx, routeHandlers, routeTree.operations, backends));
265
+ } else {
266
+ // Not found
267
+ yield ` return ${onRouteNotFound}(ctx);`;
268
+ }
269
+ yield `}`;
270
+
271
+ if (mustTerminate) {
272
+ // Not found
273
+ yield "else {";
274
+ yield ` return ${onRouteNotFound}(ctx);`;
275
+ yield `}`;
276
+ return;
277
+ }
278
+
279
+ for (const [edge, nextTree] of routeTree.edges) {
280
+ const edgePattern = edge.length === 1 ? `'${edge}'` : JSON.stringify(edge);
281
+ yield `else if (path.startsWith(${edgePattern})) {`;
282
+ yield ` path = path.slice(${edge.length});`;
283
+ yield* indent(emitRouteHandler(ctx, routeHandlers, nextTree, backends, module));
284
+ yield "}";
285
+ }
286
+
287
+ if (routeTree.bind) {
288
+ const [parameterSet, nextTree] = routeTree.bind;
289
+ const parameters = [...parameterSet];
290
+
291
+ yield `else {`;
292
+ const paramName = parameters.length === 1 ? parameters[0] : "param";
293
+ const idxName = `__${parseCase(paramName).snakeCase}_idx`;
294
+ yield ` let ${idxName} = path.indexOf("/");`;
295
+ yield ` ${idxName} = ${idxName} === -1 ? path.length : ${idxName};`;
296
+ yield ` const ${paramName} = path.slice(0, ${idxName});`;
297
+ yield ` path = path.slice(${idxName});`;
298
+ if (parameters.length !== 1) {
299
+ for (const p of parameters) {
300
+ yield ` const ${parseCase(p).camelCase} = param;`;
301
+ }
302
+ }
303
+ yield* indent(emitRouteHandler(ctx, routeHandlers, nextTree, backends, module));
304
+
305
+ yield `}`;
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Writes the dispatch code for a specific set of operations mapped to the same route.
311
+ *
312
+ * @param ctx - The emitter context.
313
+ * @param operations - The operations mapped to the route.
314
+ * @param backends - The map of backends for operations.
315
+ */
316
+ function* emitRouteOperationDispatch(
317
+ ctx: HttpContext,
318
+ routeHandlers: string,
319
+ operations: Map<HttpVerb, RouteOperation[]>,
320
+ backends: Map<OperationContainer, [ReCase, string]>,
321
+ ): Iterable<string> {
322
+ yield `switch (request.method) {`;
323
+ for (const [verb, operationList] of operations.entries()) {
324
+ if (operationList.length === 1) {
325
+ const operation = operationList[0];
326
+ const [backend] = backends.get(operation.container)!;
327
+ const operationName = keywordSafe(
328
+ backend.snakeCase + "_" + parseCase(operation.operation.name).snakeCase,
329
+ );
330
+
331
+ const backendMemberName = backend.camelCase;
332
+
333
+ const parameters =
334
+ operation.parameters.length > 0
335
+ ? ", " + operation.parameters.map((param) => parseCase(param.name).camelCase).join(", ")
336
+ : "";
337
+
338
+ yield ` case ${JSON.stringify(verb.toUpperCase())}:`;
339
+ yield ` return ${routeHandlers}.${operationName}(ctx, ${backendMemberName}${parameters});`;
340
+ } else {
341
+ // Shared route
342
+ const route = getHttpOperation(ctx.program, operationList[0].operation)[0].path;
343
+ yield ` case ${JSON.stringify(verb.toUpperCase())}:`;
344
+ yield* indent(
345
+ indent(
346
+ emitRouteOperationDispatchMultiple(ctx, routeHandlers, operationList, route, backends),
347
+ ),
348
+ );
349
+ }
350
+ }
351
+
352
+ yield ` default:`;
353
+ yield ` return ctx.errorHandlers.onRequestNotFound(ctx);`;
354
+
355
+ yield "}";
356
+ }
357
+
358
+ /**
359
+ * Writes the dispatch code for a specific set of operations mapped to the same route.
360
+ *
361
+ * @param ctx - The emitter context.
362
+ * @param operations - The operations mapped to the route.
363
+ * @param backends - The map of backends for operations.
364
+ */
365
+ function* emitRouteOperationDispatchMultiple(
366
+ ctx: HttpContext,
367
+ routeHandlers: string,
368
+ operations: RouteOperation[],
369
+ route: string,
370
+ backends: Map<OperationContainer, [ReCase, string]>,
371
+ ): Iterable<string> {
372
+ const usedContentTypes = new Set<string>();
373
+ const contentTypeMap = new Map<RouteOperation, string>();
374
+
375
+ for (const operation of operations) {
376
+ const [httpOperation] = getHttpOperation(ctx.program, operation.operation);
377
+ const operationContentType = httpOperation.parameters.parameters.find(
378
+ (param) => param.type === "header" && param.name.toLowerCase() === "content-type",
379
+ )?.param.type;
380
+
381
+ if (!operationContentType || operationContentType.kind !== "String") {
382
+ throw new UnimplementedError(
383
+ "Only string content-types are supported for route differentiation.",
384
+ );
385
+ }
386
+
387
+ if (usedContentTypes.has(operationContentType.value)) {
388
+ reportDiagnostic(ctx.program, {
389
+ code: "undifferentiable-route",
390
+ target: httpOperation.operation,
391
+ });
392
+ }
393
+
394
+ usedContentTypes.add(operationContentType.value);
395
+
396
+ contentTypeMap.set(operation, operationContentType.value);
397
+ }
398
+
399
+ const contentTypeName = ctx.gensym("contentType");
400
+
401
+ yield `const ${contentTypeName} = parseHeaderValueParameters(request.headers["content-type"])?.value;`;
402
+
403
+ yield `switch (${contentTypeName}) {`;
404
+
405
+ for (const [operation, contentType] of contentTypeMap.entries()) {
406
+ const [backend] = backends.get(operation.container)!;
407
+ const operationName = keywordSafe(
408
+ backend.snakeCase + "_" + parseCase(operation.operation.name).snakeCase,
409
+ );
410
+
411
+ const backendMemberName = backend.camelCase;
412
+
413
+ const parameters =
414
+ operation.parameters.length > 0
415
+ ? ", " + operation.parameters.map((param) => parseCase(param.name).camelCase).join(", ")
416
+ : "";
417
+
418
+ const contentTypeValue = parseHeaderValueParameters(contentType).value;
419
+
420
+ yield ` case ${JSON.stringify(contentTypeValue)}:`;
421
+ yield ` return ${routeHandlers}.${operationName}(ctx, ${backendMemberName}${parameters});`;
422
+ }
423
+
424
+ yield ` default:`;
425
+ yield ` return ctx.errorHandlers.onInvalidRequest(ctx, ${JSON.stringify(route)}, \`No operation in route '${route}' matched content-type "\${${contentTypeName}}"\`);`;
426
+ yield "}";
427
+ }
428
+
429
+ /**
430
+ * A tree of routes in an HTTP router domain.
431
+ */
432
+ interface RouteTree {
433
+ /**
434
+ * A list of operations that can be dispatched at this node.
435
+ */
436
+ operations: Map<HttpVerb, RouteOperation[]>;
437
+ /**
438
+ * A set of parameters that are bound in this position before proceeding along the subsequent tree.
439
+ */
440
+ bind?: [Set<string>, RouteTree];
441
+ /**
442
+ * A list of static edges that can be taken from this node.
443
+ */
444
+ edges: RouteTreeEdge[];
445
+ }
446
+
447
+ /**
448
+ * An edge in the route tree. The edge contains a literal string prefix that must match before the next node is visited.
449
+ */
450
+ type RouteTreeEdge = readonly [string, RouteTree];
451
+
452
+ /**
453
+ * An operation that may be dispatched at a given tree node.
454
+ */
455
+ interface RouteOperation {
456
+ /**
457
+ * The HTTP operation corresponding to this route operation.
458
+ */
459
+ operation: Operation;
460
+ /**
461
+ * The operation's container.
462
+ */
463
+ container: OperationContainer;
464
+ /**
465
+ * The path parameters that the route template for this operation binds.
466
+ */
467
+ parameters: RouteParameter[];
468
+ /**
469
+ * The HTTP verb (GET, PUT, etc.) that this operation requires.
470
+ */
471
+ verb: HttpVerb;
472
+ }
473
+
474
+ /**
475
+ * A single route split into segments of strings and parameters.
476
+ */
477
+ interface Route extends RouteOperation {
478
+ segments: RouteSegment[];
479
+ }
480
+
481
+ /**
482
+ * A segment of a single route.
483
+ */
484
+ type RouteSegment = string | RouteParameter;
485
+
486
+ /**
487
+ * A parameter in the route segment with its expected type.
488
+ */
489
+ interface RouteParameter {
490
+ name: string;
491
+ type: Type;
492
+ }
493
+
494
+ /**
495
+ * Create a route tree for a given service.
496
+ */
497
+ function createRouteTree(ctx: HttpContext, service: HttpService): RouteTree {
498
+ // First get the Route for each operation in the service.
499
+ const routes = service.operations.map(function (operation) {
500
+ const segments = getRouteSegments(ctx, operation);
501
+ return {
502
+ operation: operation.operation,
503
+ container: operation.container,
504
+ verb: operation.verb,
505
+ parameters: segments.filter((segment) => typeof segment !== "string"),
506
+ segments,
507
+ } as Route;
508
+ });
509
+
510
+ // Build the tree by iteratively removing common prefixes from the text segments.
511
+
512
+ const tree = intoRouteTree(routes);
513
+
514
+ return tree;
515
+ }
516
+
517
+ /**
518
+ * Build a route tree from a list of routes.
519
+ *
520
+ * This iteratively removes common segments from the routes and then for all routes matching a given common prefix,
521
+ * builds a nested tree from their subsequent segments.
522
+ *
523
+ * @param routes - the routes to build the tree from
524
+ */
525
+ function intoRouteTree(routes: Route[]): RouteTree {
526
+ const [operations, rest] = bifilter(routes, (route) => route.segments.length === 0);
527
+ const [literal, parameterized] = bifilter(
528
+ rest,
529
+ (route) => typeof route.segments[0]! === "string",
530
+ );
531
+
532
+ const edgeMap = new Map<string, Route[]>();
533
+
534
+ // Group the routes by common prefix
535
+
536
+ outer: for (const literalRoute of literal) {
537
+ const segment = literalRoute.segments[0] as string;
538
+
539
+ for (const edge of [...edgeMap.keys()]) {
540
+ const prefix = commonPrefix(segment, edge);
541
+
542
+ if (prefix.length > 0) {
543
+ const existing = edgeMap.get(edge)!;
544
+ edgeMap.delete(edge);
545
+ edgeMap.set(prefix, [...existing, literalRoute]);
546
+ continue outer;
547
+ }
548
+ }
549
+
550
+ edgeMap.set(segment, [literalRoute]);
551
+ }
552
+
553
+ const edges = [...edgeMap.entries()].map(
554
+ ([edge, routes]) =>
555
+ [
556
+ edge,
557
+ intoRouteTree(
558
+ routes.map(function removePrefix(route) {
559
+ const [prefix, ...rest] = route.segments as [string, ...RouteSegment[]];
560
+
561
+ if (prefix === edge) {
562
+ return { ...route, segments: rest };
563
+ } else {
564
+ return {
565
+ ...route,
566
+ segments: [prefix.substring(edge.length), ...rest],
567
+ };
568
+ }
569
+ }),
570
+ ),
571
+ ] as const,
572
+ );
573
+
574
+ let bind: [Set<string>, RouteTree] | undefined;
575
+
576
+ if (parameterized.length > 0) {
577
+ const parameters = new Set<string>();
578
+ const nextRoutes: Route[] = [];
579
+ for (const parameterizedRoute of parameterized) {
580
+ const [{ name }, ...rest] = parameterizedRoute.segments as [
581
+ RouteParameter,
582
+ ...RouteSegment[],
583
+ ];
584
+
585
+ parameters.add(name);
586
+ nextRoutes.push({ ...parameterizedRoute, segments: rest });
587
+ }
588
+
589
+ bind = [parameters, intoRouteTree(nextRoutes)];
590
+ }
591
+
592
+ const operationMap = new Map<HttpVerb, RouteOperation[]>();
593
+
594
+ for (const operation of operations) {
595
+ let operations = operationMap.get(operation.verb);
596
+ if (!operations) {
597
+ operations = [];
598
+ operationMap.set(operation.verb, operations);
599
+ }
600
+
601
+ operations.push(operation);
602
+ }
603
+
604
+ return {
605
+ operations: operationMap,
606
+ bind,
607
+ edges,
608
+ };
609
+
610
+ function commonPrefix(a: string, b: string): string {
611
+ let i = 0;
612
+ while (i < a.length && i < b.length && a[i] === b[i]) {
613
+ i++;
614
+ }
615
+ return a.substring(0, i);
616
+ }
617
+ }
618
+
619
+ function getRouteSegments(ctx: HttpContext, operation: HttpOperation): RouteSegment[] {
620
+ // Parse the route template into segments of "prefixes" (i.e. literal strings)
621
+ // and parameters (names enclosed in curly braces). The "/" character does not
622
+ // actually matter for this. We just want to know what the segments of the route
623
+ // are.
624
+ //
625
+ // Examples:
626
+ // "" => []
627
+ // "/users" => ["/users"]
628
+ // "/users/{userId}" => ["/users/", {name: "userId"}]
629
+ // "/users/{userId}/posts/{postId}" => ["/users/", {name: "userId"}, "/posts/", {name: "postId"}]
630
+
631
+ const segments: RouteSegment[] = [];
632
+
633
+ const parameterTypeMap = new Map<string, Type>(
634
+ [...operation.parameters.parameters.values()].map(
635
+ (p) =>
636
+ [
637
+ p.param.name,
638
+ p.param.type.kind === "ModelProperty" ? p.param.type.type : p.param.type,
639
+ ] as const,
640
+ ),
641
+ );
642
+
643
+ let remainingTemplate = operation.path;
644
+
645
+ while (remainingTemplate.length > 0) {
646
+ // Scan for next `{` character
647
+ const openBraceIndex = remainingTemplate.indexOf("{");
648
+
649
+ if (openBraceIndex === -1) {
650
+ // No more parameters, just add the remaining string as a segment
651
+ segments.push(remainingTemplate);
652
+ break;
653
+ }
654
+
655
+ // Add the prefix before the parameter, if there is one
656
+ if (openBraceIndex > 0) {
657
+ segments.push(remainingTemplate.substring(0, openBraceIndex));
658
+ }
659
+
660
+ // Scan for next `}` character
661
+ const closeBraceIndex = remainingTemplate.indexOf("}", openBraceIndex);
662
+
663
+ if (closeBraceIndex === -1) {
664
+ // This is an error in the HTTP layer, so we'll just treat it as if the parameter ends here
665
+ // and captures the rest of the string as its name.
666
+ segments.push({
667
+ name: remainingTemplate.substring(openBraceIndex + 1),
668
+ type: undefined as any,
669
+ });
670
+ break;
671
+ }
672
+
673
+ // Extract the parameter name
674
+ const parameterName = remainingTemplate.substring(openBraceIndex + 1, closeBraceIndex);
675
+
676
+ segments.push({
677
+ name: parameterName,
678
+ type: parameterTypeMap.get(parameterName)!,
679
+ });
680
+
681
+ // Move to the next segment
682
+ remainingTemplate = remainingTemplate.substring(closeBraceIndex + 1);
683
+ }
684
+
685
+ return segments;
686
+ }