@typespec/openapi3 0.59.0-dev.0 → 0.59.0-dev.10

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.
@@ -1,13 +1,16 @@
1
- import { compilerAssert, createDiagnosticCollector, emitFile, getAllTags, getAnyExtensionFromPath, getDoc, getEncode, getFormat, getKnownValues, getMaxItems, getMaxLength, getMaxValue, getMaxValueExclusive, getMinItems, getMinLength, getMinValue, getMinValueExclusive, getNamespaceFullName, getOpExamples, getPattern, getService, getSummary, ignoreDiagnostics, interpolatePath, isDeprecated, isGlobalNamespace, isNeverType, isSecret, isVoidType, listServices, navigateTypesInNamespace, projectProgram, resolvePath, serializeValueAsJson, } from "@typespec/compiler";
1
+ import { compilerAssert, createDiagnosticCollector, emitFile, getAllTags, getAnyExtensionFromPath, getDoc, getFormat, getKnownValues, getMaxItems, getMaxLength, getMaxValue, getMaxValueExclusive, getMinItems, getMinLength, getMinValue, getMinValueExclusive, getNamespaceFullName, getPattern, getService, getSummary, ignoreDiagnostics, interpolatePath, isDeprecated, isGlobalNamespace, isNeverType, isSecret, isVoidType, listServices, navigateTypesInNamespace, projectProgram, resolvePath, } from "@typespec/compiler";
2
2
  import { createAssetEmitter } from "@typespec/compiler/emitter-framework";
3
3
  import { createMetadataInfo, getHttpService, getServers, getStatusCodeDescription, isContentTypeHeader, isOrExtendsHttpFile, isOverloadSameEndpoint, reportIfNoRoutes, resolveAuthentication, resolveRequestVisibility, Visibility, } from "@typespec/http";
4
- import { getExtensions, getExternalDocs, getOpenAPITypeName, getParameterKey, isDefaultResponse, isReadonlyProperty, resolveInfo, resolveOperationId, shouldInline, } from "@typespec/openapi";
4
+ import { getExtensions, getExternalDocs, getOpenAPITypeName, getParameterKey, isReadonlyProperty, resolveInfo, resolveOperationId, shouldInline, } from "@typespec/openapi";
5
5
  import { buildVersionProjections } from "@typespec/versioning";
6
6
  import { stringify } from "yaml";
7
7
  import { getRef } from "./decorators.js";
8
+ import { applyEncoding } from "./encoding.js";
9
+ import { getExampleOrExamples, resolveOperationExamples } from "./examples.js";
8
10
  import { createDiagnostic } from "./lib.js";
9
11
  import { getDefaultValue, isBytesKeptRaw, OpenAPI3SchemaEmitter } from "./schema-emitter.js";
10
- import { deepEquals, isDefined } from "./util.js";
12
+ import { getOpenAPI3StatusCodes } from "./status-codes.js";
13
+ import { deepEquals, isSharedHttpOperation } from "./util.js";
11
14
  import { resolveVisibilityUsage } from "./visibility-usage.js";
12
15
  const defaultFileType = "yaml";
