@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.
Files changed (61) hide show
  1. package/dist/generated-defs/helpers/datetime.d.ts +4 -0
  2. package/dist/generated-defs/helpers/datetime.d.ts.map +1 -0
  3. package/dist/generated-defs/helpers/datetime.js +256 -0
  4. package/dist/generated-defs/helpers/datetime.js.map +1 -0
  5. package/dist/generated-defs/helpers/index.d.ts.map +1 -1
  6. package/dist/generated-defs/helpers/index.js +1 -0
  7. package/dist/generated-defs/helpers/index.js.map +1 -1
  8. package/dist/src/common/declaration.js +1 -1
  9. package/dist/src/common/declaration.js.map +1 -1
  10. package/dist/src/common/reference.js +1 -1
  11. package/dist/src/common/reference.js.map +1 -1
  12. package/dist/src/common/scalar.d.ts +175 -22
  13. package/dist/src/common/scalar.d.ts.map +1 -1
  14. package/dist/src/common/scalar.js +420 -93
  15. package/dist/src/common/scalar.js.map +1 -1
  16. package/dist/src/common/serialization/index.d.ts +2 -2
  17. package/dist/src/common/serialization/index.d.ts.map +1 -1
  18. package/dist/src/common/serialization/index.js +9 -3
  19. package/dist/src/common/serialization/index.js.map +1 -1
  20. package/dist/src/common/serialization/json.d.ts +2 -2
  21. package/dist/src/common/serialization/json.d.ts.map +1 -1
  22. package/dist/src/common/serialization/json.js +144 -42
  23. package/dist/src/common/serialization/json.js.map +1 -1
  24. package/dist/src/helpers/datetime.d.ts +92 -0
  25. package/dist/src/helpers/datetime.d.ts.map +1 -0
  26. package/dist/src/helpers/datetime.js +151 -0
  27. package/dist/src/helpers/datetime.js.map +1 -0
  28. package/dist/src/http/server/index.d.ts.map +1 -1
  29. package/dist/src/http/server/index.js +17 -12
  30. package/dist/src/http/server/index.js.map +1 -1
  31. package/dist/src/http/server/multipart.js +1 -1
  32. package/dist/src/http/server/multipart.js.map +1 -1
  33. package/dist/src/lib.d.ts +10 -1
  34. package/dist/src/lib.d.ts.map +1 -1
  35. package/dist/src/lib.js +6 -0
  36. package/dist/src/lib.js.map +1 -1
  37. package/dist/src/util/case.d.ts +9 -0
  38. package/dist/src/util/case.d.ts.map +1 -1
  39. package/dist/src/util/case.js +18 -0
  40. package/dist/src/util/case.js.map +1 -1
  41. package/dist/src/util/differentiate.d.ts +4 -4
  42. package/dist/src/util/differentiate.d.ts.map +1 -1
  43. package/dist/src/util/differentiate.js +10 -10
  44. package/dist/src/util/differentiate.js.map +1 -1
  45. package/generated-defs/helpers/datetime.ts +263 -0
  46. package/generated-defs/helpers/index.ts +1 -0
  47. package/package.json +7 -7
  48. package/src/common/declaration.ts +1 -1
  49. package/src/common/reference.ts +1 -1
  50. package/src/common/scalar.ts +709 -103
  51. package/src/common/serialization/index.ts +11 -4
  52. package/src/common/serialization/json.ts +174 -52
  53. package/src/helpers/datetime.ts +235 -0
  54. package/src/http/server/index.ts +29 -15
  55. package/src/http/server/multipart.ts +1 -1
  56. package/src/lib.ts +6 -0
  57. package/src/util/case.ts +19 -0
  58. package/src/util/differentiate.ts +15 -8
  59. package/temp/tsconfig.tsbuildinfo +1 -1
  60. package/test/datetime.test.ts +226 -0
  61. package/test/scalar.test.ts +345 -0
@@ -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, formatDiagnostic } from "@typespec/compiler";
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.program, scalar, scalar.node.id);
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
- * Get the string parsing template for a given scalar.
30
- *
31
- * It is common that a scalar type is encoded as a string. For example, in HTTP path parameters or query parameters
32
- * where the value may be an integer, but the APIs expose it as a string. In such cases the parse template may be
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
- * Get the string encoding template for a given scalar.
64
- * @param ctx
65
- * @param scalar
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
- const __JS_SCALARS_MAP = new Map<Program, Map<Scalar, string>>();
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
- function getScalarsMap(program: Program): Map<Scalar, string> {
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 = createScalarsMap(program);
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
- function createScalarsMap(program: Program): Map<Scalar, string> {
98
- const entries = [
99
- [program.resolveTypeReference("TypeSpec.bytes"), "Uint8Array"],
100
- [program.resolveTypeReference("TypeSpec.boolean"), "boolean"],
101
- [program.resolveTypeReference("TypeSpec.string"), "string"],
102
- [program.resolveTypeReference("TypeSpec.float32"), "number"],
103
- [program.resolveTypeReference("TypeSpec.float64"), "number"],
104
-
105
- [program.resolveTypeReference("TypeSpec.uint32"), "number"],
106
- [program.resolveTypeReference("TypeSpec.uint16"), "number"],
107
- [program.resolveTypeReference("TypeSpec.uint8"), "number"],
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 new Map<Scalar, string>(entries.map(([[type], scalar]) => [type! as Scalar, scalar]));
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
- program: Program,
757
+ ctx: JsContext,
758
+ module: Module,
147
759
  scalar: Scalar,
148
760
  diagnosticTarget: DiagnosticTarget | typeof NoTarget,
149
- ): string {
150
- const scalars = getScalarsMap(program);
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
- reportDiagnostic(program, {
165
- code: "unrecognized-scalar",
166
- target: diagnosticTarget,
167
- format: {
168
- scalar: getFullyQualifiedTypeName(scalar),
169
- },
170
- });
776
+ reportUnrecognizedScalar(ctx, scalar, diagnosticTarget);
171
777
 
172
- return "unknown";
778
+ return JS_SCALAR_UNKNOWN;
173
779
  }