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