13
16
  const defaultOptions = {
@@ -76,10 +79,10 @@ function createOAPIEmitter(context, options) {
76
79
  let root;
77
80
  let diagnostics;
78
81
  let currentService;
82
+ let serviceAuth;
79
83
  // Get the service namespace string for use in name shortening
80
84
  let serviceNamespaceName;
81
85
  let currentPath;
82
- let currentEndpoint;
83
86
  let metadataInfo;
84
87
  let visibilityUsage;
85
88
  // Map model properties that represent shared parameters to their parameter
@@ -175,6 +178,7 @@ function createOAPIEmitter(context, options) {
175
178
  if (servers) {
176
179
  root.servers = resolveServers(servers);
177
180
  }
181
+ attachExtensions(program, service.type, root);
178
182
  serviceNamespaceName = getNamespaceFullName(service.type);
179
183
  currentPath = root.paths;
180
184
  params = new Map();
@@ -315,15 +319,7 @@ function createOAPIEmitter(context, options) {
315
319
  /**
316
320
  * Validates that common responses are consistent and returns the minimal set that describes the differences.
317
321
  */
318
- function validateCommonResponses(statusCode, ops) {
319
- const statusCodeResponses = [];
320
- for (const op of ops) {
321
- for (const response of op.responses) {
322
- if (getOpenAPI3StatusCodes(response.statusCodes, response.type).includes(statusCode)) {
323
- statusCodeResponses.push(response);
324
- }
325
- }
326
- }
322
+ function deduplicateCommonResponses(statusCodeResponses) {
327
323
  const ref = statusCodeResponses[0];
328
324
  const sameTypeKind = statusCodeResponses.every((r) => r.type.kind === ref.type.kind);
329
325
  const sameTypeValue = statusCodeResponses.every((r) => r.type === ref.type);
@@ -335,156 +331,61 @@ function createOAPIEmitter(context, options) {
335
331
  return statusCodeResponses;
336
332
  }
337
333
  }
338
- /**
339
- * Validates that common bodies are consistent and returns the minimal set that describes the differences.
340
- */
341
- function validateCommonBodies(ops) {
342
- const allBodies = ops.map((op) => op.parameters.body);
343
- return [...new Set(allBodies)];
344
- }
345
334
  /**
346
335
  * Validates that common parameters are consistent and returns the minimal set that describes the differences.
347
336
  */
348
- function validateCommonParameters(ops, name, totalOps) {
337
+ function resolveSharedRouteParameters(ops) {
349
338
  const finalParams = [];
350
- const commonParams = [];
339
+ const parameters = new Map();
351
340
  for (const op of ops) {
352
- const param = op.parameters.parameters.find((p) => p.name === name);
353
- if (param) {
354
- commonParams.push(param);
341
+ for (const parameter of op.parameters.parameters) {
342
+ const existing = parameters.get(parameter.name);
343
+ if (existing) {
344
+ existing.push(parameter);
345
+ }
346
+ else {
347
+ parameters.set(parameter.name, [parameter]);
348
+ }
355
349
  }
356
350
  }
357
- const reference = commonParams[0];
358
- if (!reference) {
351
+ if (parameters.size === 0) {
359
352
  return [];
360
353
  }
361
- const inAllOps = ops.length === totalOps;
362
- const sameLocations = commonParams.every((p) => p.type === reference.type);
363
- const sameOptionality = commonParams.every((p) => p.param.optional === reference.param.optional);
364
- const sameTypeKind = commonParams.every((p) => p.param.type.kind === reference.param.type.kind);
365
- const sameTypeValue = commonParams.every((p) => p.param.type === reference.param.type);
366
- if (inAllOps && sameLocations && sameOptionality && sameTypeKind && sameTypeValue) {
367
- // param is consistent and in all shared operations. Only need one copy.
368
- finalParams.push(reference);
369
- }
370
- else if (!inAllOps && sameLocations && sameOptionality && sameTypeKind && sameTypeValue) {
371
- // param is consistent when used, but does not appear in all shared operations. Only need one copy, but it must be optional.
372
- reference.param.optional = true;
373
- finalParams.push(reference);
374
- }
375
- else if (inAllOps && !(sameLocations && sameOptionality && sameTypeKind)) {
376
- // param is in all shared operations, but is not consistent. Need multiple copies, which must be optional.
377
- // exception allowed when the params only differ by their value (e.g. string enum values)
378
- commonParams.forEach((p) => {
379
- p.param.optional = true;
380
- });
381
- finalParams.push(...commonParams);
382
- }
383
- else {
384
- finalParams.push(...commonParams);
385
- }
386
- return finalParams;
387
- }
388
- function getOpenAPI3StatusCodes(statusCodes, response) {
389
- if (isDefaultResponse(program, response) || statusCodes === "*") {
390
- return ["default"];
391
- }
392
- else if (typeof statusCodes === "number") {
393
- return [String(statusCodes)];
394
- }
395
- else {
396
- return rangeToOpenAPI(statusCodes, response);
397
- }
398
- }
399
- function rangeToOpenAPI(range, diagnosticTarget) {
400
- const reportInvalid = () => diagnostics.add(createDiagnostic({
401
- code: "unsupported-status-code-range",
402
- format: { start: String(range.start), end: String(range.end) },
403
- target: diagnosticTarget,
404
- }));
405
- const codes = [];
406
- let start = range.start;
407
- let end = range.end;
408
- if (range.start < 100) {
409
- reportInvalid();
410
- start = 100;
411
- codes.push("default");
412
- }
413
- else if (range.end > 599) {
414
- reportInvalid();
415
- codes.push("default");
416
- end = 599;
417
- }
418
- const groups = [1, 2, 3, 4, 5];
419
- for (const group of groups) {
420
- if (start > end) {
421
- break;
354
+ for (const sharedParams of parameters.values()) {
355
+ const reference = sharedParams[0];
356
+ const inAllOps = ops.length === sharedParams.length;
357
+ const sameLocations = sharedParams.every((p) => p.type === reference.type);
358
+ const sameOptionality = sharedParams.every((p) => p.param.optional === reference.param.optional);
359
+ const sameTypeKind = sharedParams.every((p) => p.param.type.kind === reference.param.type.kind);
360
+ const sameTypeValue = sharedParams.every((p) => p.param.type === reference.param.type);
361
+ if (inAllOps && sameLocations && sameOptionality && sameTypeKind && sameTypeValue) {
362
+ // param is consistent and in all shared operations. Only need one copy.
363
+ finalParams.push(reference);
422
364
  }
423
- const groupStart = group * 100;
424
- const groupEnd = groupStart + 99;
425
- if (start >= groupStart && start <= groupEnd) {
426
- codes.push(`${group}XX`);
427
- if (start !== groupStart || end < groupEnd) {
428
- reportInvalid();
429
- }
430
- start = groupStart + 100;
365
+ else if (!inAllOps && sameLocations && sameOptionality && sameTypeKind && sameTypeValue) {
366
+ // param is consistent when used, but does not appear in all shared operations. Only need one copy, but it must be optional.
367
+ reference.param.optional = true;
368
+ finalParams.push(reference);
431
369
  }
432
- }
433
- return codes;
434
- }
435
- function buildSharedOperations(operations) {
436
- const results = [];
437
- const paramMap = new Map();
438
- const responseMap = new Map();
439
- for (const op of operations) {
440
- // determine which parameters are shared by shared route operations
441
- for (const param of op.parameters.parameters) {
442
- if (paramMap.has(param.name)) {
443
- paramMap.get(param.name).push(op);
444
- }
445
- else {
446
- paramMap.set(param.name, [op]);
447
- }
370
+ else if (inAllOps && !(sameLocations && sameOptionality && sameTypeKind)) {
371
+ // param is in all shared operations, but is not consistent. Need multiple copies, which must be optional.
372
+ // exception allowed when the params only differ by their value (e.g. string enum values)
373
+ sharedParams.forEach((p) => {
374
+ p.param.optional = true;
375
+ });
376
+ finalParams.push(...sharedParams);
448
377
  }
449
- // determine which responses are shared by shared route operations
450
- for (const response of op.responses) {
451
- const statusCodes = getOpenAPI3StatusCodes(response.statusCodes, op.operation);
452
- for (const statusCode of statusCodes) {
453
- if (responseMap.has(statusCode)) {
454
- responseMap.get(statusCode).push(op);
455
- }
456
- else {
457
- responseMap.set(statusCode, [op]);
458
- }
459
- }
378
+ else {
379
+ finalParams.push(...sharedParams);
460
380
  }
461
381
  }
462
- const totalOps = operations.length;
463
- const shared = {
382
+ return finalParams;
383
+ }
384
+ function buildSharedOperation(operations) {
385
+ return {
464
386
  kind: "shared",
465
- operationId: operations.map((op) => resolveOperationId(program, op.operation)).join("_"),
466
- description: joinOps(operations, getDoc, " "),
467
- summary: joinOps(operations, getSummary, " "),
468
- path: operations[0].path,
469
- verb: operations[0].verb,
470
- operations: operations.map((op) => op.operation),
471
- parameters: {
472
- parameters: [],
473
- },
474
- bodies: undefined,
475
- authentication: operations[0].authentication,
476
- responses: new Map(),
387
+ operations: operations,
477
388
  };
478
- for (const [paramName, ops] of paramMap) {
479
- const commonParams = validateCommonParameters(ops, paramName, totalOps);
480
- shared.parameters.parameters.push(...commonParams);
481
- }
482
- shared.bodies = validateCommonBodies(operations);
483
- for (const [statusCode, ops] of responseMap) {
484
- shared.responses.set(statusCode, validateCommonResponses(statusCode, ops));
485
- }
486
- results.push(shared);
487
- return results;
488
389
  }
489
390
  /**
490
391
  * Groups HttpOperations together if they share the same route.
@@ -506,10 +407,7 @@ function createOAPIEmitter(context, options) {
506
407
  result.push(ops[0]);
507
408
  }
508
409
  else {
509
- const sharedOps = buildSharedOperations(ops);
510
- for (const op of sharedOps) {
511
- result.push(op);
512
- }
410
+ result.push(buildSharedOperation(ops));
513
411
  }
514
412
  }
515
413
  return result;
@@ -517,17 +415,15 @@ function createOAPIEmitter(context, options) {
517
415
  async function getOpenApiFromVersion(service, version) {
518
416
  try {
519
417
  const httpService = ignoreDiagnostics(getHttpService(program, service.type));
520
- const auth = resolveAuthentication(httpService);
418
+ const auth = (serviceAuth = resolveAuthentication(httpService));
521
419
  initializeEmitter(service, auth.schemes, auth.defaultAuth, version);
522
420
  reportIfNoRoutes(program, httpService.operations);
523
421
  for (const op of resolveOperations(httpService.operations)) {
524
- if (op.kind === "shared") {
525
- const opAuth = auth.operationsAuth.get(op.operations[0]);
526
- emitSharedOperation(op, opAuth);
527
- }
528
- else {
529
- const opAuth = auth.operationsAuth.get(op.operation);
530
- emitOperation(op, opAuth);
422
+ const result = getOperationOrSharedOperation(op);
423
+ if (result) {
424
+ const { operation, path, verb } = result;
425
+ currentPath[path] ??= {};
426
+ currentPath[path][verb] = operation;
531
427
  }
532
428
  }
533
429
  emitParameters();
@@ -565,26 +461,46 @@ function createOAPIEmitter(context, options) {
565
461
  return undefined;
566
462
  }
567
463
  }
568
- function emitSharedOperation(shared, authReference) {
569
- const { path: fullPath, verb: verb, operations: ops } = shared;
570
- if (!root.paths[fullPath]) {
571
- root.paths[fullPath] = {};
464
+ function computeSharedOperationId(shared) {
465
+ return shared.operations.map((op) => resolveOperationId(program, op.operation)).join("_");
466
+ }
467
+ function getOperationOrSharedOperation(operation) {
468
+ if (isSharedHttpOperation(operation)) {
469
+ return getSharedOperation(operation);
572
470
  }
573
- currentPath = root.paths[fullPath];
574
- if (!currentPath[verb]) {
575
- currentPath[verb] = {};
471
+ else {
472
+ return getOperation(operation);
576
473
  }
577
- currentEndpoint = currentPath[verb];
578
- for (const op of ops) {
579
- const opTags = getAllTags(program, op);
474
+ }
475
+ function getSharedOperation(shared) {
476
+ const operations = shared.operations;
477
+ const verb = operations[0].verb;
478
+ const path = operations[0].path;
479
+ const examples = resolveOperationExamples(program, shared);
480
+ const oai3Operation = {
481
+ operationId: computeSharedOperationId(shared),
482
+ parameters: [],
483
+ description: joinOps(operations, getDoc, " "),
484
+ summary: joinOps(operations, getSummary, " "),
485
+ responses: getSharedResponses(shared, examples),
486
+ };
487
+ for (const op of operations) {
488
+ applyExternalDocs(op.operation, oai3Operation);
489
+ attachExtensions(program, op.operation, oai3Operation);
490
+ if (isDeprecated(program, op.operation)) {
491
+ oai3Operation.deprecated = true;
492
+ }
493
+ }
494
+ for (const op of operations) {
495
+ const opTags = getAllTags(program, op.operation);
580
496
  if (opTags) {
581
- const currentTags = currentEndpoint.tags;
497
+ const currentTags = oai3Operation.tags;
582
498
  if (currentTags) {
583
499
  // combine tags but eliminate duplicates
584
- currentEndpoint.tags = [...new Set([...currentTags, ...opTags])];
500
+ oai3Operation.tags = [...new Set([...currentTags, ...opTags])];
585
501
  }
586
502
  else {
587
- currentEndpoint.tags = opTags;
503
+ oai3Operation.tags = opTags;
588
504
  }
589
505
  for (const tag of opTags) {
590
506
  // Add to root tags if not already there
@@ -592,105 +508,98 @@ function createOAPIEmitter(context, options) {
592
508
  }
593
509
  }
594
510
  }
595
- // Set up basic endpoint fields
596
- currentEndpoint.operationId = shared.operationId;
597
- for (const op of ops) {
598
- applyExternalDocs(op, currentEndpoint);
599
- }
600
- currentEndpoint.summary = shared.summary;
601
- currentEndpoint.description = shared.description;
602
- currentEndpoint.parameters = [];
603
- currentEndpoint.responses = {};
604
511
  // Error out if shared routes do not have consistent `@parameterVisibility`. We can
605
512
  // lift this restriction in the future if a use case develops.
606
- const visibilities = shared.operations.map((op) => {
607
- return resolveRequestVisibility(program, op, verb);
608
- });
513
+ const visibilities = operations.map((op) => resolveRequestVisibility(program, op.operation, verb));
609
514
  if (visibilities.some((v) => v !== visibilities[0])) {
610
515
  diagnostics.add(createDiagnostic({
611
516
  code: "inconsistent-shared-route-request-visibility",
612
- target: ops[0],
517
+ target: operations[0].operation,
613
518
  }));
614
519
  }
615
- const visibility = resolveRequestVisibility(program, shared.operations[0], verb);
616
- emitEndpointParameters(shared.parameters.parameters, visibility);
617
- if (shared.bodies) {
618
- if (shared.bodies.length === 1) {
619
- emitRequestBody(shared, shared.bodies[0], visibility);
620
- }
621
- else if (shared.bodies.length > 1) {
622
- emitMergedRequestBody(shared, shared.bodies, visibility);
623
- }
624
- }
625
- emitSharedResponses(shared, shared.responses);
626
- for (const op of ops) {
627
- if (isDeprecated(program, op)) {
628
- currentEndpoint.deprecated = true;
629
- }
630
- attachExtensions(program, op, currentEndpoint);
520
+ const visibility = visibilities[0];
521
+ oai3Operation.parameters = getEndpointParameters(resolveSharedRouteParameters(operations), visibility);
522
+ const bodies = [
523
+ ...new Set(operations.map((op) => op.parameters.body).filter((x) => x !== undefined)),
524
+ ];
525
+ if (bodies) {
526
+ oai3Operation.requestBody = getRequestBody(bodies, visibility, examples);
631
527
  }
528
+ const authReference = serviceAuth.operationsAuth.get(shared.operations[0].operation);
632
529
  if (authReference) {
633
- emitEndpointSecurity(authReference);
530
+ oai3Operation.security = getEndpointSecurity(authReference);
634
531
  }
532
+ return { operation: oai3Operation, verb, path };
635
533
  }
636
- function emitOperation(operation, authReference) {
534
+ function getOperation(operation) {
637
535
  const { path: fullPath, operation: op, verb, parameters } = operation;
638
536
  // If path contains a query string, issue msg and don't emit this endpoint
639
537
  if (fullPath.indexOf("?") > 0) {
640
538
  diagnostics.add(createDiagnostic({ code: "path-query", target: op }));
641
- return;
642
- }
643
- if (!root.paths[fullPath]) {
644
- root.paths[fullPath] = {};
645
- }
646
- currentPath = root.paths[fullPath];
647
- if (!currentPath[verb]) {
648
- currentPath[verb] = {};
539
+ return undefined;
649
540
  }
650
- currentEndpoint = currentPath[verb];
541
+ const visibility = resolveRequestVisibility(program, operation.operation, verb);
542
+ const examples = resolveOperationExamples(program, operation);
543
+ const oai3Operation = {
544
+ operationId: resolveOperationId(program, operation.operation),
545
+ summary: getSummary(program, operation.operation),
546
+ description: getDoc(program, operation.operation),
547
+ parameters: getEndpointParameters(parameters.parameters, visibility),
548
+ responses: getResponses(operation, operation.responses, examples),
549
+ };
651
550
  const currentTags = getAllTags(program, op);
652
551
  if (currentTags) {
653
- currentEndpoint.tags = currentTags;
552
+ oai3Operation.tags = currentTags;
654
553
  for (const tag of currentTags) {
655
554
  // Add to root tags if not already there
656
555
  tags.add(tag);
657
556
  }
658
557
  }
659
- currentEndpoint.operationId = resolveOperationId(program, operation.operation);
660
- applyExternalDocs(op, currentEndpoint);
558
+ applyExternalDocs(op, oai3Operation);
661
559
  // Set up basic endpoint fields
662
- currentEndpoint.summary = getSummary(program, operation.operation);
663
- currentEndpoint.description = getDoc(program, operation.operation);
664
- currentEndpoint.parameters = [];
665
- currentEndpoint.responses = {};
666
- const visibility = resolveRequestVisibility(program, operation.operation, verb);
667
- emitEndpointParameters(parameters.parameters, visibility);
668
- emitRequestBody(operation, parameters.body, visibility);
669
- emitResponses(operation, operation.responses);
560
+ if (parameters.body) {
561
+ oai3Operation.requestBody = getRequestBody(parameters.body && [parameters.body], visibility, examples);
562
+ }
563
+ const authReference = serviceAuth.operationsAuth.get(operation.operation);
670
564
  if (authReference) {
671
- emitEndpointSecurity(authReference);
565
+ oai3Operation.security = getEndpointSecurity(authReference);
672
566
  }
673
567
  if (isDeprecated(program, op)) {
674
- currentEndpoint.deprecated = true;
568
+ oai3Operation.deprecated = true;
675
569
  }
676
- attachExtensions(program, op, currentEndpoint);
570
+ attachExtensions(program, op, oai3Operation);
571
+ return { operation: oai3Operation, path: fullPath, verb };
677
572
  }
678
- function emitSharedResponses(operation, responses) {
679
- for (const [statusCode, statusCodeResponses] of responses) {
680
- if (statusCodeResponses.length === 1) {
681
- emitResponseObject(operation, statusCode, statusCodeResponses[0]);
682
- }
683
- else {
684
- emitMergedResponseObject(operation, statusCode, statusCodeResponses);
573
+ function getSharedResponses(operation, examples) {
574
+ const responseMap = new Map();
575
+ for (const op of operation.operations) {
576
+ for (const response of op.responses) {
577
+ const statusCodes = diagnostics.pipe(getOpenAPI3StatusCodes(program, response.statusCodes, op.operation));
578
+ for (const statusCode of statusCodes) {
579
+ if (responseMap.has(statusCode)) {
580
+ responseMap.get(statusCode).push(response);
581
+ }
582
+ else {
583
+ responseMap.set(statusCode, [response]);
584
+ }
585
+ }
685
586
  }
686
587
  }
588
+ const result = {};
589
+ for (const [statusCode, statusCodeResponses] of responseMap) {
590
+ const dedupeResponses = deduplicateCommonResponses(statusCodeResponses);
591
+ result[statusCode] = getResponseForStatusCode(operation, statusCode, dedupeResponses, examples);
592
+ }
593
+ return result;
687
594
  }
688
- function emitResponses(operation, responses) {
595
+ function getResponses(operation, responses, examples) {
596
+ const result = {};
689
597
  for (const response of responses) {
690
- for (const statusCode of getOpenAPI3StatusCodes(response.statusCodes, response.type)) {
691
- emitResponseObject(operation, statusCode, response);
598
+ for (const statusCode of diagnostics.pipe(getOpenAPI3StatusCodes(program, response.statusCodes, response.type))) {
599
+ result[statusCode] = getResponseForStatusCode(operation, statusCode, [response], examples);
692
600
  }
693
601
  }
602
+ return result;
694
603
  }
695
604
  function isBinaryPayload(body, contentType) {
696
605
  return (body.kind === "Scalar" &&
@@ -698,33 +607,28 @@ function createOAPIEmitter(context, options) {
698
607
  contentType !== "application/json" &&
699
608
  contentType !== "text/plain");
700
609
  }
701
- function emitMergedResponseObject(operation, statusCode, responses) {
610
+ function getResponseForStatusCode(operation, statusCode, responses, examples) {
702
611
  const openApiResponse = {
703
- description: undefined,
704
- content: {},
612
+ description: "",
705
613
  };
706
614
  const schemaMap = new Map();
707
615
  for (const response of responses) {
616
+ const refUrl = getRef(program, response.type);
617
+ if (refUrl) {
618
+ return { $ref: refUrl };
619
+ }
708
620
  if (response.description && response.description !== openApiResponse.description) {
709
621
  openApiResponse.description = openApiResponse.description
710
622
  ? `${openApiResponse.description} ${response.description}`
711
623
  : response.description;
712
624
  }
713
625
  emitResponseHeaders(openApiResponse, response.responses, response.type);
714
- emitResponseContent(operation, openApiResponse, response.responses, schemaMap);
626
+ emitResponseContent(operation, openApiResponse, response.responses, statusCode, examples, schemaMap);
715
627
  if (!openApiResponse.description) {
716
628
  openApiResponse.description = getResponseDescriptionForStatusCode(statusCode);
717
629
  }
718
- currentEndpoint.responses[statusCode] = openApiResponse;
719
630
  }
720
- }
721
- function emitResponseObject(operation, statusCode, response) {
722
- const openApiResponse = currentEndpoint.responses[statusCode] ?? {
723
- description: response.description ?? getResponseDescriptionForStatusCode(statusCode),
724
- };
725
- emitResponseHeaders(openApiResponse, response.responses, response.type);
726
- emitResponseContent(operation, openApiResponse, response.responses);
727
- currentEndpoint.responses[statusCode] = openApiResponse;
631
+ return openApiResponse;
728
632
  }
729
633
  function emitResponseHeaders(obj, responses, target) {
730
634
  for (const data of responses) {
@@ -750,7 +654,7 @@ function createOAPIEmitter(context, options) {
750
654
  }
751
655
  }
752
656
  }
753
- function emitResponseContent(operation, obj, responses, schemaMap = undefined) {
657
+ function emitResponseContent(operation, obj, responses, statusCode, examples, schemaMap = undefined) {
754
658
  schemaMap ??= new Map();
755
659
  for (const data of responses) {
756
660
  if (data.body === undefined) {
@@ -758,7 +662,7 @@ function createOAPIEmitter(context, options) {
758
662
  }
759
663
  obj.content ??= {};
760
664
  for (const contentType of data.body.contentTypes) {
761
- const contents = getBodyContentEntry(operation, "response", data.body, Visibility.Read, contentType);
665
+ const contents = getBodyContentEntry(data.body, Visibility.Read, contentType, examples.responses[statusCode]?.[contentType]);
762
666
  if (schemaMap.has(contentType)) {
763
667
  schemaMap.get(contentType).push(contents);
764
668
  }
@@ -836,70 +740,22 @@ function createOAPIEmitter(context, options) {
836
740
  },
837
741
  });
838
742
  }
839
- function isSharedHttpOperation(operation) {
840
- return operation.kind === "shared";
841
- }
842
- function findOperationExamples(operation) {
843
- if (isSharedHttpOperation(operation)) {
844
- return operation.operations.flatMap((op) => getOpExamples(program, op).map((x) => [op, x]));
845
- }
846
- else {
847
- return getOpExamples(program, operation.operation).map((x) => [operation.operation, x]);
848
- }
849
- }
850
- function getExamplesForBodyContentEntry(operation, target) {
851
- const examples = findOperationExamples(operation);
852
- if (examples.length === 0) {
853
- return {};
854
- }
855
- const flattenedExamples = examples
856
- .map(([op, example]) => {
857
- const value = target === "request" ? example.parameters : example.returnType;
858
- const type = target === "request" ? op.parameters : op.returnType;
859
- return value
860
- ? [{ value, title: example.title, description: example.description }, type]
861
- : undefined;
862
- })
863
- .filter(isDefined);
864
- return getExampleOrExamples(flattenedExamples);
865
- }
866
- function getExampleOrExamples(examples) {
867
- if (examples.length === 0) {
868
- return {};
869
- }
870
- if (examples.length === 1 &&
871
- examples[0][0].title === undefined &&
872
- examples[0][0].description === undefined) {
873
- const [example, type] = examples[0];
874
- return { example: serializeValueAsJson(program, example.value, type) };
875
- }
876
- else {
877
- const exampleObj = {};
878
- for (const [index, [example, type]] of examples.entries()) {
879
- exampleObj[example.title ?? `example${index}`] = {
880
- summary: example.title,
881
- description: example.description,
882
- value: serializeValueAsJson(program, example.value, type),
883
- };
884
- }
885
- return { examples: exampleObj };
886
- }
887
- }
888
- function getBodyContentEntry(operation, target, body, visibility, contentType) {
743
+ function getBodyContentEntry(body, visibility, contentType, examples) {
889
744
  const isBinary = isBinaryPayload(body.type, contentType);
890
745
  if (isBinary) {
891
746
  return { schema: { type: "string", format: "binary" } };
892
747
  }
748
+ const oai3Examples = examples && getExampleOrExamples(program, examples);
893
749
  switch (body.bodyKind) {
894
750
  case "single":
895
751
  return {
896
752
  schema: getSchemaForSingleBody(body.type, visibility, body.isExplicit && body.containsMetadataAnnotations, contentType.startsWith("multipart/") ? contentType : undefined),
897
- ...getExamplesForBodyContentEntry(operation, target),
753
+ ...oai3Examples,
898
754
  };
899
755
  case "multipart":
900
756
  return {
901
757
  ...getBodyContentForMultipartBody(body, visibility, contentType),
902
- ...getExamplesForBodyContentEntry(operation, target),
758
+ ...oai3Examples,
903
759
  };
904
760
  }
905
761
  }
@@ -991,56 +847,57 @@ function createOAPIEmitter(context, options) {
991
847
  }
992
848
  return false;
993
849
  }
994
- function getParamPlaceholder(property) {
995
- let spreadParam = false;
996
- if (property.sourceProperty) {
997
- // chase our sources all the way back to the first place this property
998
- // was defined.
999
- spreadParam = true;
1000
- property = property.sourceProperty;
1001
- while (property.sourceProperty) {
1002
- property = property.sourceProperty;
1003
- }
1004
- }
1005
- const refUrl = getRef(program, property);
1006
- if (refUrl) {
1007
- return {
1008
- $ref: refUrl,
850
+ function getParameter(parameter, visibility) {
851
+ const param = {
852
+ name: parameter.name,
853
+ in: parameter.type,
854
+ ...getOpenAPIParameterBase(parameter.param, visibility),
855
+ };
856
+ const format = mapParameterFormat(parameter);
857
+ if (format === undefined) {
858
+ param.schema = {
859
+ type: "string",
1009
860
  };
1010
861
  }
1011
- if (params.has(property)) {
1012
- return params.get(property);
1013
- }
1014
- const placeholder = {};
1015
- // only parameters inherited by spreading from non-inlined type are shared in #/components/parameters
1016
- if (spreadParam && property.model && !shouldInline(program, property.model)) {
1017
- params.set(property, placeholder);
1018
- paramModels.add(property.model);
862
+ else {
863
+ Object.assign(param, format);
1019
864
  }
1020
- return placeholder;
865
+ return param;
1021
866
  }
1022
- function emitEndpointParameters(parameters, visibility) {
867
+ function getEndpointParameters(parameters, visibility) {
868
+ const result = [];
1023
869
  for (const httpOpParam of parameters) {
1024
870
  if (params.has(httpOpParam.param)) {
1025
- currentEndpoint.parameters.push(params.get(httpOpParam.param));
871
+ result.push(params.get(httpOpParam.param));
1026
872
  continue;
1027
873
  }
874
+ // eslint-disable-next-line deprecation/deprecation
1028
875
  if (httpOpParam.type === "header" && isContentTypeHeader(program, httpOpParam.param)) {
1029
876
  continue;
1030
877
  }
1031
- emitParameter(httpOpParam, visibility);
878
+ const param = getParameterOrRef(httpOpParam, visibility);
879
+ if (param) {
880
+ const existing = result.find((x) => !("$ref" in param) && !("$ref" in x) && x.name === param.name && x.in === param.in);
881
+ if (existing && !("$ref" in param) && !("$ref" in existing)) {
882
+ mergeOpenApiParameters(existing, param);
883
+ }
884
+ else {
885
+ result.push(param);
886
+ }
887
+ }
1032
888
  }
889
+ return result;
1033
890
  }
1034
- function emitMergedRequestBody(operation, bodies, visibility) {
1035
- if (bodies === undefined) {
1036
- return;
891
+ function getRequestBody(bodies, visibility, examples) {
892
+ if (bodies === undefined || bodies.every((x) => isVoidType(x.type))) {
893
+ return undefined;
1037
894
  }
1038
895
  const requestBody = {
1039
- description: undefined,
896
+ required: bodies.every((body) => (body.property ? !body.property.optional : true)),
1040
897
  content: {},
1041
898
  };
1042
899
  const schemaMap = new Map();
1043
- for (const body of bodies) {
900
+ for (const body of bodies.filter((x) => !isVoidType(x.type))) {
1044
901
  const desc = body.property ? getDoc(program, body.property) : undefined;
1045
902
  if (desc) {
1046
903
  requestBody.description = requestBody.description
@@ -1049,67 +906,67 @@ function createOAPIEmitter(context, options) {
1049
906
  }
1050
907
  const contentTypes = body.contentTypes.length > 0 ? body.contentTypes : ["application/json"];
1051
908
  for (const contentType of contentTypes) {
1052
- const { schema: bodySchema } = getBodyContentEntry(operation, "request", body, visibility, contentType);
1053
- if (schemaMap.has(contentType)) {
1054
- schemaMap.get(contentType).push(bodySchema);
909
+ const existing = schemaMap.get(contentType);
910
+ const entry = getBodyContentEntry(body, visibility, contentType, examples.requestBody[contentType]);
911
+ if (existing) {
912
+ existing.push(entry);
1055
913
  }
1056
914
  else {
1057
- schemaMap.set(contentType, [bodySchema]);
915
+ schemaMap.set(contentType, [entry]);
1058
916
  }
1059
917
  }
1060
918
  }
1061
- const content = {};
1062
919
  for (const [contentType, schemaArray] of schemaMap) {
1063
920
  if (schemaArray.length === 1) {
1064
- content[contentType] = { schema: schemaArray[0] };
921
+ requestBody.content[contentType] = schemaArray[0];
1065
922
  }
1066
923
  else {
1067
- content[contentType] = {
1068
- schema: { anyOf: schemaArray },
924
+ requestBody.content[contentType] = {
925
+ schema: { anyOf: schemaArray.map((x) => x.schema).filter((x) => x !== undefined) },
926
+ encoding: schemaArray.find((x) => x.encoding)?.encoding,
1069
927
  };
1070
928
  }
1071
929
  }
1072
- requestBody.content = content;
1073
- currentEndpoint.requestBody = requestBody;
930
+ return requestBody;
1074
931
  }
1075
- function emitRequestBody(operation, body, visibility) {
1076
- if (body === undefined || isVoidType(body.type)) {
1077
- return;
932
+ function getParameterOrRef(parameter, visibility) {
933
+ if (isNeverType(parameter.param.type)) {
934
+ return undefined;
1078
935
  }
1079
- const requestBody = {
1080
- description: body.property ? getDoc(program, body.property) : undefined,
1081
- required: body.property ? !body.property.optional : true,
1082
- content: {},
1083
- };
1084
- const contentTypes = body.contentTypes.length > 0 ? body.contentTypes : ["application/json"];
1085
- for (const contentType of contentTypes) {
1086
- requestBody.content[contentType] = getBodyContentEntry(operation, "request", body, visibility, contentType);
936
+ let spreadParam = false;
937
+ let property = parameter.param;
938
+ if (property.sourceProperty) {
939
+ // chase our sources all the way back to the first place this property
940
+ // was defined.
941
+ spreadParam = true;
942
+ property = property.sourceProperty;
943
+ while (property.sourceProperty) {
944
+ property = property.sourceProperty;
945
+ }
1087
946
  }
1088
- currentEndpoint.requestBody = requestBody;
1089
- }
1090
- function emitParameter(parameter, visibility) {
1091
- if (isNeverType(parameter.param.type)) {
1092
- return;
947
+ const refUrl = getRef(program, property);
948
+ if (refUrl) {
949
+ return {
950
+ $ref: refUrl,
951
+ };
1093
952
  }
1094
- const existing = currentEndpoint.parameters.find((p) => p.name === parameter.name && p.in === parameter.type);
1095
- if (existing) {
1096
- populateParameter(existing, parameter, visibility);
953
+ if (params.has(property)) {
954
+ return params.get(property);
1097
955
  }
1098
- else {
1099
- const ph = getParamPlaceholder(parameter.param);
1100
- currentEndpoint.parameters.push(ph);
1101
- // If the parameter already has a $ref, don't bother populating it
1102
- if (!("$ref" in ph)) {
1103
- populateParameter(ph, parameter, visibility);
1104
- }
956
+ const param = getParameter(parameter, visibility);
957
+ // only parameters inherited by spreading from non-inlined type are shared in #/components/parameters
958
+ if (spreadParam && property.model && !shouldInline(program, property.model)) {
959
+ params.set(property, param);
960
+ paramModels.add(property.model);
1105
961
  }
962
+ return param;
1106
963
  }
1107
964
  function getOpenAPIParameterBase(param, visibility) {
1108
965
  const typeSchema = getSchemaForType(param.type, visibility);
1109
966
  if (!typeSchema) {
1110
967
  return undefined;
1111
968
  }
1112
- const schema = applyEncoding(param, applyIntrinsicDecorators(param, typeSchema));
969
+ const schema = applyEncoding(program, param, applyIntrinsicDecorators(param, typeSchema), options);
1113
970
  if (param.defaultValue) {
1114
971
  schema.default = getDefaultValue(program, param.defaultValue);
1115
972
  }
@@ -1123,35 +980,18 @@ function createOAPIEmitter(context, options) {
1123
980
  attachExtensions(program, param, oaiParam);
1124
981
  return oaiParam;
1125
982
  }
1126
- function mergeOpenApiParameters(param, base) {
1127
- if (param.schema) {
1128
- const schema = param.schema;
1129
- if (schema.enum && base.schema.enum) {
1130
- schema.enum = [...new Set([...schema.enum, ...base.schema.enum])];
983
+ function mergeOpenApiParameters(target, apply) {
984
+ if (target.schema) {
985
+ const schema = target.schema;
986
+ if (schema.enum && apply.schema.enum) {
987
+ schema.enum = [...new Set([...schema.enum, ...apply.schema.enum])];
1131
988
  }
1132
- param.schema = schema;
989
+ target.schema = schema;
1133
990
  }
1134
991
  else {
1135
- Object.assign(param, base);
1136
- }
1137
- return param;
1138
- }
1139
- function populateParameter(ph, parameter, visibility) {
1140
- ph.name = parameter.name;
1141
- ph.in = parameter.type;
1142
- const paramBase = getOpenAPIParameterBase(parameter.param, visibility);
1143
- if (paramBase) {
1144
- ph = mergeOpenApiParameters(ph, paramBase);
1145
- }
1146
- const format = mapParameterFormat(parameter);
1147
- if (format === undefined) {
1148
- ph.schema = {
1149
- type: "string",
1150
- };
1151
- }
1152
- else {
1153
- Object.assign(ph, format);
992
+ Object.assign(target, apply);
1154
993
  }
994
+ return target;
1155
995
  }
1156
996
  function mapParameterFormat(parameter) {
1157
997
  switch (parameter.type) {
@@ -1332,44 +1172,6 @@ function createOAPIEmitter(context, options) {
1332
1172
  attachExtensions(program, typespecType, newTarget);
1333
1173
  return newTarget;
1334
1174
  }
1335
- function applyEncoding(typespecType, target) {
1336
- const encodeData = getEncode(program, typespecType);
1337
- if (encodeData) {
1338
- const newTarget = { ...target };
1339
- const newType = callSchemaEmitter(encodeData.type, Visibility.Read, false, "application/json");
1340
- newTarget.type = newType.type;
1341
- // If the target already has a format it takes priority. (e.g. int32)
1342
- newTarget.format = mergeFormatAndEncoding(newTarget.format, encodeData.encoding, newType.format);
1343
- return newTarget;
1344
- }
1345
- return target;
1346
- }
1347
- function mergeFormatAndEncoding(format, encoding, encodeAsFormat) {
1348
- switch (format) {
1349
- case undefined:
1350
- return encodeAsFormat ?? encoding;
1351
- case "date-time":
1352
- switch (encoding) {
1353
- case "rfc3339":
1354
- return "date-time";
1355
- case "unixTimestamp":
1356
- return "unixtime";
1357
- case "rfc7231":
1358
- return "http-date";
1359
- default:
1360
- return encoding;
1361
- }
1362
- case "duration":
1363
- switch (encoding) {
1364
- case "ISO8601":
1365
- return "duration";
1366
- default:
1367
- return encodeAsFormat ?? encoding;
1368
- }
1369
- default:
1370
- return encodeAsFormat ?? encoding;
1371
- }
1372
- }
1373
1175
  function applyExternalDocs(typespecType, target) {
1374
1176
  const externalDocs = getExternalDocs(program, typespecType);
1375
1177
  if (externalDocs) {
@@ -1405,14 +1207,15 @@ function createOAPIEmitter(context, options) {
1405
1207
  });
1406
1208
  return security;
1407
1209
  }
1408
- function emitEndpointSecurity(authReference) {
1210
+ function getEndpointSecurity(authReference) {
1409
1211
  const security = getOpenAPISecurity(authReference);
1410
1212
  if (deepEquals(security, root.security)) {
1411
- return;
1213
+ return undefined;
1412
1214
  }
1413
1215
  if (security.length > 0) {
1414
- currentEndpoint.security = security;
1216
+ return security;
1415
1217
  }
1218
+ return undefined;
1416
1219
  }
1417
1220
  function getOpenAPI3Scheme(auth) {
1418
1221
  const scheme = getOpenAPI3SchemeInternal(auth);