UNPKG

@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
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