@typespec/http-server-js 0.58.0-alpha.15-dev.0 → 0.58.0-alpha.15-dev.2

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 (50) hide show
  1. package/dist/src/common/interface.d.ts.map +1 -1
  2. package/dist/src/common/interface.js +2 -0
  3. package/dist/src/common/interface.js.map +1 -1
  4. package/dist/src/common/scalar.d.ts +2 -1
  5. package/dist/src/common/scalar.d.ts.map +1 -1
  6. package/dist/src/common/scalar.js +14 -5
  7. package/dist/src/common/scalar.js.map +1 -1
  8. package/dist/src/common/union.d.ts.map +1 -1
  9. package/dist/src/common/union.js +1 -7
  10. package/dist/src/common/union.js.map +1 -1
  11. package/dist/src/ctx.d.ts +14 -0
  12. package/dist/src/ctx.d.ts.map +1 -1
  13. package/dist/src/ctx.js +14 -0
  14. package/dist/src/ctx.js.map +1 -1
  15. package/dist/src/http/operation.d.ts +18 -0
  16. package/dist/src/http/operation.d.ts.map +1 -0
  17. package/dist/src/http/operation.js +217 -0
  18. package/dist/src/http/operation.js.map +1 -0
  19. package/dist/src/http/server/index.d.ts.map +1 -1
  20. package/dist/src/http/server/index.js +5 -2
  21. package/dist/src/http/server/index.js.map +1 -1
  22. package/dist/src/http/server/router.d.ts.map +1 -1
  23. package/dist/src/http/server/router.js +7 -4
  24. package/dist/src/http/server/router.js.map +1 -1
  25. package/dist/src/lib.d.ts +2 -2
  26. package/dist/src/lib.d.ts.map +1 -1
  27. package/dist/src/lib.js +2 -2
  28. package/dist/src/lib.js.map +1 -1
  29. package/dist/src/scripts/scaffold/bin.d.mts.map +1 -1
  30. package/dist/src/scripts/scaffold/bin.mjs +6 -3
  31. package/dist/src/scripts/scaffold/bin.mjs.map +1 -1
  32. package/dist/src/util/encoding.d.ts +33 -0
  33. package/dist/src/util/encoding.d.ts.map +1 -0
  34. package/dist/src/util/encoding.js +55 -0
  35. package/dist/src/util/encoding.js.map +1 -0
  36. package/package.json +4 -4
  37. package/src/common/interface.ts +2 -0
  38. package/src/common/scalar.ts +17 -6
  39. package/src/common/union.ts +1 -8
  40. package/src/ctx.ts +22 -0
  41. package/src/http/operation.ts +340 -0
  42. package/src/http/server/index.ts +6 -1
  43. package/src/http/server/router.ts +7 -3
  44. package/src/lib.ts +2 -2
  45. package/src/scripts/scaffold/bin.mts +5 -3
  46. package/src/util/encoding.ts +87 -0
  47. package/temp/tsconfig.tsbuildinfo +1 -1
  48. package/test/e2e/http/type/enum/extensible/main.test.e2e.ts +45 -0
  49. package/test/e2e/http/type/model/inheritance/not-discriminated/main.test.e2e.ts +36 -0
  50. package/test/e2e/http/type/model/inheritance/recursive/main.test.e2e.ts +62 -0
@@ -1,7 +1,7 @@
1
1
  // Copyright (c) Microsoft Corporation
2
2
  // Licensed under the MIT license.
3
3
 
4
- import { DiagnosticTarget, NoTarget, Program, Scalar } from "@typespec/compiler";
4
+ import { DiagnosticTarget, EncodeData, NoTarget, Program, Scalar } from "@typespec/compiler";
5
5
  import { JsContext, Module } from "../ctx.js";
6
6
  import { reportDiagnostic } from "../lib.js";
7
7
  import { parseCase } from "../util/case.js";
@@ -715,11 +715,11 @@ const __JS_SCALARS_MAP = new WeakMap<Program, ScalarStore>();
715
715
  /**
716
716
  * Gets the scalar store for a given program.
717
717
  */
