@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.
@@ -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
- getHttpOperation,
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 { parseHeaderValueParameters } from "../../helpers/header.js";
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(emitRouteOperationDispatch(ctx, routeHandlers, routeTree.operations, backends));
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, route, backends),
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 usedContentTypes = new Set<string>();
373
- const contentTypeMap = new Map<RouteOperation, string>();
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
- for (const operation of operations) {
376
- const [httpOperation] = getHttpOperation(ctx.program, operation.operation);
377
- const operationContentType = httpOperation.parameters.parameters.find(
378
- (param) => param.type === "header" && param.name.toLowerCase() === "content-type",
379
- )?.param.type;
380
-
381
- if (!operationContentType || operationContentType.kind !== "String") {
382
- throw new UnimplementedError(
383
- "Only string content-types are supported for route differentiation.",
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
- yield `const ${contentTypeName} = parseHeaderValueParameters(request.headers["content-type"])?.value;`;
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
- yield ` case ${JSON.stringify(contentTypeValue)}:`;
421
- yield ` return ${routeHandlers}.${operationName}(ctx, ${backendMemberName}${parameters});`;
422
- }
405
+ const parameters =
406
+ operation.parameters.length > 0
407
+ ? ", " + operation.parameters.map((param) => parseCase(param.name).camelCase).join(", ")
408
+ : "";
423
409
 
424
- yield ` default:`;
425
- yield ` return ctx.errorHandlers.onInvalidRequest(ctx, ${JSON.stringify(route)}, \`No operation in route '${route}' matched content-type "\${${contentTypeName}}"\`);`;
426
- yield "}";
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 = emitterOutputDirTemplate.replace("{output-dir}", defaultOutputDir);
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
- scaffold(parseScaffoldArguments(process.argv)).catch((error) => {
793
- console.error(error);
794
- process.exit(1);
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 { parseCase } from "../../util/case.js";
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(prop: ModelProperty): string | undefined {
108
- return mockType(prop.type);
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
- return JSON.stringify(42);
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
- return JSON.stringify("P1Y2M3DT4H5M6S");
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
- renderPropertyName: (prop: ModelProperty) => string = PROPERTY_ID,
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: Model | undefined = undefined;
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
- yield* tree.body;
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, {