@typespec/http-server-js 0.58.0-alpha.13-dev.3 → 0.58.0-alpha.13-dev.6
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/.testignore +0 -3
- package/cmd/hsjs-scaffold.mjs +7 -0
- package/dist/src/http/server/index.d.ts.map +1 -1
- package/dist/src/http/server/index.js +75 -1
- 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 +35 -43
- package/dist/src/http/server/router.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/scripts/scaffold/bin.d.mts +1 -0
- package/dist/src/scripts/scaffold/bin.d.mts.map +1 -1
- package/dist/src/scripts/scaffold/bin.mjs +6 -6
- package/dist/src/scripts/scaffold/bin.mjs.map +1 -1
- package/dist/src/scripts/scaffold/data-mocks.d.ts +2 -1
- package/dist/src/scripts/scaffold/data-mocks.d.ts.map +1 -1
- package/dist/src/scripts/scaffold/data-mocks.js +36 -23
- package/dist/src/scripts/scaffold/data-mocks.js.map +1 -1
- package/dist/src/util/differentiate.d.ts +25 -2
- package/dist/src/util/differentiate.d.ts.map +1 -1
- package/dist/src/util/differentiate.js +25 -33
- package/dist/src/util/differentiate.js.map +1 -1
- package/package.json +2 -2
- package/src/http/server/index.ts +83 -1
- package/src/http/server/router.ts +45 -58
- package/src/lib.ts +6 -0
- package/src/scripts/scaffold/bin.mts +7 -6
- package/src/scripts/scaffold/data-mocks.ts +43 -23
- package/src/util/differentiate.ts +58 -23
- package/temp/tsconfig.tsbuildinfo +1 -1
package/src/http/server/index.ts
CHANGED
|
@@ -41,6 +41,7 @@ import { module as headerHelpers } from "../../../generated-defs/helpers/header.
|
|
|
41
41
|
import { module as httpHelpers } from "../../../generated-defs/helpers/http.js";
|
|
42
42
|
import { getJsScalar } from "../../common/scalar.js";
|
|
43
43
|
import { requiresJsonSerialization } from "../../common/serialization/json.js";
|
|
44
|
+
import { getFullyQualifiedTypeName } from "../../util/name.js";
|
|
44
45
|
|
|
45
46
|
const DEFAULT_CONTENT_TYPE = "application/json";
|
|
46
47
|
|
|
@@ -296,7 +297,7 @@ function* emitRawServerOperation(
|
|
|
296
297
|
|
|
297
298
|
break;
|
|
298
299
|
}
|
|
299
|
-
case "multipart/form-data":
|
|
300
|
+
case "multipart/form-data": {
|
|
300
301
|
if (body.bodyKind === "multipart") {
|
|
301
302
|
yield* indent(
|
|
302
303
|
emitMultipart(ctx, module, operation, body, names.ctx, bodyName, bodyTypeName),
|
|
@@ -305,7 +306,88 @@ function* emitRawServerOperation(
|
|
|
305
306
|
yield* indent(emitMultipartLegacy(names.ctx, bodyName, bodyTypeName));
|
|
306
307
|
}
|
|
307
308
|
break;
|
|
309
|
+
}
|
|
310
|
+
case "text/plain": {
|
|
311
|
+
const string = ctx.program.checker.getStdType("string");
|
|
312
|
+
const [assignable] = ctx.program.checker.isTypeAssignableTo(
|
|
313
|
+
body.type,
|
|
314
|
+
string,
|
|
315
|
+
body.property ?? body.type,
|
|
316
|
+
);
|
|
317
|
+
if (!assignable) {
|
|
318
|
+
const name =
|
|
319
|
+
("namespace" in body.type &&
|
|
320
|
+
body.type.namespace &&
|
|
321
|
+
getFullyQualifiedTypeName(body.type)) ||
|
|
322
|
+
("name" in body.type && typeof body.type.name === "string" && body.type.name) ||
|
|
323
|
+
"<unknown>";
|
|
324
|
+
reportDiagnostic(ctx.program, {
|
|
325
|
+
code: "unrecognized-media-type",
|
|
326
|
+
target: body.property ?? body.type,
|
|
327
|
+
format: {
|
|
328
|
+
mediaType: contentType,
|
|
329
|
+
type: name,
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
yield ` const ${bodyName} = await new Promise(function parse${bodyNameCase.pascalCase}(resolve, reject) {`;
|
|
335
|
+
yield ` const chunks: Array<Buffer> = [];`;
|
|
336
|
+
yield ` ${names.ctx}.request.on("data", function appendChunk(chunk) { chunks.push(chunk); });`;
|
|
337
|
+
yield ` ${names.ctx}.request.on("end", function finalize() {`;
|
|
338
|
+
yield ` try {`;
|
|
339
|
+
yield ` const body = Buffer.concat(chunks).toString();`;
|
|
340
|
+
yield ` resolve(body);`;
|
|
341
|
+
yield ` } catch (e) {`;
|
|
342
|
+
yield ` ${names.ctx}.errorHandlers.onInvalidRequest(`;
|
|
343
|
+
yield ` ${names.ctx},`;
|
|
344
|
+
yield ` ${JSON.stringify(operation.path)},`;
|
|
345
|
+
yield ` "invalid text in request body",`;
|
|
346
|
+
yield ` );`;
|
|
347
|
+
yield ` reject(e);`;
|
|
348
|
+
yield ` }`;
|
|
349
|
+
yield ` });`;
|
|
350
|
+
yield ` ${names.ctx}.request.on("error", reject);`;
|
|
351
|
+
yield ` }) as string;`;
|
|
352
|
+
yield "";
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
case "application/octet-stream":
|
|
308
356
|
default:
|
|
357
|
+
{
|
|
358
|
+
if (!ctx.program.checker.isStdType(body.type, "bytes")) {
|
|
359
|
+
const name =
|
|
360
|
+
("namespace" in body.type &&
|
|
361
|
+
body.type.namespace &&
|
|
362
|
+
getFullyQualifiedTypeName(body.type)) ||
|
|
363
|
+
("name" in body.type && typeof body.type.name === "string" && body.type.name) ||
|
|
364
|
+
"<unknown>";
|
|
365
|
+
|
|
366
|
+
reportDiagnostic(ctx.program, {
|
|
367
|
+
code: "unrecognized-media-type",
|
|
368
|
+
target: body.property ?? body.type,
|
|
369
|
+
format: {
|
|
370
|
+
mediaType: contentType,
|
|
371
|
+
type: name,
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
yield ` const ${bodyName} = await new Promise(function parse${bodyNameCase.pascalCase}(resolve, reject) {`;
|
|
376
|
+
yield ` const chunks: Array<Buffer> = [];`;
|
|
377
|
+
yield ` ${names.ctx}.request.on("data", function appendChunk(chunk) { chunks.push(chunk); });`;
|
|
378
|
+
yield ` ${names.ctx}.request.on("end", function finalize() {`;
|
|
379
|
+
yield ` try {`;
|
|
380
|
+
yield ` const body = Buffer.concat(chunks);`;
|
|
381
|
+
yield ` resolve(body);`;
|
|
382
|
+
yield ` } catch (e) {`;
|
|
383
|
+
yield ` reject(e);`;
|
|
384
|
+
yield ` }`;
|
|
385
|
+
yield ` });`;
|
|
386
|
+
yield ` ${names.ctx}.request.on("error", reject);`;
|
|
387
|
+
yield ` }) as Buffer;`;
|
|
388
|
+
yield "";
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
309
391
|
throw new UnimplementedError(`request deserialization for content-type: '${contentType}'`);
|
|
310
392
|
}
|
|
311
393
|
|
|
@@ -7,7 +7,8 @@ import {
|
|
|
7
7
|
HttpService,
|
|
8
8
|
HttpVerb,
|
|
9
9
|
OperationContainer,
|
|
10
|
-
|
|
10
|
+
getHeaderFieldName,
|
|
11
|
+
isHeader,
|
|
11
12
|
} from "@typespec/http";
|
|
12
13
|
import {
|
|
13
14
|
createOrGetModuleForNamespace,
|
|
@@ -22,9 +23,7 @@ import { HttpContext } from "../index.js";
|
|
|
22
23
|
|
|
23
24
|
import { module as headerHelpers } from "../../../generated-defs/helpers/header.js";
|
|
24
25
|
import { module as routerHelper } from "../../../generated-defs/helpers/router.js";
|
|
25
|
-
import {
|
|
26
|
-
import { reportDiagnostic } from "../../lib.js";
|
|
27
|
-
import { UnimplementedError } from "../../util/error.js";
|
|
26
|
+
import { differentiateModelTypes, writeCodeTree } from "../../util/differentiate.js";
|
|
28
27
|
|
|
29
28
|
/**
|
|
30
29
|
* Emit a router for the HTTP operations defined in a given service.
|
|
@@ -261,7 +260,9 @@ function* emitRouteHandler(
|
|
|
261
260
|
|
|
262
261
|
yield `if (path.length === 0) {`;
|
|
263
262
|
if (routeTree.operations.size > 0) {
|
|
264
|
-
yield* indent(
|
|
263
|
+
yield* indent(
|
|
264
|
+
emitRouteOperationDispatch(ctx, routeHandlers, routeTree.operations, backends, module),
|
|
265
|
+
);
|
|
265
266
|
} else {
|
|
266
267
|
// Not found
|
|
267
268
|
yield ` return ${onRouteNotFound}(ctx);`;
|
|
@@ -318,6 +319,7 @@ function* emitRouteOperationDispatch(
|
|
|
318
319
|
routeHandlers: string,
|
|
319
320
|
operations: Map<HttpVerb, RouteOperation[]>,
|
|
320
321
|
backends: Map<OperationContainer, [ReCase, string]>,
|
|
322
|
+
module: Module,
|
|
321
323
|
): Iterable<string> {
|
|
322
324
|
yield `switch (request.method) {`;
|
|
323
325
|
for (const [verb, operationList] of operations.entries()) {
|
|
@@ -339,11 +341,10 @@ function* emitRouteOperationDispatch(
|
|
|
339
341
|
yield ` return ${routeHandlers}.${operationName}(ctx, ${backendMemberName}${parameters});`;
|
|
340
342
|
} else {
|
|
341
343
|
// Shared route
|
|
342
|
-
const route = getHttpOperation(ctx.program, operationList[0].operation)[0].path;
|
|
343
344
|
yield ` case ${JSON.stringify(verb.toUpperCase())}:`;
|
|
344
345
|
yield* indent(
|
|
345
346
|
indent(
|
|
346
|
-
emitRouteOperationDispatchMultiple(ctx, routeHandlers, operationList,
|
|
347
|
+
emitRouteOperationDispatchMultiple(ctx, routeHandlers, operationList, backends, module),
|
|
347
348
|
),
|
|
348
349
|
);
|
|
349
350
|
}
|
|
@@ -366,64 +367,50 @@ function* emitRouteOperationDispatchMultiple(
|
|
|
366
367
|
ctx: HttpContext,
|
|
367
368
|
routeHandlers: string,
|
|
368
369
|
operations: RouteOperation[],
|
|
369
|
-
route: string,
|
|
370
370
|
backends: Map<OperationContainer, [ReCase, string]>,
|
|
371
|
+
module: Module,
|
|
371
372
|
): Iterable<string> {
|
|
372
|
-
const
|
|
373
|
-
|
|
373
|
+
const differentiated = differentiateModelTypes(
|
|
374
|
+
ctx,
|
|
375
|
+
module,
|
|
376
|
+
new Set(operations.map((op) => op.operation.parameters)),
|
|
377
|
+
{
|
|
378
|
+
renderPropertyName(prop): string {
|
|
379
|
+
return getHeaderFieldName(ctx.program, prop);
|
|
380
|
+
},
|
|
381
|
+
filter(prop): boolean {
|
|
382
|
+
return isHeader(ctx.program, prop);
|
|
383
|
+
},
|
|
384
|
+
else: {
|
|
385
|
+
kind: "verbatim",
|
|
386
|
+
body: [`return ctx.errorHandlers.onRequestNotFound(ctx);`],
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
);
|
|
374
390
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
391
|
+
yield* writeCodeTree(ctx, differentiated, {
|
|
392
|
+
referenceModelProperty(p) {
|
|
393
|
+
const headerName = getHeaderFieldName(ctx.program, p);
|
|
394
|
+
return `request.headers["${headerName}"]`;
|
|
395
|
+
},
|
|
396
|
+
*renderResult(type) {
|
|
397
|
+
const operation = operations.find((op) => op.operation.parameters === type)!;
|
|
398
|
+
const [backend] = backends.get(operation.container)!;
|
|
399
|
+
const operationName = keywordSafe(
|
|
400
|
+
backend.snakeCase + "_" + parseCase(operation.operation.name).snakeCase,
|
|
384
401
|
);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
if (usedContentTypes.has(operationContentType.value)) {
|
|
388
|
-
reportDiagnostic(ctx.program, {
|
|
389
|
-
code: "undifferentiable-route",
|
|
390
|
-
target: httpOperation.operation,
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
usedContentTypes.add(operationContentType.value);
|
|
395
|
-
|
|
396
|
-
contentTypeMap.set(operation, operationContentType.value);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
const contentTypeName = ctx.gensym("contentType");
|
|
400
402
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
yield `switch (${contentTypeName}) {`;
|
|
404
|
-
|
|
405
|
-
for (const [operation, contentType] of contentTypeMap.entries()) {
|
|
406
|
-
const [backend] = backends.get(operation.container)!;
|
|
407
|
-
const operationName = keywordSafe(
|
|
408
|
-
backend.snakeCase + "_" + parseCase(operation.operation.name).snakeCase,
|
|
409
|
-
);
|
|
410
|
-
|
|
411
|
-
const backendMemberName = keywordSafe(backend.camelCase);
|
|
412
|
-
|
|
413
|
-
const parameters =
|
|
414
|
-
operation.parameters.length > 0
|
|
415
|
-
? ", " + operation.parameters.map((param) => parseCase(param.name).camelCase).join(", ")
|
|
416
|
-
: "";
|
|
417
|
-
|
|
418
|
-
const contentTypeValue = parseHeaderValueParameters(contentType).value;
|
|
403
|
+
const backendMemberName = keywordSafe(backend.camelCase);
|
|
419
404
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
405
|
+
const parameters =
|
|
406
|
+
operation.parameters.length > 0
|
|
407
|
+
? ", " + operation.parameters.map((param) => parseCase(param.name).camelCase).join(", ")
|
|
408
|
+
: "";
|
|
423
409
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
410
|
+
yield `return ${routeHandlers}.${operationName}(ctx, ${backendMemberName}${parameters});`;
|
|
411
|
+
},
|
|
412
|
+
subject: "(request.headers)",
|
|
413
|
+
});
|
|
427
414
|
}
|
|
428
415
|
|
|
429
416
|
/**
|
package/src/lib.ts
CHANGED
|
@@ -148,6 +148,12 @@ export const $lib = createTypeSpecLibrary({
|
|
|
148
148
|
default: paramMessage`Unknown encoding '${"encoding"}' to type '${"target"}' for type '${"type"}'.`,
|
|
149
149
|
},
|
|
150
150
|
},
|
|
151
|
+
"unrecognized-media-type": {
|
|
152
|
+
severity: "error",
|
|
153
|
+
messages: {
|
|
154
|
+
default: paramMessage`unrecognized media (MIME) type '${"mediaType"}' for type '${"type"}'.`,
|
|
155
|
+
},
|
|
156
|
+
},
|
|
151
157
|
},
|
|
152
158
|
});
|
|
153
159
|
|
|
@@ -191,7 +191,9 @@ export async function scaffold(options: ScaffoldingOptions) {
|
|
|
191
191
|
config.options?.["@typespec/http-server-js"]?.["emitter-output-dir"];
|
|
192
192
|
const defaultOutputDir = path.resolve(path.dirname(projectYamlPath), "tsp-output");
|
|
193
193
|
|
|
194
|
-
const emitterOutputDir =
|
|
194
|
+
const emitterOutputDir =
|
|
195
|
+
emitterOutputDirTemplate?.replace("{output-dir}", defaultOutputDir) ??
|
|
196
|
+
path.join(defaultOutputDir, "@typespec", "http-server-js");
|
|
195
197
|
|
|
196
198
|
const baseOutputDir = options["no-standalone"] ? cwd : path.resolve(cwd, emitterOutputDir);
|
|
197
199
|
const tsConfigOutputPath = path.resolve(baseOutputDir, COMMON_PATHS.tsConfigJson);
|
|
@@ -662,7 +664,7 @@ function* emitControllerOperationHandlers(
|
|
|
662
664
|
yield `async ${opName}(ctx: HttpContext, ${paramsDeclarationLine}): ${returnType} {`;
|
|
663
665
|
}
|
|
664
666
|
|
|
665
|
-
const mockReturn = mockType(op.returnType);
|
|
667
|
+
const mockReturn = mockType(ctx, module, op.returnType);
|
|
666
668
|
|
|
667
669
|
if (mockReturn === undefined) {
|
|
668
670
|
importNotImplementedError = true;
|
|
@@ -789,7 +791,6 @@ function updatePackageJson(
|
|
|
789
791
|
}
|
|
790
792
|
}
|
|
791
793
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
});
|
|
794
|
+
export async function main() {
|
|
795
|
+
await scaffold(parseScaffoldArguments(process.argv));
|
|
796
|
+
}
|
|
@@ -9,7 +9,11 @@ import {
|
|
|
9
9
|
Union,
|
|
10
10
|
} from "@typespec/compiler";
|
|
11
11
|
import { $ } from "@typespec/compiler/experimental/typekit";
|
|
12
|
-
import {
|
|
12
|
+
import { JsContext, Module } from "../../ctx.js";
|
|
13
|
+
import { isUnspeakable, parseCase } from "../../util/case.js";
|
|
14
|
+
|
|
15
|
+
import { module as dateTimeHelper } from "../../../generated-defs/helpers/datetime.js";
|
|
16
|
+
import { KEYWORDS } from "../../util/keywords.js";
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
19
|
* Generates a mock value for a TypeSpec Model.
|
|
@@ -19,13 +23,13 @@ import { parseCase } from "../../util/case.js";
|
|
|
19
23
|
* @returns A JavaScript string representation of the mock data
|
|
20
24
|
* @throws Error if a property cannot be mocked
|
|
21
25
|
*/
|
|
22
|
-
function mockModel(type: Model): string {
|
|
26
|
+
function mockModel(ctx: JsContext, module: Module, type: Model): string {
|
|
23
27
|
if ($.array.is(type)) {
|
|
24
|
-
return mockArray(type);
|
|
28
|
+
return mockArray(ctx, module, type);
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
if ($.record.is(type)) {
|
|
28
|
-
return mockRecord(type);
|
|
32
|
+
return mockRecord(ctx, module, type);
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
const mock: string[][] = [];
|
|
@@ -37,18 +41,18 @@ function mockModel(type: Model): string {
|
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
for (const [name, prop] of properties) {
|
|
40
|
-
if (prop.optional) {
|
|
44
|
+
if (prop.optional || isUnspeakable(prop.name)) {
|
|
41
45
|
continue;
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
const propMock = mockType(prop.type);
|
|
48
|
+
const propMock = mockType(ctx, module, prop.type);
|
|
45
49
|
|
|
46
50
|
if (!propMock) {
|
|
47
51
|
throw new Error(`Could not mock property ${name} of type ${prop.type.kind}`);
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
const propName = parseCase(name).camelCase;
|
|
51
|
-
mock.push([propName, propMock]);
|
|
55
|
+
mock.push([KEYWORDS.has(propName) ? `_${propName}` : propName, propMock]);
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
// If all properties were optional, return an empty object
|
|
@@ -67,9 +71,9 @@ function mockModel(type: Model): string {
|
|
|
67
71
|
* @param type - The TypeSpec array Model to mock
|
|
68
72
|
* @returns A JavaScript string representation of the mock array
|
|
69
73
|
*/
|
|
70
|
-
function mockArray(type: Model): string {
|
|
74
|
+
function mockArray(ctx: JsContext, module: Module, type: Model): string {
|
|
71
75
|
const elementType = $.array.getElementType(type);
|
|
72
|
-
const mockedType = mockType(elementType);
|
|
76
|
+
const mockedType = mockType(ctx, module, elementType);
|
|
73
77
|
|
|
74
78
|
// If we can't mock the element type, return an empty array
|
|
75
79
|
if (mockedType === undefined) {
|
|
@@ -85,9 +89,9 @@ function mockArray(type: Model): string {
|
|
|
85
89
|
* @param type - The TypeSpec record Model to mock
|
|
86
90
|
* @returns A JavaScript string representation of the mock record
|
|
87
91
|
*/
|
|
88
|
-
function mockRecord(type: Model): string {
|
|
92
|
+
function mockRecord(ctx: JsContext, module: Module, type: Model): string {
|
|
89
93
|
const elementType = $.record.getElementType(type);
|
|
90
|
-
const mockedType = mockType(elementType);
|
|
94
|
+
const mockedType = mockType(ctx, module, elementType);
|
|
91
95
|
|
|
92
96
|
if (mockedType === undefined) {
|
|
93
97
|
return "{}";
|
|
@@ -104,8 +108,12 @@ function mockRecord(type: Model): string {
|
|
|
104
108
|
* @param prop - The TypeSpec model property to mock
|
|
105
109
|
* @returns A JavaScript string representation of the mocked property or undefined if it cannot be mocked
|
|
106
110
|
*/
|
|
107
|
-
function mockModelProperty(
|
|
108
|
-
|
|
111
|
+
function mockModelProperty(
|
|
112
|
+
ctx: JsContext,
|
|
113
|
+
module: Module,
|
|
114
|
+
prop: ModelProperty,
|
|
115
|
+
): string | undefined {
|
|
116
|
+
return mockType(ctx, module, prop.type);
|
|
109
117
|
}
|
|
110
118
|
|
|
111
119
|
/**
|
|
@@ -125,9 +133,9 @@ function mockLiteral(type: LiteralType): string {
|
|
|
125
133
|
* @param type - The TypeSpec type to mock
|
|
126
134
|
* @returns A JavaScript string representation of the mock data, or undefined if the type cannot be mocked
|
|
127
135
|
*/
|
|
128
|
-
export function mockType(type: Type): string | undefined {
|
|
136
|
+
export function mockType(ctx: JsContext, module: Module, type: Type): string | undefined {
|
|
129
137
|
if ($.model.is(type)) {
|
|
130
|
-
return mockModel(type);
|
|
138
|
+
return mockModel(ctx, module, type);
|
|
131
139
|
}
|
|
132
140
|
|
|
133
141
|
if ($.literal.is(type)) {
|
|
@@ -135,15 +143,15 @@ export function mockType(type: Type): string | undefined {
|
|
|
135
143
|
}
|
|
136
144
|
|
|
137
145
|
if ($.modelProperty.is(type)) {
|
|
138
|
-
return mockModelProperty(type);
|
|
146
|
+
return mockModelProperty(ctx, module, type);
|
|
139
147
|
}
|
|
140
148
|
|
|
141
149
|
if ($.scalar.is(type)) {
|
|
142
|
-
return mockScalar(type);
|
|
150
|
+
return mockScalar(ctx, module, type);
|
|
143
151
|
}
|
|
144
152
|
|
|
145
153
|
if ($.union.is(type)) {
|
|
146
|
-
return mockUnion(type);
|
|
154
|
+
return mockUnion(ctx, module, type);
|
|
147
155
|
}
|
|
148
156
|
|
|
149
157
|
if (isVoidType(type)) {
|
|
@@ -159,12 +167,12 @@ export function mockType(type: Type): string | undefined {
|
|
|
159
167
|
* @param union - The TypeSpec union to mock
|
|
160
168
|
* @returns A JavaScript string representation of a mock for one variant, or undefined if no suitable variant is found
|
|
161
169
|
*/
|
|
162
|
-
function mockUnion(union: Union): string | undefined {
|
|
170
|
+
function mockUnion(ctx: JsContext, module: Module, union: Union): string | undefined {
|
|
163
171
|
for (const variant of union.variants.values()) {
|
|
164
172
|
if (isErrorType(variant.type)) {
|
|
165
173
|
continue;
|
|
166
174
|
}
|
|
167
|
-
return mockType(variant.type);
|
|
175
|
+
return mockType(ctx, module, variant.type);
|
|
168
176
|
}
|
|
169
177
|
|
|
170
178
|
return undefined;
|
|
@@ -177,12 +185,19 @@ function mockUnion(union: Union): string | undefined {
|
|
|
177
185
|
* @param scalar - The TypeSpec scalar to mock
|
|
178
186
|
* @returns A JavaScript string representation of a suitable mock value for the scalar type
|
|
179
187
|
*/
|
|
180
|
-
function mockScalar(scalar: Scalar): string | undefined {
|
|
188
|
+
function mockScalar(ctx: JsContext, module: Module, scalar: Scalar): string | undefined {
|
|
181
189
|
if ($.scalar.isBoolean(scalar) || $.scalar.extendsBoolean(scalar)) {
|
|
182
190
|
return JSON.stringify(true);
|
|
183
191
|
}
|
|
184
192
|
if ($.scalar.isNumeric(scalar) || $.scalar.extendsNumeric(scalar)) {
|
|
185
|
-
|
|
193
|
+
switch ((scalar as Scalar).name) {
|
|
194
|
+
case "integer":
|
|
195
|
+
case "int64":
|
|
196
|
+
case "uint64":
|
|
197
|
+
return "42n";
|
|
198
|
+
default:
|
|
199
|
+
return "42";
|
|
200
|
+
}
|
|
186
201
|
}
|
|
187
202
|
|
|
188
203
|
if ($.scalar.isUtcDateTime(scalar) || $.scalar.extendsUtcDateTime(scalar)) {
|
|
@@ -194,7 +209,12 @@ function mockScalar(scalar: Scalar): string | undefined {
|
|
|
194
209
|
}
|
|
195
210
|
|
|
196
211
|
if ($.scalar.isDuration(scalar) || $.scalar.extendsDuration(scalar)) {
|
|
197
|
-
|
|
212
|
+
module.imports.push({
|
|
213
|
+
from: dateTimeHelper,
|
|
214
|
+
binder: ["Duration"],
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
return 'Duration.parseISO8601("P1Y2M3DT4H5M6S")';
|
|
198
218
|
}
|
|
199
219
|
|
|
200
220
|
if ($.scalar.isOffsetDateTime(scalar) || $.scalar.extendsOffsetDateTime(scalar)) {
|
|
@@ -112,7 +112,7 @@ export interface Switch {
|
|
|
112
112
|
*/
|
|
113
113
|
export interface Verbatim {
|
|
114
114
|
kind: "verbatim";
|
|
115
|
-
body: Iterable<string
|
|
115
|
+
body: Iterable<string> | (() => Iterable<string>);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
/**
|
|
@@ -380,7 +380,7 @@ export function differentiateTypes(
|
|
|
380
380
|
const intrinsics = (categories.Intrinsic as (VoidType | NullType)[]) ?? [];
|
|
381
381
|
|
|
382
382
|
if (literals.length + scalars.length + intrinsics.length === 0) {
|
|
383
|
-
return differentiateModelTypes(ctx, module, select(models, cases), renderPropertyName);
|
|
383
|
+
return differentiateModelTypes(ctx, module, select(models, cases), { renderPropertyName });
|
|
384
384
|
} else {
|
|
385
385
|
const branches: IfBranch[] = [];
|
|
386
386
|
|
|
@@ -505,7 +505,7 @@ export function differentiateTypes(
|
|
|
505
505
|
branches,
|
|
506
506
|
else:
|
|
507
507
|
models.length > 0
|
|
508
|
-
? differentiateModelTypes(ctx, module, select(models, cases), renderPropertyName)
|
|
508
|
+
? differentiateModelTypes(ctx, module, select(models, cases), { renderPropertyName })
|
|
509
509
|
: undefined,
|
|
510
510
|
};
|
|
511
511
|
}
|
|
@@ -589,6 +589,38 @@ function overlaps(range: IntegerRange, other: IntegerRange): boolean {
|
|
|
589
589
|
return range[0] <= other[1] && range[1] >= other[0];
|
|
590
590
|
}
|
|
591
591
|
|
|
592
|
+
/**
|
|
593
|
+
* Optional paramters for model differentiation.
|
|
594
|
+
*/
|
|
595
|
+
interface DifferentiateModelOptions {
|
|
596
|
+
/**
|
|
597
|
+
* A function that converts a model property reference over the subject to a string.
|
|
598
|
+
*
|
|
599
|
+
* Default: `(prop) => prop.name`
|
|
600
|
+
*/
|
|
601
|
+
renderPropertyName?: (prop: ModelProperty) => string;
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* A filter function that determines which properties to consider for differentiation.
|
|
605
|
+
*
|
|
606
|
+
* Default: `() => true`
|
|
607
|
+
*/
|
|
608
|
+
filter?: (prop: ModelProperty) => boolean;
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* The default case to use if no other cases match.
|
|
612
|
+
*
|
|
613
|
+
* Default: undefined.
|
|
614
|
+
*/
|
|
615
|
+
else?: CodeTree | undefined;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const DEFAULT_DIFFERENTIATE_OPTIONS = {
|
|
619
|
+
renderPropertyName: PROPERTY_ID,
|
|
620
|
+
filter: () => true,
|
|
621
|
+
else: undefined,
|
|
622
|
+
} as const;
|
|
623
|
+
|
|
592
624
|
/**
|
|
593
625
|
* Differentiate a set of model types based on their properties. This function returns a CodeTree that will test an input
|
|
594
626
|
* "subject" and determine which of the cases it matches, executing the corresponding code block.
|
|
@@ -602,8 +634,15 @@ export function differentiateModelTypes(
|
|
|
602
634
|
ctx: JsContext,
|
|
603
635
|
module: Module,
|
|
604
636
|
models: Set<Model>,
|
|
605
|
-
|
|
637
|
+
options?: DifferentiateModelOptions,
|
|
638
|
+
): CodeTree;
|
|
639
|
+
export function differentiateModelTypes(
|
|
640
|
+
ctx: JsContext,
|
|
641
|
+
module: Module,
|
|
642
|
+
models: Set<Model>,
|
|
643
|
+
_options: DifferentiateModelOptions = {},
|
|
606
644
|
): CodeTree {
|
|
645
|
+
const options = { ...DEFAULT_DIFFERENTIATE_OPTIONS, ..._options };
|
|
607
646
|
// Horrible n^2 operation to get the unique properties of all models in the map, but hopefully n is small, so it should
|
|
608
647
|
// be okay until you have a lot of models to differentiate.
|
|
609
648
|
|
|
@@ -623,14 +662,14 @@ export function differentiateModelTypes(
|
|
|
623
662
|
for (const model of models) {
|
|
624
663
|
const props = new Set<string>();
|
|
625
664
|
|
|
626
|
-
for (const prop of getAllProperties(model)) {
|
|
665
|
+
for (const prop of getAllProperties(model).filter(options.filter)) {
|
|
627
666
|
// Don't consider optional properties for differentiation.
|
|
628
667
|
if (prop.optional) continue;
|
|
629
668
|
|
|
630
669
|
// Ignore properties that have no parseable name.
|
|
631
670
|
if (isUnspeakable(prop.name)) continue;
|
|
632
671
|
|
|
633
|
-
const renderedPropName = renderPropertyName(prop) as RenderedPropertyName;
|
|
672
|
+
const renderedPropName = options.renderPropertyName(prop) as RenderedPropertyName;
|
|
634
673
|
|
|
635
674
|
// CASE - literal value
|
|
636
675
|
|
|
@@ -716,7 +755,7 @@ export function differentiateModelTypes(
|
|
|
716
755
|
|
|
717
756
|
const branches: IfBranch[] = [];
|
|
718
757
|
|
|
719
|
-
let defaultCase:
|
|
758
|
+
let defaultCase: CodeTree | undefined = options.else;
|
|
720
759
|
|
|
721
760
|
for (const [model, unique] of uniqueProps) {
|
|
722
761
|
const literals = uniqueLiterals.get(model);
|
|
@@ -727,14 +766,11 @@ export function differentiateModelTypes(
|
|
|
727
766
|
code: "undifferentiable-model",
|
|
728
767
|
target: model,
|
|
729
768
|
});
|
|
730
|
-
return
|
|
731
|
-
kind: "result",
|
|
732
|
-
type: defaultCase,
|
|
733
|
-
};
|
|
769
|
+
return defaultCase;
|
|
734
770
|
} else {
|
|
735
771
|
// Allow a single default case. This covers more APIs that have a single model that is not differentiated by a
|
|
736
772
|
// unique property, in which case we can make it the `else` case.
|
|
737
|
-
defaultCase = model;
|
|
773
|
+
defaultCase = { kind: "result", type: model };
|
|
738
774
|
continue;
|
|
739
775
|
}
|
|
740
776
|
}
|
|
@@ -744,7 +780,7 @@ export function differentiateModelTypes(
|
|
|
744
780
|
const firstUniqueLiteral = literals.values().next().value as RenderedPropertyName;
|
|
745
781
|
|
|
746
782
|
const property = [...model.properties.values()].find(
|
|
747
|
-
(p) => (renderPropertyName(p) as RenderedPropertyName) === firstUniqueLiteral,
|
|
783
|
+
(p) => (options.renderPropertyName(p) as RenderedPropertyName) === firstUniqueLiteral,
|
|
748
784
|
)!;
|
|
749
785
|
|
|
750
786
|
branches.push({
|
|
@@ -752,7 +788,7 @@ export function differentiateModelTypes(
|
|
|
752
788
|
kind: "binary-op",
|
|
753
789
|
left: {
|
|
754
790
|
kind: "binary-op",
|
|
755
|
-
left: { kind: "literal", value: renderPropertyName(property) },
|
|
791
|
+
left: { kind: "literal", value: options.renderPropertyName(property) },
|
|
756
792
|
operator: "in",
|
|
757
793
|
right: SUBJECT,
|
|
758
794
|
},
|
|
@@ -774,7 +810,7 @@ export function differentiateModelTypes(
|
|
|
774
810
|
const firstUniqueRange = ranges.values().next().value as RenderedPropertyName;
|
|
775
811
|
|
|
776
812
|
const property = [...model.properties.values()].find(
|
|
777
|
-
(p) => renderPropertyName(p) === firstUniqueRange,
|
|
813
|
+
(p) => options.renderPropertyName(p) === firstUniqueRange,
|
|
778
814
|
)!;
|
|
779
815
|
|
|
780
816
|
const range = [...propertyRanges.get(firstUniqueRange)!.entries()].find(
|
|
@@ -786,7 +822,7 @@ export function differentiateModelTypes(
|
|
|
786
822
|
kind: "binary-op",
|
|
787
823
|
left: {
|
|
788
824
|
kind: "binary-op",
|
|
789
|
-
left: { kind: "literal", value: renderPropertyName(property) },
|
|
825
|
+
left: { kind: "literal", value: options.renderPropertyName(property) },
|
|
790
826
|
operator: "in",
|
|
791
827
|
right: SUBJECT,
|
|
792
828
|
},
|
|
@@ -817,12 +853,7 @@ export function differentiateModelTypes(
|
|
|
817
853
|
return {
|
|
818
854
|
kind: "if-chain",
|
|
819
855
|
branches,
|
|
820
|
-
else: defaultCase
|
|
821
|
-
? {
|
|
822
|
-
kind: "result",
|
|
823
|
-
type: defaultCase,
|
|
824
|
-
}
|
|
825
|
-
: undefined,
|
|
856
|
+
else: defaultCase,
|
|
826
857
|
};
|
|
827
858
|
}
|
|
828
859
|
|
|
@@ -903,7 +934,11 @@ export function* writeCodeTree(
|
|
|
903
934
|
break;
|
|
904
935
|
}
|
|
905
936
|
case "verbatim":
|
|
906
|
-
|
|
937
|
+
if (typeof tree.body === "function") {
|
|
938
|
+
yield* tree.body();
|
|
939
|
+
} else {
|
|
940
|
+
yield* tree.body;
|
|
941
|
+
}
|
|
907
942
|
break;
|
|
908
943
|
default:
|
|
909
944
|
throw new UnreachableError("writeCodeTree for " + (tree satisfies never as CodeTree).kind, {
|