@typespec/openapi3
Version:
TypeSpec library for emitting OpenAPI 3.0 and OpenAPI 3.1 from the TypeSpec REST protocol binding and converting OpenAPI3 to TypeSpec
1,159 lines • 62.1 kB
JavaScript
import { compilerAssert, createDiagnosticCollector, emitFile, getAllTags, getAnyExtensionFromPath, getDoc, getFormat, getMaxItems, getMaxLength, getMaxValueExclusive, getMinItems, getMinLength, getMinValueExclusive, getNamespaceFullName, getPattern, getService, getSummary, ignoreDiagnostics, interpolatePath, isDeprecated, isGlobalNamespace, isNeverType, isSecret, isVoidType, listServices, navigateTypesInNamespace, resolvePath, } from "@typespec/compiler";
import { unsafe_mutateSubgraphWithNamespace, } from "@typespec/compiler/experimental";
import { $ } from "@typespec/compiler/typekit";
import { createMetadataInfo, getHttpService, getServers, getStatusCodeDescription, isOrExtendsHttpFile, isOverloadSameEndpoint, reportIfNoRoutes, resolveAuthentication, resolveRequestVisibility, Visibility, } from "@typespec/http";
import { getStreamMetadata } from "@typespec/http/experimental";
import { getExtensions, getExternalDocs, getOpenAPITypeName, getParameterKey, getTagsMetadata, isReadonlyProperty, shouldInline, } from "@typespec/openapi";
import { stringify } from "yaml";
import { getRef } from "./decorators.js";
import { getExampleOrExamples, resolveOperationExamples } from "./examples.js";
import { resolveJsonSchemaModule } from "./json-schema.js";
import { createDiagnostic, reportDiagnostic, } from "./lib.js";
import { getOpenApiSpecProps } from "./openapi-spec-mappings.js";
import { OperationIdResolver } from "./operation-id-resolver/operation-id-resolver.js";
import { getParameterStyle } from "./parameters.js";
import { getMaxValueAsJson, getMinValueAsJson } from "./range.js";
import { resolveSSEModule } from "./sse-module.js";
import { getOpenAPI3StatusCodes } from "./status-codes.js";
import { deepEquals, ensureValidComponentFixedFieldKey, getDefaultValue, isBytesKeptRaw, isSharedHttpOperation, } from "./util.js";
import { resolveVersioningModule } from "./versioning-module.js";
import { resolveVisibilityUsage } from "./visibility-usage.js";
import { resolveXmlModule } from "./xml-module.js";
const defaultFileType = "yaml";
const defaultOptions = {
"new-line": "lf",
"omit-unreachable-types": false,
"include-x-typespec-name": "never",
"safeint-strategy": "int64",
"seal-object-schemas": false,
};
export async function $onEmit(context) {
const options = resolveOptions(context);
for (const specVersion of options.openapiVersions) {
const emitter = createOAPIEmitter(context, options, specVersion);
await emitter.emitOpenAPI();
}
}
/**
* Get the OpenAPI 3 document records from the given program. The documents are
* returned as a JS object.
*
* @param program The program to emit to OpenAPI 3
* @param options OpenAPI 3 emit options
* @returns An array of OpenAPI 3 document records.
*/
export async function getOpenAPI3(program, options = {}) {
const context = {
program,
// this value doesn't matter for getting the OpenAPI3 objects
emitterOutputDir: "tsp-output",
options: options,
};
const resolvedOptions = resolveOptions(context);
const serviceRecords = [];
for (const specVersion of resolvedOptions.openapiVersions) {
const emitter = createOAPIEmitter(context, resolvedOptions, specVersion);
serviceRecords.push(...(await emitter.getOpenAPI()));
}
return serviceRecords;
}
function findFileTypeFromFilename(filename) {
if (filename === undefined) {
return defaultFileType;
}
switch (getAnyExtensionFromPath(filename)) {
case ".yaml":
case ".yml":
return "yaml";
case ".json":
return "json";
default:
return defaultFileType;
}
}
export function resolveOptions(context) {
const resolvedOptions = { ...defaultOptions, ...context.options };
const fileType = resolvedOptions["file-type"] ?? findFileTypeFromFilename(resolvedOptions["output-file"]);
const outputFile = resolvedOptions["output-file"] ?? `openapi.{service-name-if-multiple}.{version}.${fileType}`;
const openapiVersions = resolvedOptions["openapi-versions"] ?? ["3.0.0"];
const specDir = openapiVersions.length > 1 ? "{openapi-version}" : "";
return {
fileType,
newLine: resolvedOptions["new-line"],
omitUnreachableTypes: resolvedOptions["omit-unreachable-types"],
includeXTypeSpecName: resolvedOptions["include-x-typespec-name"],
safeintStrategy: resolvedOptions["safeint-strategy"],
outputFile: resolvePath(context.emitterOutputDir, specDir, outputFile),
openapiVersions,
sealObjectSchemas: resolvedOptions["seal-object-schemas"],
parameterExamplesStrategy: resolvedOptions["experimental-parameter-examples"],
operationIdStrategy: resolveOperationIdStrategy(resolvedOptions["operation-id-strategy"]),
};
}
const defaultOperationIdStrategy = { kind: "parent-container", separator: "_" };
function resolveOperationIdStrategy(strategy) {
if (strategy === undefined) {
return defaultOperationIdStrategy;
}
if (typeof strategy === "string") {
return { kind: strategy, separator: resolveOperationIdDefaultStrategySeparator(strategy) };
}
return {
kind: strategy.kind,
separator: strategy.separator ?? resolveOperationIdDefaultStrategySeparator(strategy.kind),
};
}
function resolveOperationIdDefaultStrategySeparator(strategy) {
switch (strategy) {
case "parent-container":
return "_";
case "fqn":
return ".";
case "explicit-only":
return "";
}
}
function createOAPIEmitter(context, options, specVersion = "3.0.0") {
const { applyEncoding, createRootDoc, createSchemaEmitter, getRawBinarySchema, isRawBinarySchema, } = getOpenApiSpecProps(specVersion);
const program = context.program;
let schemaEmitter;
let operationIdResolver;
let root;
let diagnostics;
let currentService;
let serviceAuth;
// Get the service namespace string for use in name shortening
let serviceNamespaceName;
let currentPath;
let metadataInfo;
let visibilityUsage;
let sseModule;
// Map model properties that represent shared parameters to their parameter
// definition that will go in #/components/parameters. Inlined parameters do not go in
// this map.
let params;
// Keep track of models that have had properties spread into parameters. We won't
// consider these unreferenced when emitting unreferenced types.
let paramModels;
// De-dupe the per-endpoint tags that will be added into the #/tags
let tags;
// The per-endpoint tags that will be added into the #/tags
const tagsMetadata = {};
const typeNameOptions = {
// shorten type names by removing TypeSpec and service namespace
namespaceFilter(ns) {
const name = getNamespaceFullName(ns);
return name !== serviceNamespaceName;
},
};
return { emitOpenAPI, getOpenAPI };
/**
* Check if an HTTP operation or its container (interface/namespace) is deprecated
*/
function isOperationDeprecated(httpOp) {
if (isDeprecated(program, httpOp.operation)) {
return true;
}
if (isDeprecated(program, httpOp.container)) {
return true;
}
// Check parent namespaces recursively
let current = httpOp.container.namespace;
while (current && current.name !== "") {
if (isDeprecated(program, current)) {
return true;
}
current = current.namespace;
}
return false;
}
async function emitOpenAPI() {
const services = await getOpenAPI();
// first, emit diagnostics
for (const serviceRecord of services) {
if (serviceRecord.versioned) {
for (const documentRecord of serviceRecord.versions) {
program.reportDiagnostics(documentRecord.diagnostics);
}
}
else {
program.reportDiagnostics(serviceRecord.diagnostics);
}
}
if (program.compilerOptions.dryRun || program.hasError()) {
return;
}
const multipleService = services.length > 1;
for (const serviceRecord of services) {
if (serviceRecord.versioned) {
for (const documentRecord of serviceRecord.versions) {
await emitFile(program, {
path: resolveOutputFile(serviceRecord.service, multipleService, documentRecord.version),
content: serializeDocument(documentRecord.document, options.fileType),
newLine: options.newLine,
});
}
}
else {
await emitFile(program, {
path: resolveOutputFile(serviceRecord.service, multipleService),
content: serializeDocument(serviceRecord.document, options.fileType),
newLine: options.newLine,
});
}
}
}
function initializeEmitter(service, allHttpAuthentications, defaultAuth, optionalDependencies, version) {
diagnostics = createDiagnosticCollector();
currentService = service;
sseModule = optionalDependencies.sseModule;
metadataInfo = createMetadataInfo(program, {
canonicalVisibility: Visibility.Read,
canShareProperty: (p) => isReadonlyProperty(program, p),
});
visibilityUsage = resolveVisibilityUsage(program, metadataInfo, service.type, options.omitUnreachableTypes);
schemaEmitter = createSchemaEmitter({
program,
context,
metadataInfo,
visibilityUsage,
options,
optionalDependencies,
});
operationIdResolver = new OperationIdResolver(program, {
strategy: options.operationIdStrategy.kind,
separator: options.operationIdStrategy.separator,
});
const securitySchemes = getOpenAPISecuritySchemes(allHttpAuthentications);
const security = getOpenAPISecurity(defaultAuth);
root = createRootDoc(program, service.type, version);
if (security.length > 0) {
root.security = security;
}
root.components.securitySchemes = securitySchemes;
const servers = getServers(program, service.type);
if (servers) {
root.servers = resolveServers(servers);
}
attachExtensions(program, service.type, root);
serviceNamespaceName = getNamespaceFullName(service.type);
currentPath = root.paths;
params = new Map();
paramModels = new Set();
tags = new Set();
// Get Tags Metadata
const metadata = getTagsMetadata(program, service.type);
if (metadata) {
for (const [name, tag] of Object.entries(metadata)) {
const tagData = { name: name, ...tag };
tagsMetadata[name] = tagData;
}
}
}
function isValidServerVariableType(program, type) {
const tk = $(program);
switch (type.kind) {
case "String":
case "Union":
case "Scalar":
return tk.type.isAssignableTo(type, tk.builtin.string, type);
case "Enum":
for (const member of type.members.values()) {
if (member.value && typeof member.value !== "string") {
return false;
}
}
return true;
default:
return false;
}
}
function validateValidServerVariable(program, prop) {
const isValid = isValidServerVariableType(program, prop.type);
if (!isValid) {
diagnostics.add(createDiagnostic({
code: "invalid-server-variable",
format: { propName: prop.name },
target: prop,
}));
}
return isValid;
}
function resolveServers(servers) {
return servers.map((server) => {
const variables = {};
for (const [name, prop] of server.parameters) {
if (!validateValidServerVariable(program, prop)) {
continue;
}
const variable = {
default: prop.defaultValue ? getDefaultValue(program, prop.defaultValue, prop) : "",
description: getDoc(program, prop),
};
if (prop.type.kind === "Enum") {
variable.enum = getSchemaValue(prop.type, Visibility.Read, "application/json")
.enum;
}
else if (prop.type.kind === "Union") {
variable.enum = getSchemaValue(prop.type, Visibility.Read, "application/json")
.enum;
}
else if (prop.type.kind === "String") {
variable.enum = [prop.type.value];
}
attachExtensions(program, prop, variable);
variables[name] = variable;
}
return {
url: server.url,
description: server.description,
variables,
};
});
}
async function getOpenAPI() {
const versioningModule = await resolveVersioningModule();
const serviceRecords = [];
const services = listServices(program);
if (services.length === 0) {
services.push({ type: program.getGlobalNamespaceType() });
}
for (const service of services) {
const versions = versioningModule?.getVersioningMutators(program, service.type);
if (versions === undefined) {
const document = await getOpenApiFromVersion(service);
if (document === undefined) {
// an error occurred producing this document, so don't return it
return serviceRecords;
}
serviceRecords.push({
service,
versioned: false,
document: document[0],
diagnostics: document[1],
});
}
else if (versions.kind === "transient") {
const document = await getVersionSnapshotDocument(service, versions.mutator);
if (document === undefined) {
// an error occurred producing this document, so don't return it
return serviceRecords;
}
serviceRecords.push({
service,
versioned: false,
document: document[0],
diagnostics: document[1],
});
}
else {
// versioned spec
const serviceRecord = {
service,
versioned: true,
versions: [],
};
serviceRecords.push(serviceRecord);
for (const snapshot of versions.snapshots) {
const document = await getVersionSnapshotDocument(service, snapshot.mutator, snapshot.version?.value);
if (document === undefined) {
// an error occurred producing this document
continue;
}
serviceRecord.versions.push({
service,
version: snapshot.version.value,
document: document[0],
diagnostics: document[1],
});
}
}
}
return serviceRecords;
}
async function getVersionSnapshotDocument(service, mutator, version) {
const subgraph = unsafe_mutateSubgraphWithNamespace(program, [mutator], service.type);
compilerAssert(subgraph.type.kind === "Namespace", "Should not have mutated to another type");
const document = await getOpenApiFromVersion(getService(program, subgraph.type), version);
return document;
}
function resolveOutputFile(service, multipleService, version) {
return interpolatePath(options.outputFile, {
"openapi-version": specVersion,
"service-name-if-multiple": multipleService ? getNamespaceFullName(service.type) : undefined,
"service-name": getNamespaceFullName(service.type),
version,
});
}
/**
* Validates that common responses are consistent and returns the minimal set that describes the differences.
*/
function deduplicateCommonResponses(statusCodeResponses) {
const ref = statusCodeResponses[0];
const sameTypeKind = statusCodeResponses.every((r) => r.type.kind === ref.type.kind);
const sameTypeValue = statusCodeResponses.every((r) => r.type === ref.type);
if (sameTypeKind && sameTypeValue) {
// response is consistent and in all shared operations. Only need one copy.
return [ref];
}
else {
return statusCodeResponses;
}
}
/**
* Validates that common parameters are consistent and returns the minimal set that describes the differences.
*/
function resolveSharedRouteParameters(ops) {
const finalProps = [];
const properties = new Map();
for (const op of ops) {
for (const property of op.parameters.properties) {
if (!isHttpParameterProperty(property)) {
continue;
}
const existing = properties.get(property.options.name);
if (existing) {
existing.push(property);
}
else {
properties.set(property.options.name, [property]);
}
}
}
if (properties.size === 0) {
return [];
}
for (const sharedParams of properties.values()) {
const reference = sharedParams[0];
const inAllOps = ops.length === sharedParams.length;
const sameLocations = sharedParams.every((p) => p.kind === reference.kind);
const sameOptionality = sharedParams.every((p) => p.property.optional === reference.property.optional);
const sameTypeKind = sharedParams.every((p) => p.property.type.kind === reference.property.type.kind);
const sameTypeValue = sharedParams.every((p) => p.property.type === reference.property.type);
if (inAllOps && sameLocations && sameOptionality && sameTypeKind && sameTypeValue) {
// param is consistent and in all shared operations. Only need one copy.
finalProps.push(reference);
}
else if (!inAllOps && sameLocations && sameOptionality && sameTypeKind && sameTypeValue) {
// param is consistent when used, but does not appear in all shared operations. Only need one copy, but it must be optional.
reference.property.optional = true;
finalProps.push(reference);
}
else if (inAllOps && !(sameLocations && sameOptionality && sameTypeKind)) {
// param is in all shared operations, but is not consistent. Need multiple copies, which must be optional.
// exception allowed when the params only differ by their value (e.g. string enum values)
sharedParams.forEach((p) => {
p.property.optional = true;
});
finalProps.push(...sharedParams);
}
else {
finalProps.push(...sharedParams);
}
}
return finalProps;
}
function buildSharedOperation(operations) {
return {
kind: "shared",
operations: operations,
};
}
/**
* Groups HttpOperations together if they share the same route.
*/
function resolveOperations(operations) {
const result = [];
const pathMap = new Map();
operations.forEach((op) => {
// we don't emit overloads anyhow so emit them from grouping
if (op.overloading !== undefined && isOverloadSameEndpoint(op)) {
return;
}
const opKey = `${op.verb}|${op.path}`;
pathMap.has(opKey) ? pathMap.get(opKey).push(op) : pathMap.set(opKey, [op]);
});
// now push either the singular HttpOperations or the constructed SharedHttpOperations
for (const [_, ops] of pathMap) {
if (ops.length === 1) {
result.push(ops[0]);
}
else {
result.push(buildSharedOperation(ops));
}
}
return result;
}
async function getOpenApiFromVersion(service, version) {
try {
const httpService = ignoreDiagnostics(getHttpService(program, service.type));
const auth = (serviceAuth = resolveAuthentication(httpService));
const xmlModule = await resolveXmlModule();
const jsonSchemaModule = await resolveJsonSchemaModule();
const sseModule = await resolveSSEModule();
initializeEmitter(service, auth.schemes, auth.defaultAuth, { xmlModule, jsonSchemaModule, sseModule }, version);
reportIfNoRoutes(program, httpService.operations);
for (const op of resolveOperations(httpService.operations)) {
const result = getOperationOrSharedOperation(op);
if (result) {
const { operation, path, verb } = result;
currentPath[path] ??= {};
currentPath[path][verb] = operation;
}
}
emitParameters();
emitSchemas(service.type);
emitTags();
// Clean up empty entries
if (root.components) {
for (const elem of Object.keys(root.components)) {
if (Object.keys(root.components[elem]).length === 0) {
delete root.components[elem];
}
}
}
return [root, diagnostics.diagnostics];
}
catch (err) {
if (err instanceof ErrorTypeFoundError) {
// Return early, there must be a parse error if an ErrorType was
// inserted into the TypeSpec output
return;
}
else {
throw err;
}
}
}
function joinOps(operations, func, joinChar) {
const values = operations
.map((op) => func(program, op.operation))
.filter((op) => op !== undefined);
if (values.length) {
return values.join(joinChar);
}
else {
return undefined;
}
}
function computeSharedOperationId(shared) {
if (options.operationIdStrategy.kind === "explicit-only")
return undefined;
const operationIds = shared.operations.map((op) => operationIdResolver.resolve(op.operation));
const uniqueOpIds = new Set(operationIds);
if (uniqueOpIds.size === 1)
return uniqueOpIds.values().next().value;
return operationIds.join("_");
}
function getOperationOrSharedOperation(operation) {
if (isSharedHttpOperation(operation)) {
return getSharedOperation(operation);
}
else {
return getOperation(operation);
}
}
function getSharedOperation(shared) {
const operations = shared.operations;
const verb = operations[0].verb;
const path = operations[0].path;
const examples = resolveOperationExamples(program, shared, {
parameterExamplesStrategy: options.parameterExamplesStrategy,
});
const oai3Operation = {
operationId: computeSharedOperationId(shared),
parameters: [],
description: joinOps(operations, getDoc, " "),
summary: joinOps(operations, getSummary, " "),
responses: getSharedResponses(shared, examples),
};
for (const op of operations) {
applyExternalDocs(op.operation, oai3Operation);
attachExtensions(program, op.operation, oai3Operation);
if (isOperationDeprecated(op)) {
oai3Operation.deprecated = true;
}
}
for (const op of operations) {
const opTags = getAllTags(program, op.operation);
if (opTags) {
const currentTags = oai3Operation.tags;
if (currentTags) {
// combine tags but eliminate duplicates
oai3Operation.tags = [...new Set([...currentTags, ...opTags])];
}
else {
oai3Operation.tags = opTags;
}
for (const tag of opTags) {
// Add to root tags if not already there
tags.add(tag);
}
}
}
// Error out if shared routes do not have consistent `@parameterVisibility`. We can
// lift this restriction in the future if a use case develops.
const visibilities = operations.map((op) => resolveRequestVisibility(program, op.operation, verb));
if (visibilities.some((v) => v !== visibilities[0])) {
diagnostics.add(createDiagnostic({
code: "inconsistent-shared-route-request-visibility",
target: operations[0].operation,
}));
}
const visibility = visibilities[0];
oai3Operation.parameters = getEndpointParameters(resolveSharedRouteParameters(operations), visibility, examples);
const bodies = [
...new Set(operations.map((op) => op.parameters.body).filter((x) => x !== undefined)),
];
if (bodies) {
oai3Operation.requestBody = getRequestBody(bodies, visibility, examples);
}
const authReference = serviceAuth.operationsAuth.get(shared.operations[0].operation);
if (authReference) {
oai3Operation.security = getEndpointSecurity(authReference);
}
return { operation: oai3Operation, verb, path };
}
function getOperation(operation) {
const { path: fullPath, operation: op, verb, parameters } = operation;
// If path contains a query string, issue msg and don't emit this endpoint
if (fullPath.indexOf("?") > 0) {
diagnostics.add(createDiagnostic({ code: "path-query", target: op }));
return undefined;
}
const visibility = resolveRequestVisibility(program, operation.operation, verb);
const examples = resolveOperationExamples(program, operation, {
parameterExamplesStrategy: options.parameterExamplesStrategy,
});
const oai3Operation = {
operationId: operationIdResolver.resolve(operation.operation),
summary: getSummary(program, operation.operation),
description: getDoc(program, operation.operation),
parameters: getEndpointParameters(parameters.properties, visibility, examples),
responses: getResponses(operation, operation.responses, examples),
};
const currentTags = getAllTags(program, op);
if (currentTags) {
oai3Operation.tags = currentTags;
for (const tag of currentTags) {
// Add to root tags if not already there
tags.add(tag);
}
}
applyExternalDocs(op, oai3Operation);
// Set up basic endpoint fields
if (parameters.body) {
oai3Operation.requestBody = getRequestBody(parameters.body && [parameters.body], visibility, examples);
}
const authReference = serviceAuth.operationsAuth.get(operation.operation);
if (authReference) {
oai3Operation.security = getEndpointSecurity(authReference);
}
if (isOperationDeprecated(operation)) {
oai3Operation.deprecated = true;
}
attachExtensions(program, op, oai3Operation);
return { operation: oai3Operation, path: fullPath, verb };
}
function getSharedResponses(operation, examples) {
const responseMap = new Map();
for (const op of operation.operations) {
for (const response of op.responses) {
const statusCodes = diagnostics.pipe(getOpenAPI3StatusCodes(program, response.statusCodes, op.operation));
for (const statusCode of statusCodes) {
if (responseMap.has(statusCode)) {
responseMap.get(statusCode).push(response);
}
else {
responseMap.set(statusCode, [response]);
}
}
}
}
const result = {};
for (const [statusCode, statusCodeResponses] of responseMap) {
const dedupeResponses = deduplicateCommonResponses(statusCodeResponses);
result[statusCode] = getResponseForStatusCode(operation, statusCode, dedupeResponses, examples);
}
return result;
}
function getResponses(operation, responses, examples) {
const responseMap = new Map();
// Group responses by status code first. When named unions are expanded into individual
// response variants, multiple variants may map to the same status code. We need to collect
// all variants for each status code before processing to properly merge content types and
// select the appropriate description.
for (const response of responses) {
for (const statusCode of diagnostics.pipe(getOpenAPI3StatusCodes(program, response.statusCodes, response.type))) {
if (responseMap.has(statusCode)) {
responseMap.get(statusCode).push(response);
}
else {
responseMap.set(statusCode, [response]);
}
}
}
// Generate OpenAPI response for each status code
const result = {};
for (const [statusCode, statusCodeResponses] of responseMap) {
result[statusCode] = getResponseForStatusCode(operation, statusCode, statusCodeResponses, examples);
}
return result;
}
function isBinaryPayload(body, contentType) {
return (body.kind === "Scalar" &&
body.name === "bytes" &&
contentType !== "application/json" &&
contentType !== "text/plain");
}
function getResponseForStatusCode(operation, statusCode, responses, examples) {
const openApiResponse = {
description: "",
};
const schemaMap = new Map();
for (const response of responses) {
const refUrl = getRef(program, response.type);
if (refUrl) {
return { $ref: refUrl };
}
if (response.description && response.description !== openApiResponse.description) {
openApiResponse.description = openApiResponse.description
? `${openApiResponse.description} ${response.description}`
: response.description;
}
emitResponseHeaders(openApiResponse, response.responses, response.type);
emitResponseContent(operation, openApiResponse, response.responses, statusCode, examples, schemaMap);
if (!openApiResponse.description) {
openApiResponse.description = getResponseDescriptionForStatusCode(statusCode);
}
}
return openApiResponse;
}
function emitResponseHeaders(obj, responses, target) {
for (const data of responses) {
if (data.headers && Object.keys(data.headers).length > 0) {
obj.headers ??= {};
// OpenAPI can't represent different headers per content type.
// So we merge headers here, and report any duplicates unless they are identical
for (const [key, value] of Object.entries(data.headers)) {
const headerVal = getResponseHeader(value);
const existing = obj.headers[key];
if (existing) {
if (!deepEquals(existing, headerVal)) {
diagnostics.add(createDiagnostic({
code: "duplicate-header",
format: { header: key },
target: target,
}));
}
continue;
}
obj.headers[key] = headerVal;
}
}
}
}
function emitResponseContent(operation, obj, responses, statusCode, examples, schemaMap = undefined) {
schemaMap ??= new Map();
for (const data of responses) {
if (data.body === undefined || isVoidType(data.body.type)) {
continue;
}
obj.content ??= {};
for (const contentType of data.body.contentTypes) {
const contents = getBodyContentEntry(data, Visibility.Read, contentType, examples.responses[statusCode]?.[contentType]);
if (schemaMap.has(contentType)) {
schemaMap.get(contentType).push(contents);
}
else {
schemaMap.set(contentType, [contents]);
}
}
for (const [contentType, contents] of schemaMap) {
if (contents.length === 1) {
obj.content[contentType] = contents[0];
}
else {
obj.content[contentType] = {
schema: { anyOf: contents.map((x) => x.schema) },
};
}
}
}
}
function getResponseDescriptionForStatusCode(statusCode) {
if (statusCode === "default") {
return "An unexpected error response.";
}
return getStatusCodeDescription(statusCode) ?? "unknown";
}
function getResponseHeader(prop) {
return getOpenAPIParameterBase(prop, Visibility.Read);
}
function callSchemaEmitter(type, visibility, ignoreMetadataAnnotations, contentType) {
const result = emitTypeWithSchemaEmitter(type, visibility, ignoreMetadataAnnotations, contentType);
switch (result.kind) {
case "code":
return result.value;
case "declaration":
return { $ref: `#/components/schemas/${result.name}` };
case "circular":
diagnostics.add(createDiagnostic({
code: "inline-cycle",
format: { type: getOpenAPITypeName(program, type, typeNameOptions) },
target: type,
}));
return {};
case "none":
return {};
}
}
function getSchemaValue(type, visibility, contentType) {
const result = emitTypeWithSchemaEmitter(type, visibility, false, contentType);
switch (result.kind) {
case "code":
case "declaration":
return result.value;
case "circular":
diagnostics.add(createDiagnostic({
code: "inline-cycle",
format: { type: getOpenAPITypeName(program, type, typeNameOptions) },
target: type,
}));
return {};
case "none":
return {};
}
}
function emitTypeWithSchemaEmitter(type, visibility, ignoreMetadataAnnotations, contentType) {
if (!metadataInfo.isTransformed(type, visibility)) {
visibility = Visibility.Read;
}
contentType = contentType === "application/json" ? undefined : contentType;
return schemaEmitter.emitType(type, {
referenceContext: {
visibility,
serviceNamespaceName: serviceNamespaceName,
ignoreMetadataAnnotations: ignoreMetadataAnnotations ?? false,
contentType,
},
});
}
function getBodyContentEntry(dataOrBody, visibility, contentType, examples) {
const isResponseContent = "body" in dataOrBody && dataOrBody.body !== undefined;
const body = isResponseContent
? dataOrBody.body
: dataOrBody;
const isBinary = isBinaryPayload(body.type, contentType);
if (isBinary) {
return { schema: getRawBinarySchema(contentType) };
}
// Check if this is a stream response (only for responses)
if (sseModule && isResponseContent) {
// Use getStreamMetadata to check if this is a stream response
const streamMetadata = getStreamMetadata(program, dataOrBody);
if (streamMetadata) {
if (specVersion === "3.1.0" || specVersion === "3.0.0") {
// Streams with itemSchema are not supported in OpenAPI 3.0/3.1 - emit warning and continue without itemSchema
reportDiagnostic(program, {
code: "streams-not-supported",
target: body.type,
});
// Fall through to normal processing without itemSchema
}
else {
// Full stream support for OpenAPI 3.2.0
const mediaType = {};
sseModule.attachSSEItemSchema(program, options, streamMetadata.streamType, mediaType, (type) => callSchemaEmitter(type, visibility, false, "application/json"));
if (Object.keys(mediaType).length > 0) {
return mediaType;
}
}
}
}
const oai3Examples = examples && getExampleOrExamples(program, examples);
switch (body.bodyKind) {
case "single":
return {
schema: getSchemaForSingleBody(body.type, visibility, body.isExplicit && body.containsMetadataAnnotations, undefined),
...oai3Examples,
};
case "multipart":
return {
...getBodyContentForMultipartBody(body, visibility, contentType),
...oai3Examples,
};
case "file":
return {
schema: getRawBinarySchema(contentType),
};
}
}
function getSchemaForSingleBody(type, visibility, ignoreMetadataAnnotations, multipart) {
const effectiveType = metadataInfo.getEffectivePayloadType(type, visibility);
return callSchemaEmitter(effectiveType, visibility, ignoreMetadataAnnotations, multipart ?? "application/json");
}
function getBodyContentForMultipartBody(body, visibility, contentType) {
const properties = {};
const requiredProperties = [];
const encodings = {};
for (const [partIndex, part] of body.parts.entries()) {
const partName = part.name ?? `part${partIndex}`;
let schema = part.body.bodyKind === "file"
? getRawBinarySchema()
: isBytesKeptRaw(program, part.body.type)
? getRawBinarySchema()
: getSchemaForSingleBody(part.body.type, visibility, part.body.isExplicit && part.body.containsMetadataAnnotations, part.body.type.kind === "Union" &&
[...part.body.type.variants.values()].some((x) => isBinaryPayload(x.type, contentType))
? contentType
: undefined);
if (part.multi) {
schema = {
type: "array",
items: schema,
};
}
if (part.property) {
const doc = getDoc(program, part.property);
if (doc) {
if (schema.$ref) {
schema = { allOf: [{ $ref: schema.$ref }], description: doc };
}
else {
schema = { ...schema, description: doc };
}
}
// Attach any OpenAPI extensions from the part property
attachExtensions(program, part.property, schema);
}
properties[partName] = schema;
const encoding = resolveEncodingForMultipartPart(part, visibility, schema);
if (encoding) {
encodings[partName] = encoding;
}
if (!part.optional) {
requiredProperties.push(partName);
}
}
const schema = {
type: "object",
properties,
required: requiredProperties,
};
const name = "name" in body.type && body.type.name !== ""
? getOpenAPITypeName(program, body.type, typeNameOptions)
: undefined;
if (name) {
root.components.schemas[name] = schema;
}
const result = {
schema: name ? { $ref: "#/components/schemas/" + name } : schema,
};
if (Object.keys(encodings).length > 0) {
result.encoding = encodings;
}
return result;
}
function resolveEncodingForMultipartPart(part, visibility, schema) {
const encoding = {};
if (!isDefaultContentTypeForOpenAPI3(part.body.contentTypes, schema)) {
encoding.contentType = part.body.contentTypes.join(", ");
}
const headers = part.headers;
if (headers.length > 0) {
encoding.headers = {};
for (const header of headers) {
const schema = getOpenAPIParameterBase(header.property, visibility);
if (schema !== undefined) {
encoding.headers[header.options.name] = schema;
}
}
}
if (Object.keys(encoding).length === 0) {
return undefined;
}
return encoding;
}
function isDefaultContentTypeForOpenAPI3(contentTypes, schema) {
if (contentTypes.length === 0) {
return false;
}
if (contentTypes.length > 1) {
return false;
}
const contentType = contentTypes[0];
switch (contentType) {
case "text/plain":
return schema.type === "string" || schema.type === "number";
case "application/octet-stream":
return (isRawBinarySchema(schema) ||
(schema.type === "array" && !!schema.items && isRawBinarySchema(schema.items)));
case "application/json":
return schema.type === "object";
}
return false;
}
function getParameter(httpProperty, visibility, examples) {
const param = {
name: httpProperty.options.name,
in: httpProperty.kind,
...getOpenAPIParameterBase(httpProperty.property, visibility),
};
const attributes = getParameterAttributes(httpProperty);
if (attributes === undefined) {
param.schema = {
type: "string",
};
}
else {
Object.assign(param, attributes);
}
if (isDeprecated(program, httpProperty.property)) {
param.deprecated = true;
}
const paramExamples = getExampleOrExamples(program, examples);
Object.assign(param, paramExamples);
return param;
}
function getEndpointParameters(properties, visibility, examples) {
const result = [];
for (const httpProp of properties) {
if (params.has(httpProp.property)) {
result.push(params.get(httpProp.property));
continue;
}
if (!isHttpParameterProperty(httpProp)) {
continue;
}
const param = getParameterOrRef(httpProp, visibility, examples.parameters[httpProp.options.name] ?? []);
if (param) {
const existing = result.find((x) => !("$ref" in param) && !("$ref" in x) && x.name === param.name && x.in === param.in);
if (existing && !("$ref" in param) && !("$ref" in existing)) {
mergeOpenApiParameters(existing, param);
}
else {
result.push(param);
}
}
}
return result;
}
function getRequestBody(bodies, visibility, examples) {
if (bodies === undefined || bodies.every((x) => isVoidType(x.type))) {
return undefined;
}
const requestBody = {
required: bodies.every((body) => (body.property ? !body.property.optional : true)),
content: {},
};
const schemaMap = new Map();
for (const body of bodies.filter((x) => !isVoidType(x.type))) {
const desc = body.property ? getDoc(program, body.property) : undefined;
if (desc) {
requestBody.description = requestBody.description
? `${requestBody.description} ${desc}`
: desc;
}
const contentTypes = body.contentTypes.length > 0 ? body.contentTypes : ["application/json"];
for (const contentType of contentTypes) {
const existing = schemaMap.get(contentType);
const entry = getBodyContentEntry(body, visibility, contentType, examples.requestBody[contentType]);
if (existing) {
existing.push(entry);
}
else {
schemaMap.set(contentType, [entry]);
}
}
}
for (const [contentType, schemaArray] of schemaMap) {
if (schemaArray.length === 1) {
requestBody.content[contentType] = schemaArray[0];
}
else {
requestBody.content[contentType] = {
schema: { anyOf: schemaArray.map((x) => x.schema).filter((x) => x !== undefined) },
encoding: schemaArray.find((x) => x.encoding)?.encoding,
};
}
}
return requestBody;
}
function getParameterOrRef(httpProperty, visibility, examples) {
if (isNeverType(httpProperty.property.type)) {
return undefined;
}
let spreadParam = false;
let property = httpProperty.property;
if (property.sourceProperty) {
// chase our sources all the way back to the first place this property
// was defined.
spreadParam = true;
property = property.sourceProperty;
while (property.sourceProperty) {
property = property.sourceProperty;
}
}
const refUrl = getRef(program, property);
if (refUrl) {
return {
$ref: refUrl,
};
}
if (params.has(property)) {
return params.get(property);
}
const param = getParameter(httpProperty, visibility, examples);
// only parameters inherited by spreading from non-inlined type are shared in #/components/parameters
if (spreadParam && property.model && !shouldInline(program, property.model)) {
params.set(property, param);
paramModels.add(property.model);
}
return param;
}
function getOpenAPIParameterBase(param, visibility) {
const typeSchema = getSchemaForType(param.type, visibility);
if (!typeSchema) {
return undefined;
}
const schema = applyEncoding(program, param, applyIntrinsicDecorators(param, typeSchema), options);
if (param.defaultValue) {
schema.default = getDefaultValue(program, param.defaultValue, param);
}
// Description is already provided in the parameter itself.
delete schema.description;
const oaiParam = {
required: !param.optional,
description: getDoc(program, param),
schema,
};
attachExtensions(program, param, oaiParam);
return oaiParam;
}
function mergeOpenApiParameters(target, apply) {
if (target.schema) {
const schema = target.schema;
if ("enum" in schema &&
schema.enum &&
apply.schema &&
"enum" in apply.schema &&
apply.schema.enum) {
schema.enum = [...new Set([...schema.enum, ...apply.schema.enum])];
}
target.schema = schema;
}
else {
Object.assign(target, apply);
}
return target;
}
function getParameterAttributes(httpProperty) {
switch (httpProperty.kind) {
case "header":
return getHeaderParameterAttributes(httpProperty);
case "cookie":
// style and explode options are omitted from cookies
// https://github.com/microsoft/typespec/pull/4761#discussion_r1803365689
return { explode: false };
case "query":
return get