@typespec/http-server-js 0.58.0-alpha.15-dev.0 → 0.58.0-alpha.15-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/common/interface.d.ts.map +1 -1
- package/dist/src/common/interface.js +2 -0
- package/dist/src/common/interface.js.map +1 -1
- package/dist/src/common/scalar.d.ts +2 -1
- package/dist/src/common/scalar.d.ts.map +1 -1
- package/dist/src/common/scalar.js +14 -5
- package/dist/src/common/scalar.js.map +1 -1
- package/dist/src/common/union.d.ts.map +1 -1
- package/dist/src/common/union.js +1 -7
- package/dist/src/common/union.js.map +1 -1
- package/dist/src/ctx.d.ts +14 -0
- package/dist/src/ctx.d.ts.map +1 -1
- package/dist/src/ctx.js +14 -0
- package/dist/src/ctx.js.map +1 -1
- package/dist/src/http/operation.d.ts +18 -0
- package/dist/src/http/operation.d.ts.map +1 -0
- package/dist/src/http/operation.js +217 -0
- package/dist/src/http/operation.js.map +1 -0
- package/dist/src/http/server/index.d.ts.map +1 -1
- package/dist/src/http/server/index.js +5 -2
- package/dist/src/http/server/index.js.map +1 -1
- package/dist/src/http/server/router.d.ts.map +1 -1
- package/dist/src/http/server/router.js +7 -4
- package/dist/src/http/server/router.js.map +1 -1
- package/dist/src/lib.d.ts +2 -2
- package/dist/src/lib.d.ts.map +1 -1
- package/dist/src/lib.js +2 -2
- package/dist/src/lib.js.map +1 -1
- package/dist/src/scripts/scaffold/bin.d.mts.map +1 -1
- package/dist/src/scripts/scaffold/bin.mjs +2 -1
- package/dist/src/scripts/scaffold/bin.mjs.map +1 -1
- package/dist/src/util/encoding.d.ts +33 -0
- package/dist/src/util/encoding.d.ts.map +1 -0
- package/dist/src/util/encoding.js +55 -0
- package/dist/src/util/encoding.js.map +1 -0
- package/package.json +4 -4
- package/src/common/interface.ts +2 -0
- package/src/common/scalar.ts +17 -6
- package/src/common/union.ts +1 -8
- package/src/ctx.ts +22 -0
- package/src/http/operation.ts +340 -0
- package/src/http/server/index.ts +6 -1
- package/src/http/server/router.ts +7 -3
- package/src/lib.ts +2 -2
- package/src/scripts/scaffold/bin.mts +2 -1
- package/src/util/encoding.ts +87 -0
- package/temp/tsconfig.tsbuildinfo +1 -1
- package/test/e2e/http/type/enum/extensible/main.test.e2e.ts +45 -0
- package/test/e2e/http/type/model/inheritance/not-discriminated/main.test.e2e.ts +36 -0
- package/test/e2e/http/type/model/inheritance/recursive/main.test.e2e.ts +62 -0
package/src/common/scalar.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
1142
|
+
const scalars = getScalarStore(ctx);
|
|
1132
1143
|
|
|
1133
1144
|
let _scalar: Scalar | undefined = scalar;
|
|
1134
1145
|
|
package/src/common/union.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/http/server/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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:
|
|
493
|
+
operation: canonicalOperation,
|
|
490
494
|
container: operation.container,
|
|
491
|
-
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
|
@@ -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";
|
|
@@ -700,7 +701,7 @@ function* emitControllerOperationHandlers(
|
|
|
700
701
|
let importNotImplementedError = false;
|
|
701
702
|
for (const httpOperation of httpOperations) {
|
|
702
703
|
// TODO: unify construction of signature with emitOperation in common/interface.ts
|
|
703
|
-
const op = httpOperation.operation;
|
|
704
|
+
const op = canonicalizeHttpOperation(ctx, httpOperation.operation);
|
|
704
705
|
|
|
705
706
|
const opNameCase = parseCase(op.name);
|
|
706
707
|
|
|
@@ -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
|
+
}
|