@typespec/http-server-js 0.58.0-alpha.12-dev.0 → 0.58.0-alpha.12-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/generated-defs/helpers/datetime.d.ts +4 -0
- package/dist/generated-defs/helpers/datetime.d.ts.map +1 -0
- package/dist/generated-defs/helpers/datetime.js +256 -0
- package/dist/generated-defs/helpers/datetime.js.map +1 -0
- package/dist/generated-defs/helpers/index.d.ts.map +1 -1
- package/dist/generated-defs/helpers/index.js +1 -0
- package/dist/generated-defs/helpers/index.js.map +1 -1
- package/dist/src/common/declaration.js +1 -1
- package/dist/src/common/declaration.js.map +1 -1
- package/dist/src/common/reference.js +1 -1
- package/dist/src/common/reference.js.map +1 -1
- package/dist/src/common/scalar.d.ts +175 -22
- package/dist/src/common/scalar.d.ts.map +1 -1
- package/dist/src/common/scalar.js +420 -93
- package/dist/src/common/scalar.js.map +1 -1
- package/dist/src/common/serialization/index.d.ts +2 -2
- package/dist/src/common/serialization/index.d.ts.map +1 -1
- package/dist/src/common/serialization/index.js +9 -3
- package/dist/src/common/serialization/index.js.map +1 -1
- package/dist/src/common/serialization/json.d.ts +2 -2
- package/dist/src/common/serialization/json.d.ts.map +1 -1
- package/dist/src/common/serialization/json.js +144 -42
- package/dist/src/common/serialization/json.js.map +1 -1
- package/dist/src/helpers/datetime.d.ts +92 -0
- package/dist/src/helpers/datetime.d.ts.map +1 -0
- package/dist/src/helpers/datetime.js +151 -0
- package/dist/src/helpers/datetime.js.map +1 -0
- package/dist/src/http/server/index.d.ts.map +1 -1
- package/dist/src/http/server/index.js +17 -12
- package/dist/src/http/server/index.js.map +1 -1
- package/dist/src/http/server/multipart.js +1 -1
- package/dist/src/http/server/multipart.js.map +1 -1
- package/dist/src/lib.d.ts +10 -1
- package/dist/src/lib.d.ts.map +1 -1
- package/dist/src/lib.js +6 -0
- package/dist/src/lib.js.map +1 -1
- package/dist/src/util/case.d.ts +9 -0
- package/dist/src/util/case.d.ts.map +1 -1
- package/dist/src/util/case.js +18 -0
- package/dist/src/util/case.js.map +1 -1
- package/dist/src/util/differentiate.d.ts +4 -4
- package/dist/src/util/differentiate.d.ts.map +1 -1
- package/dist/src/util/differentiate.js +10 -10
- package/dist/src/util/differentiate.js.map +1 -1
- package/generated-defs/helpers/datetime.ts +263 -0
- package/generated-defs/helpers/index.ts +1 -0
- package/package.json +7 -7
- package/src/common/declaration.ts +1 -1
- package/src/common/reference.ts +1 -1
- package/src/common/scalar.ts +709 -103
- package/src/common/serialization/index.ts +11 -4
- package/src/common/serialization/json.ts +174 -52
- package/src/helpers/datetime.ts +235 -0
- package/src/http/server/index.ts +29 -15
- package/src/http/server/multipart.ts +1 -1
- package/src/lib.ts +6 -0
- package/src/util/case.ts +19 -0
- package/src/util/differentiate.ts +15 -8
- package/temp/tsconfig.tsbuildinfo +1 -1
- package/test/datetime.test.ts +226 -0
- package/test/scalar.test.ts +345 -0
package/src/common/scalar.ts
CHANGED
|
@@ -1,135 +1,746 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation
|
|
2
2
|
// Licensed under the MIT license.
|
|
3
3
|
|
|
4
|
-
import { DiagnosticTarget, NoTarget, Program, Scalar
|
|
5
|
-
import { JsContext } from "../ctx.js";
|
|
4
|
+
import { DiagnosticTarget, NoTarget, Program, Scalar } from "@typespec/compiler";
|
|
5
|
+
import { JsContext, Module } from "../ctx.js";
|
|
6
6
|
import { reportDiagnostic } from "../lib.js";
|
|
7
7
|
import { parseCase } from "../util/case.js";
|
|
8
|
-
import { UnimplementedError } from "../util/error.js";
|
|
9
8
|
import { getFullyQualifiedTypeName } from "../util/name.js";
|
|
10
9
|
|
|
10
|
+
import { HttpOperationParameter } from "@typespec/http";
|
|
11
|
+
import { module as dateTimeModule } from "../../generated-defs/helpers/datetime.js";
|
|
12
|
+
import { UnreachableError } from "../util/error.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A specification of a TypeSpec scalar type.
|
|
16
|
+
*/
|
|
17
|
+
export interface ScalarInfo {
|
|
18
|
+
/**
|
|
19
|
+
* The TypeScript type that represents the scalar, or a function if the scalar requires a representation
|
|
20
|
+
* that is not built-in.
|
|
21
|
+
*/
|
|
22
|
+
type: MaybeDependent<string>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A map of supported encodings for the scalar.
|
|
26
|
+
*/
|
|
27
|
+
encodings?: {
|
|
28
|
+
[target: string]: {
|
|
29
|
+
/**
|
|
30
|
+
* The default encoding for the target.
|
|
31
|
+
*/
|
|
32
|
+
default?: MaybeDependent<ScalarEncoding>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The encoding for the scalar when encoded using a particular method.
|
|
36
|
+
*/
|
|
37
|
+
[encoding: string]: MaybeDependent<ScalarEncoding> | undefined;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A map of default encodings for the scalar.
|
|
43
|
+
*/
|
|
44
|
+
defaultEncodings?: {
|
|
45
|
+
/**
|
|
46
|
+
* The default encoding pair to use for a given MIME type.
|
|
47
|
+
*/
|
|
48
|
+
byMimeType?: { [contentType: string]: [string, string] };
|
|
49
|
+
/**
|
|
50
|
+
* The default encoding pair to use in the context of HTTP metadata.
|
|
51
|
+
*/
|
|
52
|
+
http?: {
|
|
53
|
+
[K in HttpOperationParameter["type"]]?: [string, string];
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Whether or not this scalar can serve as a JSON-compatible type.
|
|
59
|
+
*
|
|
60
|
+
* If JSON serialization reaches a non-compatible scalar and no more encodings are available, it is treated as
|
|
61
|
+
* an unknown type.
|
|
62
|
+
*/
|
|
63
|
+
isJsonCompatible: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A function that resolves a value dependent on the context and module it's requested from.
|
|
68
|
+
*/
|
|
69
|
+
export interface Dependent<T> {
|
|
70
|
+
(ctx: JsContext, module: Module): T;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A value that might be dependent.
|
|
75
|
+
*/
|
|
76
|
+
export type MaybeDependent<T> = T | Dependent<T>;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A definition of a scalar encoding.
|
|
80
|
+
*/
|
|
81
|
+
export type ScalarEncoding = ScalarEncodingTemplates | ScalarEncodingVia;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A definition of a scalar encoding with templates.
|
|
85
|
+
*/
|
|
86
|
+
export interface ScalarEncodingTemplates {
|
|
87
|
+
/**
|
|
88
|
+
* The template to use to encode the scalar.
|
|
89
|
+
*
|
|
90
|
+
* The position of the string "{}" in the template will be replaced with the value to encode.
|
|
91
|
+
*/
|
|
92
|
+
encodeTemplate: MaybeDependent<string>;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The template to use to decode the scalar.
|
|
96
|
+
*
|
|
97
|
+
* The position of the string "{}" in the template will be replaced with the value to decode.
|
|
98
|
+
*/
|
|
99
|
+
decodeTemplate: MaybeDependent<string>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface ScalarEncodingVia {
|
|
103
|
+
/**
|
|
104
|
+
* If set, the name of the encoding to use as a base for this encoding.
|
|
105
|
+
*
|
|
106
|
+
* This can be used to define an encoding that is a modification of another encoding, such as a URL-encoded version
|
|
107
|
+
* of a base64-encoded value, which depends on the base64 encoding.
|
|
108
|
+
*/
|
|
109
|
+
via: string;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Optional encoding template, defaults to "{}"
|
|
113
|
+
*/
|
|
114
|
+
encodeTemplate?: MaybeDependent<string>;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Optional decoding template, defaults to "{}"
|
|
118
|
+
*/
|
|
119
|
+
decodeTemplate?: MaybeDependent<string>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Resolves the encoding of Duration values to a number of seconds.
|
|
124
|
+
*/
|
|
125
|
+
const DURATION_NUMBER_ENCODING: Dependent<ScalarEncoding> = (_, module) => {
|
|
126
|
+
module.imports.push({ from: dateTimeModule, binder: ["Duration"] });
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
encodeTemplate: "Duration.totalSeconds({})",
|
|
130
|
+
decodeTemplate: "Duration.fromSeconds({})",
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Resolves the encoding of Duration values to a BigInt number of seconds.
|
|
136
|
+
*/
|
|
137
|
+
const DURATION_BIGINT_ENCODING: Dependent<ScalarEncoding> = (_, module) => {
|
|
138
|
+
module.imports.push({ from: dateTimeModule, binder: ["Duration"] });
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
encodeTemplate: "Duration.totalSecondsBigInt({})",
|
|
142
|
+
decodeTemplate: "Duration.fromSeconds(globalThis.Number({}))",
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const TYPESPEC_DURATION: ScalarInfo = {
|
|
147
|
+
type: function importDuration(_, module) {
|
|
148
|
+
module.imports.push({ from: dateTimeModule, binder: ["Duration"] });
|
|
149
|
+
|
|
150
|
+
return "Duration";
|
|
151
|
+
},
|
|
152
|
+
encodings: {
|
|
153
|
+
"TypeSpec.string": {
|
|
154
|
+
default: {
|
|
155
|
+
via: "iso8601",
|
|
156
|
+
},
|
|
157
|
+
iso8601: function importDurationForEncode(_, module) {
|
|
158
|
+
module.imports.push({ from: dateTimeModule, binder: ["Duration"] });
|
|
159
|
+
return {
|
|
160
|
+
encodeTemplate: "Duration.toISO8601({})",
|
|
161
|
+
decodeTemplate: "Duration.parseISO8601({})",
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
...Object.fromEntries(
|
|
166
|
+
["int32", "uint32"].map((n) => [
|
|
167
|
+
`TypeSpec.${n}`,
|
|
168
|
+
{
|
|
169
|
+
default: { via: "seconds" },
|
|
170
|
+
seconds: DURATION_NUMBER_ENCODING,
|
|
171
|
+
},
|
|
172
|
+
]),
|
|
173
|
+
),
|
|
174
|
+
...Object.fromEntries(
|
|
175
|
+
["int64", "uint64"].map((n) => [
|
|
176
|
+
`TypeSpec.${n}`,
|
|
177
|
+
{
|
|
178
|
+
default: { via: "seconds" },
|
|
179
|
+
seconds: DURATION_BIGINT_ENCODING,
|
|
180
|
+
},
|
|
181
|
+
]),
|
|
182
|
+
),
|
|
183
|
+
},
|
|
184
|
+
defaultEncodings: {
|
|
185
|
+
byMimeType: {
|
|
186
|
+
"application/json": ["TypeSpec.string", "iso8601"],
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
isJsonCompatible: false,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const NUMBER: ScalarInfo = {
|
|
193
|
+
type: "number",
|
|
194
|
+
encodings: {
|
|
195
|
+
"TypeSpec.string": {
|
|
196
|
+
default: {
|
|
197
|
+
encodeTemplate: "globalThis.String({})",
|
|
198
|
+
decodeTemplate: "globalThis.Number({})",
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
isJsonCompatible: true,
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Declarative scalar table.
|
|
207
|
+
*
|
|
208
|
+
* This table defines how TypeSpec scalars are represented in JS/TS.
|
|
209
|
+
*
|
|
210
|
+
* The entries are the fully-qualified names of scalars, and the values are objects that describe how the scalar
|
|
211
|
+
* is represented.
|
|
212
|
+
*
|
|
213
|
+
* Each representation has a `type`, indicating the TypeScript type that represents the scalar at runtime.
|
|
214
|
+
*
|
|
215
|
+
* The `encodings` object describes how the scalar can be encoded/decoded to/from other types. Encodings
|
|
216
|
+
* are named, and each encoding has an `encodeTemplate` and `decodeTemplate` that describe how to encode and decode
|
|
217
|
+
* the scalar to/from the target type using the encoding. Encodings can also optionally have a `via` field that
|
|
218
|
+
* indicates that the encoding is a modification of the data yielded by another encoding.
|
|
219
|
+
*
|
|
220
|
+
* The `defaultEncodings` object describes the default encodings to use for the scalar in various contexts. The
|
|
221
|
+
* `byMimeType` object maps MIME types to encoding pairs, and the `http` object maps HTTP metadata contexts to
|
|
222
|
+
* encoding pairs.
|
|
223
|
+
*/
|
|
224
|
+
const SCALARS = new Map<string, ScalarInfo>([
|
|
225
|
+
[
|
|
226
|
+
"TypeSpec.bytes",
|
|
227
|
+
{
|
|
228
|
+
type: "Uint8Array",
|
|
229
|
+
encodings: {
|
|
230
|
+
"TypeSpec.string": {
|
|
231
|
+
base64: {
|
|
232
|
+
encodeTemplate:
|
|
233
|
+
"({} instanceof globalThis.Buffer ? {} : globalThis.Buffer.from({})).toString('base64')",
|
|
234
|
+
decodeTemplate: "globalThis.Buffer.from({}, 'base64')",
|
|
235
|
+
},
|
|
236
|
+
base64url: {
|
|
237
|
+
via: "base64",
|
|
238
|
+
encodeTemplate: "globalThis.encodeURIComponent({})",
|
|
239
|
+
decodeTemplate: "globalThis.decodeURIComponent({})",
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
defaultEncodings: {
|
|
244
|
+
byMimeType: { "application/json": ["TypeSpec.string", "base64"] },
|
|
245
|
+
},
|
|
246
|
+
isJsonCompatible: false,
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
[
|
|
250
|
+
"TypeSpec.boolean",
|
|
251
|
+
{
|
|
252
|
+
type: "boolean",
|
|
253
|
+
encodings: {
|
|
254
|
+
"TypeSpec.string": {
|
|
255
|
+
default: {
|
|
256
|
+
encodeTemplate: "globalThis.String({})",
|
|
257
|
+
decodeTemplate: '({} === "false" ? false : globalThis.Boolean({}))',
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
isJsonCompatible: true,
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
[
|
|
265
|
+
"TypeSpec.string",
|
|
266
|
+
{
|
|
267
|
+
type: "string",
|
|
268
|
+
// This little no-op encoding makes it so that we can attempt to encode string to itself infallibly and it will
|
|
269
|
+
// do nothing. We therefore don't need to redundantly describe HTTP encodings for query, header, etc. because
|
|
270
|
+
// they rely on the ["TypeSpec.string", "default"] encoding in the absence of a more specific encoding.
|
|
271
|
+
encodings: {
|
|
272
|
+
"TypeSpec.string": {
|
|
273
|
+
default: { encodeTemplate: "{}", decodeTemplate: "{}" },
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
isJsonCompatible: true,
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
|
|
280
|
+
["TypeSpec.float32", NUMBER],
|
|
281
|
+
["TypeSpec.float64", NUMBER],
|
|
282
|
+
["TypeSpec.uint32", NUMBER],
|
|
283
|
+
["TypeSpec.uint16", NUMBER],
|
|
284
|
+
["TypeSpec.uint8", NUMBER],
|
|
285
|
+
["TypeSpec.int32", NUMBER],
|
|
286
|
+
["TypeSpec.int16", NUMBER],
|
|
287
|
+
["TypeSpec.int8", NUMBER],
|
|
288
|
+
["TypeSpec.safeint", NUMBER],
|
|
289
|
+
|
|
290
|
+
[
|
|
291
|
+
"TypeSpec.integer",
|
|
292
|
+
{
|
|
293
|
+
type: "bigint",
|
|
294
|
+
encodings: {
|
|
295
|
+
"TypeSpec.string": {
|
|
296
|
+
default: {
|
|
297
|
+
encodeTemplate: "globalThis.String({})",
|
|
298
|
+
decodeTemplate: "globalThis.BigInt({})",
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
isJsonCompatible: false,
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
["TypeSpec.plainDate", { type: "Date", isJsonCompatible: false }],
|
|
306
|
+
["TypeSpec.plainTime", { type: "Date", isJsonCompatible: false }],
|
|
307
|
+
["TypeSpec.utcDateTime", { type: "Date", isJsonCompatible: false }],
|
|
308
|
+
["TypeSpec.offsetDateTime", { type: "Date", isJsonCompatible: false }],
|
|
309
|
+
["TypeSpec.unixTimestamp32", { type: "Date", isJsonCompatible: false }],
|
|
310
|
+
["TypeSpec.duration", TYPESPEC_DURATION],
|
|
311
|
+
]);
|
|
312
|
+
|
|
11
313
|
/**
|
|
12
314
|
* Emits a declaration for a scalar type.
|
|
13
315
|
*
|
|
14
316
|
* This is rare in TypeScript, as the scalar will ordinarily be used inline, but may be desirable in some cases.
|
|
15
317
|
*
|
|
16
318
|
* @param ctx - The emitter context.
|
|
319
|
+
* @param module - The module that the scalar is being emitted in.
|
|
17
320
|
* @param scalar - The scalar to emit.
|
|
18
321
|
* @returns a string that declares an alias to the scalar type in TypeScript.
|
|
19
322
|
*/
|
|
20
|
-
export function emitScalar(ctx: JsContext, scalar: Scalar): string {
|
|
21
|
-
const jsScalar = getJsScalar(ctx
|
|
323
|
+
export function emitScalar(ctx: JsContext, scalar: Scalar, module: Module): string {
|
|
324
|
+
const jsScalar = getJsScalar(ctx, module, scalar, scalar.node.id);
|
|
22
325
|
|
|
23
326
|
const name = parseCase(scalar.name).pascalCase;
|
|
24
327
|
|
|
25
|
-
return `type ${name} = ${jsScalar};`;
|
|
328
|
+
return `type ${name} = ${jsScalar.type};`;
|
|
26
329
|
}
|
|
27
330
|
|
|
28
331
|
/**
|
|
29
|
-
*
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
* used to coerce the string value to the correct scalar type.
|
|
34
|
-
*
|
|
35
|
-
* The result of this function contains the string "{}" exactly once, which should be replaced with the text of an
|
|
36
|
-
* expression evaluating to the string representation of the scalar.
|
|
37
|
-
*
|
|
38
|
-
* For example, scalars that are represented by JS `number` are parsed with the template `Number({})`, which will
|
|
39
|
-
* convert the string to a number.
|
|
40
|
-
*
|
|
41
|
-
* @param ctx - The emitter context.
|
|
42
|
-
* @param scalar - The scalar to parse from a string
|
|
43
|
-
* @returns a template expression string that can be used to parse a string into the scalar type.
|
|
44
|
-
*/
|
|
45
|
-
export function parseTemplateForScalar(ctx: JsContext, scalar: Scalar): string {
|
|
46
|
-
const jsScalar = getJsScalar(ctx.program, scalar, scalar);
|
|
47
|
-
|
|
48
|
-
switch (jsScalar) {
|
|
49
|
-
case "string":
|
|
50
|
-
return "{}";
|
|
51
|
-
case "number":
|
|
52
|
-
return "Number({})";
|
|
53
|
-
case "bigint":
|
|
54
|
-
return "BigInt({})";
|
|
55
|
-
case "Uint8Array":
|
|
56
|
-
return "Buffer.from({}, 'base64')";
|
|
57
|
-
default:
|
|
58
|
-
throw new UnimplementedError(`parse template for scalar '${jsScalar}'`);
|
|
59
|
-
}
|
|
332
|
+
* Helper function template that makes any type T computable sensitive to the JsContext and module it is referenced from.
|
|
333
|
+
*/
|
|
334
|
+
interface Contextualized<T> {
|
|
335
|
+
(ctx: JsContext, module: Module): T;
|
|
60
336
|
}
|
|
61
337
|
|
|
62
338
|
/**
|
|
63
|
-
*
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
*/
|
|
67
|
-
export function encodeTemplateForScalar(ctx: JsContext, scalar: Scalar): string {
|
|
68
|
-
const jsScalar = getJsScalar(ctx.program, scalar, scalar);
|
|
69
|
-
|
|
70
|
-
switch (jsScalar) {
|
|
71
|
-
case "string":
|
|
72
|
-
return "{}";
|
|
73
|
-
case "number":
|
|
74
|
-
return "String({})";
|
|
75
|
-
case "bigint":
|
|
76
|
-
return "String({})";
|
|
77
|
-
case "Uint8Array":
|
|
78
|
-
return "{}.toString('base64')";
|
|
79
|
-
default:
|
|
80
|
-
throw new UnimplementedError(`encode template for scalar '${jsScalar}'`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
339
|
+
* The store of scalars for a given program.
|
|
340
|
+
*/
|
|
341
|
+
type ScalarStore = Map<Scalar, Contextualized<JsScalar>>;
|
|
83
342
|
|
|
84
|
-
|
|
343
|
+
/**
|
|
344
|
+
* The store of all scalars known to the emitter in all active Programs.
|
|
345
|
+
*/
|
|
346
|
+
const __JS_SCALARS_MAP = new WeakMap<Program, ScalarStore>();
|
|
85
347
|
|
|
86
|
-
|
|
348
|
+
/**
|
|
349
|
+
* Gets the scalar store for a given program.
|
|
350
|
+
*/
|
|
351
|
+
function getScalarStore(program: Program): ScalarStore {
|
|
87
352
|
let scalars = __JS_SCALARS_MAP.get(program);
|
|
88
353
|
|
|
89
354
|
if (scalars === undefined) {
|
|
90
|
-
scalars =
|
|
355
|
+
scalars = createScalarStore(program);
|
|
91
356
|
__JS_SCALARS_MAP.set(program, scalars);
|
|
92
357
|
}
|
|
93
358
|
|
|
94
359
|
return scalars;
|
|
95
360
|
}
|
|
96
361
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
[program.resolveTypeReference("TypeSpec.int32"), "number"],
|
|
109
|
-
[program.resolveTypeReference("TypeSpec.int16"), "number"],
|
|
110
|
-
[program.resolveTypeReference("TypeSpec.int8"), "number"],
|
|
111
|
-
|
|
112
|
-
[program.resolveTypeReference("TypeSpec.safeint"), "number"],
|
|
113
|
-
[program.resolveTypeReference("TypeSpec.integer"), "bigint"],
|
|
114
|
-
[program.resolveTypeReference("TypeSpec.plainDate"), "Date"],
|
|
115
|
-
[program.resolveTypeReference("TypeSpec.plainTime"), "Date"],
|
|
116
|
-
[program.resolveTypeReference("TypeSpec.utcDateTime"), "Date"],
|
|
117
|
-
] as const;
|
|
118
|
-
|
|
119
|
-
for (const [[type, diagnostics]] of entries) {
|
|
120
|
-
if (!type) {
|
|
121
|
-
const diagnosticString = diagnostics.map((x) => formatDiagnostic(x)).join("\n");
|
|
122
|
-
throw new Error(`failed to construct TypeSpec -> JavaScript scalar map: ${diagnosticString}`);
|
|
123
|
-
} else if (type.kind !== "Scalar") {
|
|
124
|
-
throw new Error(
|
|
125
|
-
`type ${(type as any).name ?? "<anonymous>"} is a '${type.kind}', expected 'scalar'`,
|
|
126
|
-
);
|
|
362
|
+
/**
|
|
363
|
+
* Initializes a scalar store for a given program.
|
|
364
|
+
*/
|
|
365
|
+
function createScalarStore(program: Program): ScalarStore {
|
|
366
|
+
const m = new Map<Scalar, Contextualized<JsScalar>>();
|
|
367
|
+
|
|
368
|
+
for (const [scalarName, scalarInfo] of SCALARS) {
|
|
369
|
+
const [scalar, diagnostics] = program.resolveTypeReference(scalarName);
|
|
370
|
+
|
|
371
|
+
if (diagnostics.length > 0 || !scalar || scalar.kind !== "Scalar") {
|
|
372
|
+
throw new UnreachableError(`Failed to resolve built-in scalar '${scalarName}'`);
|
|
127
373
|
}
|
|
374
|
+
|
|
375
|
+
m.set(scalar, createJsScalar(program, scalar, scalarInfo, m));
|
|
128
376
|
}
|
|
129
377
|
|
|
130
|
-
return
|
|
378
|
+
return m;
|
|
131
379
|
}
|
|
132
380
|
|
|
381
|
+
/**
|
|
382
|
+
* Binds a ScalarInfo specification to a JsScalar.
|
|
383
|
+
*
|
|
384
|
+
* @param program - The program that contains the scalar.
|
|
385
|
+
* @param scalar - The scalar to bind.
|
|
386
|
+
* @param scalarInfo - The scalar information spec to bind.
|
|
387
|
+
* @param store - The scalar store to use for the scalar.
|
|
388
|
+
* @returns a function that takes a JsContext and Module and returns a JsScalar.
|
|
389
|
+
*/
|
|
390
|
+
function createJsScalar(
|
|
391
|
+
program: Program,
|
|
392
|
+
scalar: Scalar,
|
|
393
|
+
scalarInfo: ScalarInfo,
|
|
394
|
+
store: ScalarStore,
|
|
395
|
+
): Contextualized<JsScalar> {
|
|
396
|
+
return (ctx, module) => {
|
|
397
|
+
const _http: { [K in HttpOperationParameter["type"]]?: Encoder } = {};
|
|
398
|
+
let _type: string | undefined = undefined;
|
|
399
|
+
|
|
400
|
+
const self = {
|
|
401
|
+
get type() {
|
|
402
|
+
return (_type ??=
|
|
403
|
+
typeof scalarInfo.type === "function" ? scalarInfo.type(ctx, module) : scalarInfo.type);
|
|
404
|
+
},
|
|
405
|
+
|
|
406
|
+
scalar,
|
|
407
|
+
|
|
408
|
+
getEncoding(encoding: string, target: Scalar): Encoder | undefined {
|
|
409
|
+
encoding = encoding.toLowerCase();
|
|
410
|
+
let encodingSpec = scalarInfo.encodings?.[getFullyQualifiedTypeName(target)]?.[encoding];
|
|
411
|
+
|
|
412
|
+
if (encodingSpec === undefined) {
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
encodingSpec =
|
|
417
|
+
typeof encodingSpec === "function" ? encodingSpec(ctx, module) : encodingSpec;
|
|
418
|
+
|
|
419
|
+
let _target: JsScalar | undefined = undefined;
|
|
420
|
+
let _decodeTemplate: string | undefined = undefined;
|
|
421
|
+
let _encodeTemplate: string | undefined = undefined;
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
get target() {
|
|
425
|
+
return (_target ??= store.get(target)!(ctx, module));
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
decode(subject) {
|
|
429
|
+
_decodeTemplate ??=
|
|
430
|
+
typeof encodingSpec.decodeTemplate === "function"
|
|
431
|
+
? encodingSpec.decodeTemplate(ctx, module)
|
|
432
|
+
: (encodingSpec.decodeTemplate ?? "{}");
|
|
433
|
+
|
|
434
|
+
subject = `(${subject})`;
|
|
435
|
+
|
|
436
|
+
// If we have a via, decode it last
|
|
437
|
+
|
|
438
|
+
subject = _decodeTemplate.replaceAll("{}", subject);
|
|
439
|
+
|
|
440
|
+
if (isVia(encodingSpec)) {
|
|
441
|
+
const via = self.getEncoding(encodingSpec.via, target);
|
|
442
|
+
|
|
443
|
+
if (via === undefined) {
|
|
444
|
+
return subject;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
subject = via.decode(subject);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return subject;
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
encode(subject) {
|
|
454
|
+
_encodeTemplate ??=
|
|
455
|
+
typeof encodingSpec.encodeTemplate === "function"
|
|
456
|
+
? encodingSpec.encodeTemplate(ctx, module)
|
|
457
|
+
: (encodingSpec.encodeTemplate ?? "{}");
|
|
458
|
+
|
|
459
|
+
subject = `(${subject})`;
|
|
460
|
+
|
|
461
|
+
// If we have a via, encode to it first
|
|
462
|
+
|
|
463
|
+
if (isVia(encodingSpec)) {
|
|
464
|
+
const via = self.getEncoding(encodingSpec.via, target);
|
|
465
|
+
|
|
466
|
+
if (via === undefined) {
|
|
467
|
+
return subject;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
subject = via.encode(subject);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
subject = _encodeTemplate.replaceAll("{}", subject);
|
|
474
|
+
|
|
475
|
+
return subject;
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
},
|
|
479
|
+
|
|
480
|
+
getDefaultMimeEncoding(target: string): Encoder | undefined {
|
|
481
|
+
const encoding = scalarInfo.defaultEncodings?.byMimeType?.[target];
|
|
482
|
+
|
|
483
|
+
if (encoding === undefined) {
|
|
484
|
+
return undefined;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const [encodingType, encodingName] = encoding;
|
|
488
|
+
|
|
489
|
+
const [encodingScalar, diagnostics] = program.resolveTypeReference(encodingType);
|
|
490
|
+
|
|
491
|
+
if (diagnostics.length > 0 || !encodingScalar || encodingScalar.kind !== "Scalar") {
|
|
492
|
+
throw new UnreachableError(`Failed to resolve built-in scalar '${encodingType}'`);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return self.getEncoding(encodingName, encodingScalar);
|
|
496
|
+
},
|
|
497
|
+
|
|
498
|
+
http: {
|
|
499
|
+
get header(): Encoder {
|
|
500
|
+
return (_http.header ??= getHttpEncoder(ctx, module, self, "header"));
|
|
501
|
+
},
|
|
502
|
+
get query(): Encoder {
|
|
503
|
+
return (_http.query ??= getHttpEncoder(ctx, module, self, "query"));
|
|
504
|
+
},
|
|
505
|
+
get cookie(): Encoder {
|
|
506
|
+
return (_http.cookie ??= getHttpEncoder(ctx, module, self, "cookie"));
|
|
507
|
+
},
|
|
508
|
+
get path(): Encoder {
|
|
509
|
+
return (_http.path ??= getHttpEncoder(ctx, module, self, "path"));
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
|
|
513
|
+
isJsonCompatible: scalarInfo.isJsonCompatible,
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
return self;
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Helper to get the HTTP encoders for the scalar.
|
|
521
|
+
*/
|
|
522
|
+
function getHttpEncoder(
|
|
523
|
+
ctx: JsContext,
|
|
524
|
+
module: Module,
|
|
525
|
+
self: JsScalar,
|
|
526
|
+
form: HttpOperationParameter["type"],
|
|
527
|
+
) {
|
|
528
|
+
const [target, encoding] = scalarInfo.defaultEncodings?.http?.[form] ?? [
|
|
529
|
+
"TypeSpec.string",
|
|
530
|
+
"default",
|
|
531
|
+
];
|
|
532
|
+
|
|
533
|
+
const [targetScalar, diagnostics] = program.resolveTypeReference(target);
|
|
534
|
+
|
|
535
|
+
if (diagnostics.length > 0 || !targetScalar || targetScalar.kind !== "Scalar") {
|
|
536
|
+
throw new UnreachableError(`Failed to resolve built-in scalar '${target}'`);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
let encoder = self.getEncoding(encoding, targetScalar);
|
|
540
|
+
|
|
541
|
+
if (encoder === undefined && scalarInfo.defaultEncodings?.http?.[form]) {
|
|
542
|
+
throw new UnreachableError(`Default HTTP ${form} encoding specified but failed to resolve.`);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
encoder ??= getDefaultHttpStringEncoder(ctx, module, form);
|
|
546
|
+
|
|
547
|
+
return encoder;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Returns `true` if the encoding is provided `via` another encoding. False otherwise.
|
|
553
|
+
*/
|
|
554
|
+
function isVia(encoding: ScalarEncoding): encoding is ScalarEncodingVia {
|
|
555
|
+
return "via" in encoding;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** Map to ensure we don't report the same unrecognized scalar many times. */
|
|
559
|
+
const REPORTED_UNRECOGNIZED_SCALARS = new WeakMap<Program, Set<Scalar>>();
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Reports a scalar as unrecognized, so that the spec author knows it is treated as `unknown`.
|
|
563
|
+
*
|
|
564
|
+
* @param ctx - The emitter context.
|
|
565
|
+
* @param scalar - The scalar that was not recognized.
|
|
566
|
+
* @param target - The diagnostic target to report the error on.
|
|
567
|
+
*/
|
|
568
|
+
export function reportUnrecognizedScalar(
|
|
569
|
+
ctx: JsContext,
|
|
570
|
+
scalar: Scalar,
|
|
571
|
+
target: DiagnosticTarget | typeof NoTarget,
|
|
572
|
+
) {
|
|
573
|
+
let reported = REPORTED_UNRECOGNIZED_SCALARS.get(ctx.program);
|
|
574
|
+
|
|
575
|
+
if (reported === undefined) {
|
|
576
|
+
reported = new Set();
|
|
577
|
+
REPORTED_UNRECOGNIZED_SCALARS.set(ctx.program, reported);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (reported.has(scalar)) {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
reportDiagnostic(ctx.program, {
|
|
585
|
+
code: "unrecognized-scalar",
|
|
586
|
+
target: target,
|
|
587
|
+
format: {
|
|
588
|
+
scalar: getFullyQualifiedTypeName(scalar),
|
|
589
|
+
},
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
reported.add(scalar);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Gets the default string encoder for HTTP metadata.
|
|
597
|
+
*/
|
|
598
|
+
function getDefaultHttpStringEncoder(
|
|
599
|
+
ctx: JsContext,
|
|
600
|
+
module: Module,
|
|
601
|
+
form: HttpOperationParameter["type"],
|
|
602
|
+
): Encoder {
|
|
603
|
+
const string = ctx.program.checker.getStdType("string");
|
|
604
|
+
|
|
605
|
+
const scalar = getJsScalar(ctx, module, string, NoTarget);
|
|
606
|
+
|
|
607
|
+
return {
|
|
608
|
+
target: scalar,
|
|
609
|
+
encode: HTTP_ENCODE_STRING,
|
|
610
|
+
decode: HTTP_DECODE_STRING,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Encoders for HTTP metadata.
|
|
615
|
+
const HTTP_ENCODE_STRING: Encoder["encode"] = (subject) => `JSON.stringify(${subject})`;
|
|
616
|
+
const HTTP_DECODE_STRING: Encoder["decode"] = (subject) => `JSON.parse(${subject})`;
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* An encoder that encodes a scalar type to the `target` scalar type.
|
|
620
|
+
*
|
|
621
|
+
* The type that this encoder encodes _from_ is the type of the scalar that it is bound to. It _MUST_ be used only with expressions
|
|
622
|
+
* of the type that represents the source scalar.
|
|
623
|
+
*/
|
|
624
|
+
export interface Encoder {
|
|
625
|
+
/**
|
|
626
|
+
* The target scalar type that this encoder encodes to.
|
|
627
|
+
*/
|
|
628
|
+
readonly target: JsScalar;
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Produces an expression that encodes the `subject` expression of the source type into the target.
|
|
632
|
+
*
|
|
633
|
+
* @param subject - An expression of the type that represents the source scalar.
|
|
634
|
+
*/
|
|
635
|
+
encode(subject: string): string;
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Produces an expression that decodes the `subject` expression from the target into the source type.
|
|
639
|
+
*
|
|
640
|
+
* @param subject - An expression of the type that represents the target scalar.
|
|
641
|
+
*/
|
|
642
|
+
decode(subject: string): string;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* A representation of a TypeSpec scalar in TypeScript.
|
|
647
|
+
*/
|
|
648
|
+
export interface JsScalar {
|
|
649
|
+
/**
|
|
650
|
+
* The TypeScript type that represents the scalar.
|
|
651
|
+
*/
|
|
652
|
+
readonly type: string;
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* The TypeSpec scalar that it represents, or "unknown" if the Scalar is not recognized.
|
|
656
|
+
*/
|
|
657
|
+
readonly scalar: Scalar | "unknown";
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Get an encoder that encodes this scalar type to a different scalar type using a given encoding.
|
|
661
|
+
*
|
|
662
|
+
* @param encoding - the encoding to use (e.g. "base64", "base64url", etc.)
|
|
663
|
+
* @param target - the target scalar type to encode to
|
|
664
|
+
* @returns an encoder that encodes this scalar type to the target scalar type using the given encoding, or undefined
|
|
665
|
+
* if the encoding is not supported.
|
|
666
|
+
*/
|
|
667
|
+
getEncoding(encoding: string, target: Scalar): Encoder | undefined;
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Get the default encoder for a given media type.
|
|
671
|
+
*
|
|
672
|
+
* @param mimeType - the media type to get the default encoder for (e.g. "application/json", "text/plain", etc.)
|
|
673
|
+
* @returns an encoder that encodes this scalar type to the target scalar type using the given encoding, or undefined
|
|
674
|
+
* if no default encoder is defined for the given media type.
|
|
675
|
+
*/
|
|
676
|
+
getDefaultMimeEncoding(mimeType: string): Encoder | undefined;
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Whether this scalar can be used directly in JSON serialization.
|
|
680
|
+
*
|
|
681
|
+
* If true, this scalar will be represented faithfully if it is passed to JSON.stringify or JSON.parse.
|
|
682
|
+
*/
|
|
683
|
+
isJsonCompatible: boolean;
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* A map of encoders when this type is used in HTTP metadata.
|
|
687
|
+
*/
|
|
688
|
+
readonly http: {
|
|
689
|
+
readonly [K in HttpOperationParameter["type"]]: Encoder;
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* A dummy encoder that just converts the value to a string and does not decode it.
|
|
695
|
+
*
|
|
696
|
+
* This is used for "unknown" scalars.
|
|
697
|
+
*/
|
|
698
|
+
const DEFAULT_STRING_ENCODER_RAW: Omit<Encoder, "target"> = {
|
|
699
|
+
encode(subject) {
|
|
700
|
+
return `String(${subject})`;
|
|
701
|
+
},
|
|
702
|
+
decode(subject) {
|
|
703
|
+
return `${subject}`;
|
|
704
|
+
},
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* A JsScalar value that represents an unknown scalar.
|
|
709
|
+
*/
|
|
710
|
+
export const JS_SCALAR_UNKNOWN: JsScalar = {
|
|
711
|
+
type: "unknown",
|
|
712
|
+
scalar: "unknown",
|
|
713
|
+
getEncoding: () => undefined,
|
|
714
|
+
getDefaultMimeEncoding: () => undefined,
|
|
715
|
+
http: {
|
|
716
|
+
get header() {
|
|
717
|
+
return {
|
|
718
|
+
target: JS_SCALAR_UNKNOWN,
|
|
719
|
+
...DEFAULT_STRING_ENCODER_RAW,
|
|
720
|
+
};
|
|
721
|
+
},
|
|
722
|
+
get query() {
|
|
723
|
+
return {
|
|
724
|
+
target: JS_SCALAR_UNKNOWN,
|
|
725
|
+
...DEFAULT_STRING_ENCODER_RAW,
|
|
726
|
+
};
|
|
727
|
+
},
|
|
728
|
+
get cookie() {
|
|
729
|
+
return {
|
|
730
|
+
target: JS_SCALAR_UNKNOWN,
|
|
731
|
+
...DEFAULT_STRING_ENCODER_RAW,
|
|
732
|
+
};
|
|
733
|
+
},
|
|
734
|
+
get path() {
|
|
735
|
+
return {
|
|
736
|
+
target: JS_SCALAR_UNKNOWN,
|
|
737
|
+
...DEFAULT_STRING_ENCODER_RAW,
|
|
738
|
+
};
|
|
739
|
+
},
|
|
740
|
+
},
|
|
741
|
+
isJsonCompatible: true,
|
|
742
|
+
};
|
|
743
|
+
|
|
133
744
|
/**
|
|
134
745
|
* Gets a TypeScript type that can represent a given TypeSpec scalar.
|
|
135
746
|
*
|
|
@@ -143,11 +754,12 @@ function createScalarsMap(program: Program): Map<Scalar, string> {
|
|
|
143
754
|
* @returns a string containing a TypeScript type that can represent the scalar
|
|
144
755
|
*/
|
|
145
756
|
export function getJsScalar(
|
|
146
|
-
|
|
757
|
+
ctx: JsContext,
|
|
758
|
+
module: Module,
|
|
147
759
|
scalar: Scalar,
|
|
148
760
|
diagnosticTarget: DiagnosticTarget | typeof NoTarget,
|
|
149
|
-
):
|
|
150
|
-
const scalars =
|
|
761
|
+
): JsScalar {
|
|
762
|
+
const scalars = getScalarStore(ctx.program);
|
|
151
763
|
|
|
152
764
|
let _scalar: Scalar | undefined = scalar;
|
|
153
765
|
|
|
@@ -155,19 +767,13 @@ export function getJsScalar(
|
|
|
155
767
|
const jsScalar = scalars.get(_scalar);
|
|
156
768
|
|
|
157
769
|
if (jsScalar !== undefined) {
|
|
158
|
-
return jsScalar;
|
|
770
|
+
return jsScalar(ctx, module);
|
|
159
771
|
}
|
|
160
772
|
|
|
161
773
|
_scalar = _scalar.baseScalar;
|
|
162
774
|
}
|
|
163
775
|
|
|
164
|
-
|
|
165
|
-
code: "unrecognized-scalar",
|
|
166
|
-
target: diagnosticTarget,
|
|
167
|
-
format: {
|
|
168
|
-
scalar: getFullyQualifiedTypeName(scalar),
|
|
169
|
-
},
|
|
170
|
-
});
|
|
776
|
+
reportUnrecognizedScalar(ctx, scalar, diagnosticTarget);
|
|
171
777
|
|
|
172
|
-
return
|
|
778
|
+
return JS_SCALAR_UNKNOWN;
|
|
173
779
|
}
|