@typespec/http-server-js 0.58.0-alpha.13-dev.2 → 0.58.0-alpha.13-dev.4

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 (34) hide show
  1. package/.testignore +0 -4
  2. package/dist/src/common/enum.d.ts.map +1 -1
  3. package/dist/src/common/enum.js +2 -1
  4. package/dist/src/common/enum.js.map +1 -1
  5. package/dist/src/common/serialization/index.d.ts +2 -2
  6. package/dist/src/common/serialization/index.d.ts.map +1 -1
  7. package/dist/src/common/serialization/index.js +5 -1
  8. package/dist/src/common/serialization/index.js.map +1 -1
  9. package/dist/src/common/serialization/json.d.ts.map +1 -1
  10. package/dist/src/common/serialization/json.js +20 -6
  11. package/dist/src/common/serialization/json.js.map +1 -1
  12. package/dist/src/http/server/index.d.ts.map +1 -1
  13. package/dist/src/http/server/index.js +75 -1
  14. package/dist/src/http/server/index.js.map +1 -1
  15. package/dist/src/http/server/router.d.ts.map +1 -1
  16. package/dist/src/http/server/router.js +35 -43
  17. package/dist/src/http/server/router.js.map +1 -1
  18. package/dist/src/lib.d.ts +10 -1
  19. package/dist/src/lib.d.ts.map +1 -1
  20. package/dist/src/lib.js +6 -0
  21. package/dist/src/lib.js.map +1 -1
  22. package/dist/src/util/differentiate.d.ts +25 -2
  23. package/dist/src/util/differentiate.d.ts.map +1 -1
  24. package/dist/src/util/differentiate.js +25 -33
  25. package/dist/src/util/differentiate.js.map +1 -1
  26. package/package.json +1 -1
  27. package/src/common/enum.ts +2 -1
  28. package/src/common/serialization/index.ts +9 -3
  29. package/src/common/serialization/json.ts +25 -8
  30. package/src/http/server/index.ts +83 -1
  31. package/src/http/server/router.ts +45 -58
  32. package/src/lib.ts +6 -0
  33. package/src/util/differentiate.ts +58 -23
  34. package/temp/tsconfig.tsbuildinfo +1 -1
@@ -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
 
@@ -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, {