718
- function getScalarStore(ctx: JsContext, module: Module): ScalarStore {
718
+ function getScalarStore(ctx: JsContext): ScalarStore {
719
719
  let scalars = __JS_SCALARS_MAP.get(ctx.program);
720
720
 
721
721
  if (scalars === undefined) {
722
- scalars = createScalarStore(ctx, module);
722
+ scalars = createScalarStore(ctx);
723
723
  __JS_SCALARS_MAP.set(ctx.program, scalars);
724
724
  }
725
725
 
@@ -729,7 +729,7 @@ function getScalarStore(ctx: JsContext, module: Module): ScalarStore {
729
729
  /**
730
730
  * Initializes a scalar store for a given program.
731
731
  */
732
- function createScalarStore(ctx: JsContext, module: Module): ScalarStore {
732
+ function createScalarStore(ctx: JsContext): ScalarStore {
733
733
  const m = new Map<Scalar, Contextualized<JsScalar>>();
734
734
 
735
735
  for (const [scalarName, scalarInfo] of SCALARS) {
@@ -773,7 +773,17 @@ function createJsScalar(
773
773
 
774
774
  scalar,
775
775
 
776
- getEncoding(encoding: string, target: Scalar): Encoder | undefined {
776
+ getEncoding(encodeDataOrString: EncodeData | string, target?: Scalar): Encoder | undefined {
777
+ let encoding: string = "default";
778
+
779
+ if (typeof encodeDataOrString === "string") {
780
+ encoding = encodeDataOrString;
781
+ target = target!;
782
+ } else {
783
+ encoding = encodeDataOrString.encoding ?? "default";
784
+ target = encodeDataOrString.type;
785
+ }
786
+
777
787
  const encodingTable = scalarInfo.encodings?.[getFullyQualifiedTypeName(target)];
778
788
  let encodingSpec = encodingTable?.[encoding] ?? encodingTable?.[encoding.toLowerCase()];
779
789
 
@@ -1034,6 +1044,7 @@ export interface JsScalar {
1034
1044
  * if the encoding is not supported.
1035
1045
  */
1036
1046
  getEncoding(encoding: string, target: Scalar): Encoder | undefined;
1047
+ getEncoding(encoding: EncodeData): Encoder | undefined;
1037
1048
 
1038
1049
  /**
1039
1050
  * Get the default encoder for a given media type.
@@ -1128,7 +1139,7 @@ export function getJsScalar(
1128
1139
  scalar: Scalar,
1129
1140
  diagnosticTarget: DiagnosticTarget | typeof NoTarget,
1130
1141
  ): JsScalar {
1131
- const scalars = getScalarStore(ctx, module);
1142
+ const scalars = getScalarStore(ctx);
1132
1143
 
1133
1144
  let _scalar: Scalar | undefined = scalar;
1134
1145
 
@@ -25,13 +25,6 @@ export function emitUnionType(ctx: JsContext, variants: UnionVariant[], module:
25
25
  const name = emitTypeReference(ctx, v.type, v, module);
26
26
 
27
27
  variantTypes.push(name);
28
-
29
- // if (isImportableType(ctx, v.type)) {
30
- // module.imports.push({
31
- // binder: [name],
32
- // from: createOrGetModuleForNamespace(ctx, v.type.namespace!),
33
- // });
34
- // }
35
28
  }
36
29
 
37
30
  return variantTypes.join(" | ");
@@ -72,5 +65,5 @@ export function* emitUnion(
72
65
  }),
73
66
  );
74
67
 
75
- yield `export type ${name} = ${variantTypes.join(" | ")};`;
68
+ yield `export type ${name} = ${variantTypes.length === 0 ? "never" : variantTypes.join(" | ")};`;
76
69
  }
package/src/ctx.ts CHANGED
@@ -27,6 +27,8 @@ import { parseCase } from "./util/case.js";
27
27
  import { UnimplementedError } from "./util/error.js";
28
28
  import { createOnceQueue, OnceQueue } from "./util/once-queue.js";
29
29
 
30
+ import { unsafe_Mutator } from "@typespec/compiler/experimental";
31
+ import { MetadataInfo } from "@typespec/http";
30
32
  import { createModule as initializeHelperModule } from "../generated-defs/helpers/index.js";
31
33
 
32
34
  export type DeclarationType = Model | Enum | Union | Interface | Scalar;
@@ -135,6 +137,10 @@ export interface JsContext {
135
137
  serializations: OnceQueue<SerializableType>;
136
138
 
137
139
  gensym: (name: string) => string;
140
+
141
+ metadataInfo?: MetadataInfo;
142
+
143
+ canonicalizationCache: { [vfKey: string]: unsafe_Mutator | undefined };
138
144
  }
139
145
 
140
146
  export async function createInitialContext(
@@ -211,6 +217,8 @@ export async function createInitialContext(
211
217
  gensym: (name) => {
212
218
  return gensym(jsCtx, name);
213
219
  },
220
+
221
+ canonicalizationCache: {},
214
222
  };
215
223
 
216
224
  return jsCtx;
@@ -359,6 +367,20 @@ export interface Import {
359
367
  from: Module | string;
360
368
  }
361
369
 
370
+ /**
371
+ * A module that does not exist and is not emitted. Use this for functions that require a module but you only
372
+ * want to analyze the type and not emit any relative paths.
373
+ *
374
+ * For example, this is used internally to canonicalize operation types, because it calls some functions that
375
+ * require a module, but canonicalizing the operation does not itself emit any code.
376
+ */
377
+ export const NoModule: Module = {
378
+ name: "",
379
+ cursor: createPathCursor(),
380
+ imports: [],
381
+ declarations: [],
382
+ };
383
+
362
384
  /**
363
385
  * An output module within the module tree.
364
386
  */
@@ -0,0 +1,340 @@
1
+ import {
2
+ getParameterVisibilityFilter,
3
+ getReturnTypeVisibilityFilter,
4
+ isVisible,
5
+ Model,
6
+ ModelProperty,
7
+ Operation,
8
+ Program,
9
+ Tuple,
10
+ Type,
11
+ Union,
12
+ UnionVariant,
13
+ VisibilityFilter,
14
+ } from "@typespec/compiler";
15
+ import {
16
+ unsafe_mutateSubgraph as mutateSubgraph,
17
+ unsafe_Mutator as Mutator,
18
+ unsafe_MutatorFlow as MutatorFlow,
19
+ } from "@typespec/compiler/experimental";
20
+ import { $ } from "@typespec/compiler/typekit";
21
+ import { useStateMap } from "@typespec/compiler/utils";
22
+ import {
23
+ createMetadataInfo,
24
+ getHttpOperation,
25
+ getVisibilitySuffix,
26
+ HttpVisibilityProvider,
27
+ isApplicableMetadataOrBody,
28
+ resolveRequestVisibility,
29
+ Visibility,
30
+ } from "@typespec/http";
31
+ import { JsContext, NoModule } from "../ctx.js";
32
+ import { createStateSymbol } from "../lib.js";
33
+ import { resolveEncodingChain } from "../util/encoding.js";
34
+
35
+ const CANONICAL_VISIBILITY = Visibility.Read;
36
+
37
+ const [getCachedCanonicalOperation, setCachedCanonicalOperation] = useStateMap<
38
+ Operation,
39
+ Operation
40
+ >(createStateSymbol("CanonicalOperationCache"));
41
+
42
+ /**
43
+ * Gets the 'canonicalized' version of an operation.
44
+ *
45
+ * This is the version of the operation that is accurate to `@typespec/http` interpretation of the
46
+ * operation.
47
+ *
48
+ * - Implicit visibility is applied to the parameters and return type according to the HTTP verb.
49
+ * - Properties and scalars that have unrecognized encoding chains are replaced by their lowest recognized
50
+ * logical type.
51
+ *
52
+ * @param ctx
53
+ * @param operation
54
+ * @returns
55
+ */
56
+ export function canonicalizeHttpOperation(ctx: JsContext, operation: Operation): Operation {
57
+ let canonical = getCachedCanonicalOperation(ctx.program, operation);
58
+
59
+ if (canonical) return canonical;
60
+
61
+ const metadataInfo = (ctx.metadataInfo ??= createMetadataInfo(ctx.program, {
62
+ canonicalVisibility: CANONICAL_VISIBILITY,
63
+ }));
64
+
65
+ canonical = _canonicalizeHttpOperation();
66
+
67
+ setCachedCanonicalOperation(ctx.program, operation, canonical);
68
+
69
+ return canonical;
70
+
71
+ function _canonicalizeHttpOperation(): Operation {
72
+ const [httpOperation] = getHttpOperation(ctx.program, operation);
73
+
74
+ const httpVisibilityProvider = HttpVisibilityProvider();
75
+
76
+ const parameterVisibilityFilter = getParameterVisibilityFilter(
77
+ ctx.program,
78
+ operation,
79
+ httpVisibilityProvider,
80
+ );
81
+
82
+ const httpParameterVisibility = resolveRequestVisibility(
83
+ ctx.program,
84
+ operation,
85
+ httpOperation.verb,
86
+ );
87
+
88
+ const parameterMutator = bindMutator(httpParameterVisibility, parameterVisibilityFilter);
89
+
90
+ const mutatedParameters = cachedMutateSubgraph(
91
+ ctx.program,
92
+ parameterMutator,
93
+ operation.parameters,
94
+ ).type as Model;
95
+
96
+ const returnTypeVisibilityFilter = getReturnTypeVisibilityFilter(
97
+ ctx.program,
98
+ operation,
99
+ httpVisibilityProvider,
100
+ );
101
+
102
+ // For return types, the visibility is always Visibility.Read, but we could have a
103
+ // custom returnTypeVisibilityFilter that is more restrictive. We will always use
104
+ // Visibility.Read as the HTTP visibility for suffixing and metadataInfo tests, but
105
+ // still check visibility based on the configured filter.
106
+ const returnTypeMutator = bindMutator(Visibility.Read, returnTypeVisibilityFilter);
107
+
108
+ const mutatedReturnType = isCanonicalizationSubject(operation.returnType)
109
+ ? cachedMutateSubgraph(ctx.program, returnTypeMutator, operation.returnType).type
110
+ : operation.returnType;
111
+
112
+ const clonedOperation = $(ctx.program).type.clone(operation);
113
+
114
+ clonedOperation.parameters = mutatedParameters;
115
+ clonedOperation.returnType = mutatedReturnType;
116
+
117
+ $(ctx.program).type.finishType(clonedOperation);
118
+
119
+ return clonedOperation;
120
+ }
121
+
122
+ function bindMutator(httpVisibility: Visibility, visibilityFilter: VisibilityFilter): Mutator {
123
+ const cacheKey =
124
+ String(httpVisibility) + "--" + VisibilityFilter.toCacheKey(ctx.program, visibilityFilter);
125
+
126
+ const cached = ctx.canonicalizationCache[cacheKey];
127
+
128
+ if (cached) return cached;
129
+
130
+ const visibilitySuffix = getVisibilitySuffix(httpVisibility, CANONICAL_VISIBILITY);
131
+
132
+ const primaryMutator: Mutator = {
133
+ name: "CanonicalizeHttpOperation",
134
+ Model: {
135
+ filter: () => MutatorFlow.DoNotRecur,
136
+ replace: (model, clone, program, realm) => {
137
+ let modified = false;
138
+
139
+ const indexer = model.indexer;
140
+
141
+ if (indexer) {
142
+ if ($(realm).array.is(model)) {
143
+ // Array items have a bit of a special visibility concern
144
+
145
+ const { type: mutated } = isCanonicalizationSubject(indexer.value)
146
+ ? cachedMutateSubgraph(program, arrayItemMutator, indexer.value)
147
+ : { type: indexer.value };
148
+
149
+ clone.indexer = { key: indexer.key, value: mutated };
150
+ } else {
151
+ const { type: mutated } = isCanonicalizationSubject(indexer.value)
152
+ ? cachedMutateSubgraph(program, primaryMutator, indexer.value)
153
+ : { type: indexer.value };
154
+
155
+ clone.indexer = { key: indexer.key, value: mutated };
156
+ }
157
+
158
+ modified ||= indexer.value !== clone.indexer.value;
159
+ }
160
+
161
+ for (const [name, property] of model.properties) {
162
+ if (isVisible(program, property, visibilityFilter)) {
163
+ const mutated = cachedMutateSubgraph(program, mpMutator, property)
164
+ .type as ModelProperty;
165
+
166
+ clone.properties.set(name, mutated);
167
+
168
+ modified ||= property.type !== mutated.type;
169
+ } else {
170
+ clone.properties.delete(name);
171
+ realm.remove(property);
172
+ modified = true;
173
+ }
174
+ }
175
+
176
+ if (clone.name) clone.name = clone.name + visibilitySuffix;
177
+
178
+ return modified ? clone : model;
179
+ },
180
+ },
181
+ Union: {
182
+ filter: () => MutatorFlow.DoNotRecur,
183
+ replace: (union, clone, program, realm) => {
184
+ let modified = false;
185
+ for (const [name, variant] of union.variants) {
186
+ const { type: mutated } = isCanonicalizationSubject(variant.type)
187
+ ? cachedMutateSubgraph(program, primaryMutator, variant.type)
188
+ : variant;
189
+
190
+ clone.variants.set(
191
+ name,
192
+ $(realm).unionVariant.create({
193
+ name: variant.name,
194
+ type: mutated,
195
+ union: clone,
196
+ }),
197
+ );
198
+
199
+ modified ||= variant.type !== mutated;
200
+ }
201
+
202
+ if (clone.name) clone.name = clone.name + visibilitySuffix;
203
+
204
+ return modified ? clone : union;
205
+ },
206
+ },
207
+ ModelProperty: {
208
+ filter: () => MutatorFlow.DoNotRecur,
209
+ replace: (modelProperty, clone, program, realm) => {
210
+ // Passthrough -- but with encoding decay.
211
+ const encoders = resolveEncodingChain(ctx, NoModule, modelProperty, modelProperty.type);
212
+
213
+ if (encoders.canonicalType !== modelProperty.type) {
214
+ return encoders.canonicalType;
215
+ } else {
216
+ return modelProperty;
217
+ }
218
+ },
219
+ },
220
+ UnionVariant: {
221
+ filter: () => MutatorFlow.DoNotRecur,
222
+ replace: (variant, clone, program, realm) => {
223
+ const { type: mutated } = isCanonicalizationSubject(variant.type)
224
+ ? cachedMutateSubgraph(program, primaryMutator, variant.type)
225
+ : variant;
226
+
227
+ clone.type = mutated;
228
+
229
+ return mutated !== variant.type ? clone : variant;
230
+ },
231
+ },
232
+ Tuple: {
233
+ filter: () => MutatorFlow.DoNotRecur,
234
+ replace: (tuple, clone, program, realm) => {
235
+ let modified = false;
236
+ clone.values = [...tuple.values];
237
+ for (const [idx, value] of tuple.values.map((v, idx) => [idx, v] as const)) {
238
+ const { type: mutated } = isCanonicalizationSubject(value)
239
+ ? cachedMutateSubgraph(program, primaryMutator, value)
240
+ : { type: value };
241
+
242
+ clone.values[idx] = mutated;
243
+
244
+ modified ||= value !== mutated;
245
+ }
246
+
247
+ return modified ? clone : tuple;
248
+ },
249
+ },
250
+ Scalar: {
251
+ filter: () => MutatorFlow.DoNotRecur,
252
+ replace: (scalar, clone, program, realm) => {
253
+ // Passthrough -- but with encoding decay.
254
+ const encoders = resolveEncodingChain(ctx, NoModule, scalar, scalar);
255
+
256
+ return encoders.canonicalType;
257
+ },
258
+ },
259
+ };
260
+
261
+ const arrayItemMutator =
262
+ httpVisibility & Visibility.Item
263
+ ? primaryMutator
264
+ : bindMutator(httpVisibility | Visibility.Item, visibilityFilter);
265
+
266
+ const mpMutator: Mutator = {
267
+ name: primaryMutator.name + "ModelProperty",
268
+ ModelProperty: {
269
+ filter: () => MutatorFlow.DoNotRecur,
270
+ replace: (modelProperty, clone, program, realm) => {
271
+ let modified = false;
272
+ const { type: originalCanonicalType } = isCanonicalizationSubject(modelProperty.type)
273
+ ? cachedMutateSubgraph(ctx.program, primaryMutator, modelProperty.type)
274
+ : { type: modelProperty.type };
275
+
276
+ const encodingChain = resolveEncodingChain(
277
+ ctx,
278
+ NoModule,
279
+ modelProperty,
280
+ originalCanonicalType,
281
+ );
282
+
283
+ clone.type = encodingChain.canonicalType;
284
+
285
+ modified ||= modelProperty.type !== clone.type;
286
+
287
+ if (
288
+ metadataInfo.isPayloadProperty(modelProperty, httpVisibility) &&
289
+ !isApplicableMetadataOrBody(ctx.program, modelProperty, httpVisibility) &&
290
+ metadataInfo.isOptional(modelProperty, httpVisibility)
291
+ ) {
292
+ clone.optional = true;
293
+ modified ||= !modelProperty.optional;
294
+ }
295
+
296
+ return modified ? clone : modelProperty;
297
+ },
298
+ },
299
+ };
300
+
301
+ return (ctx.canonicalizationCache[cacheKey] = primaryMutator);
302
+ }
303
+ }
304
+
305
+ type CanonicalizationSubject = Model | Union | ModelProperty | UnionVariant | Tuple;
306
+
307
+ function isCanonicalizationSubject(t: Type): t is CanonicalizationSubject {
308
+ return (
309
+ t.kind === "Model" ||
310
+ t.kind === "Union" ||
311
+ t.kind === "ModelProperty" ||
312
+ t.kind === "UnionVariant" ||
313
+ t.kind === "Tuple" ||
314
+ t.kind === "Scalar"
315
+ );
316
+ }
317
+
318
+ const MUTATOR_RESULT = Symbol.for("TypeSpec.HttpServerJs.MutatorResult");
319
+
320
+ interface MutatorResultCache {
321
+ [MUTATOR_RESULT]: WeakMap<CanonicalizationSubject, ReturnType<typeof mutateSubgraph>>;
322
+ }
323
+
324
+ function cachedMutateSubgraph(
325
+ program: Program,
326
+ mutator: Mutator,
327
+ type: CanonicalizationSubject,
328
+ ): ReturnType<typeof mutateSubgraph> {
329
+ const cache = ((mutator as unknown as MutatorResultCache)[MUTATOR_RESULT] ??= new WeakMap());
330
+
331
+ let cached = cache.get(type);
332
+
333
+ if (cached) return cached;
334
+
335
+ cached = mutateSubgraph(program, [mutator], type);
336
+
337
+ cache.set(type, cached);
338
+
339
+ return cached;
340
+ }
@@ -14,6 +14,7 @@ import {
14
14
  HttpOperation,
15
15
  HttpOperationParameter,
16
16
  getHeaderFieldName,
17
+ getHttpOperation,
17
18
  isBody,
18
19
  isHeader,
19
20
  isStatusCode,
@@ -46,6 +47,7 @@ import {
46
47
  transposeExpressionFromJson,
47
48
  } from "../../common/serialization/json.js";
48
49
  import { getFullyQualifiedTypeName } from "../../util/name.js";
50
+ import { canonicalizeHttpOperation } from "../operation.js";
49
51
 
50
52
  const DEFAULT_CONTENT_TYPE = "application/json";
51
53
 
@@ -96,12 +98,15 @@ function* emitRawServerOperation(
96
98
  module: Module,
97
99
  responderNames: Pick<Names, "isHttpResponder" | "httpResponderSym">,
98
100
  ): Iterable<string> {
99
- const op = operation.operation;
101
+ let op = operation.operation;
100
102
  const operationNameCase = parseCase(op.name);
101
103
 
102
104
  const container = op.interface ?? op.namespace!;
103
105
  const containerNameCase = parseCase(container.name);
104
106
 
107
+ op = canonicalizeHttpOperation(ctx, op);
108
+ [operation] = getHttpOperation(ctx.program, op);
109
+
105
110
  module.imports.push({
106
111
  binder: [containerNameCase.pascalCase],
107
112
  from: createOrGetModuleForNamespace(ctx, container.namespace!),
@@ -8,6 +8,7 @@ import {
8
8
  HttpVerb,
9
9
  OperationContainer,
10
10
  getHeaderFieldName,
11
+ getHttpOperation,
11
12
  isHeader,
12
13
  } from "@typespec/http";
13
14
  import {
@@ -24,6 +25,7 @@ import { HttpContext } from "../index.js";
24
25
  import { module as headerHelpers } from "../../../generated-defs/helpers/header.js";
25
26
  import { module as routerHelper } from "../../../generated-defs/helpers/router.js";
26
27
  import { differentiateModelTypes, writeCodeTree } from "../../util/differentiate.js";
28
+ import { canonicalizeHttpOperation } from "../operation.js";
27
29
 
28
30
  /**
29
31
  * Emit a router for the HTTP operations defined in a given service.
@@ -484,11 +486,13 @@ interface RouteParameter {
484
486
  function createRouteTree(ctx: HttpContext, service: HttpService): RouteTree {
485
487
  // First get the Route for each operation in the service.
486
488
  const routes = service.operations.map(function (operation) {
487
- const segments = getRouteSegments(ctx, operation);
489
+ const canonicalOperation = canonicalizeHttpOperation(ctx, operation.operation);
490
+ const [httpOperation] = getHttpOperation(ctx.program, canonicalOperation);
491
+ const segments = getRouteSegments(ctx, httpOperation);
488
492
  return {
489
- operation: operation.operation,
493
+ operation: canonicalOperation,
490
494
  container: operation.container,
491
- verb: operation.verb,
495
+ verb: httpOperation.verb,
492
496
  parameters: segments.filter((segment) => typeof segment !== "string"),
493
497
  segments,
494
498
  } as Route;
package/src/lib.ts CHANGED
@@ -174,6 +174,6 @@ export const $lib = createTypeSpecLibrary({
174
174
  },
175
175
  });
176
176
 
177
- const { reportDiagnostic } = $lib;
177
+ const { reportDiagnostic, createStateSymbol } = $lib;
178
178
 
179
- export { reportDiagnostic };
179
+ export { createStateSymbol, reportDiagnostic };
@@ -28,6 +28,7 @@ import { module as httpHelperModule } from "../../../generated-defs/helpers/http
28
28
  import { module as routerModule } from "../../../generated-defs/helpers/router.js";
29
29
  import { emitOptionsType } from "../../common/interface.js";
30
30
  import { emitTypeReference, isValueLiteralType } from "../../common/reference.js";
31
+ import { canonicalizeHttpOperation } from "../../http/operation.js";
31
32
  import { JsEmitterOptions } from "../../lib.js";
32
33
  import { getAllProperties } from "../../util/extends.js";
33
34
  import { bifilter, indent } from "../../util/iter.js";
@@ -609,8 +610,9 @@ function getAllExternalDependencies(ctx: JsContext): Set<string> {
609
610
  for (const _import of module.imports) {
610
611
  if (
611
612
  typeof _import.from === "string" &&
612
- !_import.from.startsWith(".") &&
613
- !_import.from.startsWith("/")
613
+ !_import.from.startsWith(".") && // is a relative path
614
+ !_import.from.startsWith("/") && // is an absolute path
615
+ !_import.from.startsWith("node:") // is node builtin
614
616
  ) {
615
617
  externalDependencies.add(_import.from);
616
618
  } else if (typeof _import.from !== "string") {
@@ -700,7 +702,7 @@ function* emitControllerOperationHandlers(
700
702
  let importNotImplementedError = false;
701
703
  for (const httpOperation of httpOperations) {
702
704
  // TODO: unify construction of signature with emitOperation in common/interface.ts
703
- const op = httpOperation.operation;
705
+ const op = canonicalizeHttpOperation(ctx, httpOperation.operation);
704
706
 
705
707
  const opNameCase = parseCase(op.name);
706
708
 
@@ -0,0 +1,87 @@
1
+ import { getEncode, ModelProperty, Scalar, Type } from "@typespec/compiler";
2
+ import { Encoder, getJsScalar } from "../common/scalar.js";
3
+ import { JsContext, Module } from "../ctx.js";
4
+
5
+ /**
6
+ * A resolved encoding chain for a model property or scalar.
7
+ */
8
+ export interface ResolvedEncodingChain {
9
+ /**
10
+ * The canonical type of the property -- a.k.a. the logical type that the service implementor will use.
11
+ */
12
+ canonicalType: Type;
13
+ /**
14
+ * The ultimate encoding target. This will always be the same as the `.type` of the last encoder in the chain, or
15
+ * the same as the canonical type if there are no encoders.
16
+ */
17
+ targetType: Type;
18
+ /**
19
+ * The chain of encoders tht apply to the canonical type. These are applied in order front-to-back to encode the
20
+ * canonical type into the target type, and are applied back-to-front to decode the canonical type from the target type.
21
+ */
22
+ encoders: Encoder[];
23
+ }
24
+
25
+ const ENCODE_SOURCES: { [k in "ModelProperty" | "Scalar"]: (t: ModelProperty | Scalar) => Type } = {
26
+ ModelProperty: (t) => (t as ModelProperty).type,
27
+ Scalar: (t) => t,
28
+ };
29
+
30
+ /**
31
+ * Resolves the chain of `@encode` encoders that apply to a given property with a given canonical (logical) type.
32
+ *
33
+ * @param ctx - The context to use for resolving encoders.
34
+ * @param module - The module that the property is defined in.
35
+ * @param encodeSource - The original property to resolve encoders for.
36
+ * @param canonicalType - The canonical type of the property -- this might be different from the type of the property itself.
37
+ * @returns A resolved encoding chain describing the final canonical type, the ultimate target type of the chain, and the encoders that apply to the property in order.
38
+ */
39
+ export function resolveEncodingChain(
40
+ ctx: JsContext,
41
+ module: Module,
42
+ encodeSource: ModelProperty | Scalar,
43
+ canonicalType: Type,
44
+ ): ResolvedEncodingChain {
45
+ let encoders: Encoder[] = [];
46
+ let targetType: Type = canonicalType;
47
+
48
+ for (const [kind, select] of Object.entries(ENCODE_SOURCES)) {
49
+ while (encodeSource.kind === kind) {
50
+ const s = select(encodeSource);
51
+ const encoding = getEncode(ctx.program, encodeSource);
52
+
53
+ if (!encoding) break;
54
+
55
+ targetType = encodeSource = encoding.type;
56
+
57
+ if (s.kind !== "Scalar") {
58
+ // Decay because we don't know how to encode anything other than a scalar.
59
+ // Should be unreachable?
60
+ decay(encoding.type);
61
+ } else {
62
+ const sourceJsScalar = getJsScalar(ctx, module, s, encodeSource);
63
+
64
+ const encoder = sourceJsScalar.getEncoding(encoding);
65
+
66
+ if (encoder) {
67
+ encoders.push(encoder);
68
+ } else {
69
+ // Decay because we don't know what the encoding is.
70
+ // Invalidate the entire chain and set the current encoding.type as the canonical type.
71
+ decay(encoding.type);
72
+ }
73
+ }
74
+ }
75
+ }
76
+
77
+ return {
78
+ canonicalType,
79
+ targetType,
80
+ encoders,
81
+ };
82
+
83
+ function decay(t: ModelProperty | Scalar) {
84
+ encoders = [];
85
+ canonicalType = encodeSource = t;
86
+ }
87
+ }