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

257 lines 12 kB
import { ArrayBuilder, createAssetEmitter, ObjectBuilder, setProperty, } from "@typespec/asset-emitter"; import { compilerAssert, getDiscriminatedUnion, getDoc, getExamples, getMaxValueExclusive, getMinValueExclusive, getSummary, } from "@typespec/compiler"; import { getOneOf } from "./decorators.js"; import { serializeExample } from "./examples.js"; import { reportDiagnostic } from "./lib.js"; import { applyEncoding, getRawBinarySchema } from "./openapi-helpers-3-1.js"; import { OpenAPI3SchemaEmitterBase } from "./schema-emitter.js"; import { isBytesKeptRaw, isLiteralType, literalType } from "./util.js"; function createWrappedSchemaEmitterClass(metadataInfo, visibilityUsage, options, optionalDependencies) { return class extends OpenAPI31SchemaEmitter { constructor(emitter) { super(emitter, metadataInfo, visibilityUsage, options, optionalDependencies); } }; } export const createSchemaEmitter3_1 = ({ program, context, ...rest }) => { return createAssetEmitter(program, createWrappedSchemaEmitterClass(rest.metadataInfo, rest.visibilityUsage, rest.options, rest.optionalDependencies), context); }; /** * OpenAPI 3.1 schema emitter. Deals with emitting content of `components/schemas` section. */ export class OpenAPI31SchemaEmitter extends OpenAPI3SchemaEmitterBase { #applySchemaExamples(program, type, target) { const examples = getExamples(program, type); if (examples.length > 0) { setProperty(target, "examples", examples.map((example) => serializeExample(program, example.value, type))); } } applyCustomConstraints(type, target, refSchema) { const applyConstraint = (fn, key) => { const value = fn(program, type); if (value !== undefined) { target[key] = value; } }; const applyTypeConstraint = (fn, key) => { const constraintType = fn(this.emitter.getProgram(), type); if (constraintType) { const ref = this.emitter.emitTypeReference(constraintType); compilerAssert(ref.kind === "code", "Unexpected non-code result from emit reference"); setProperty(target, key, ref.value); } }; const program = this.emitter.getProgram(); const minValueExclusive = getMinValueExclusive(program, type); if (minValueExclusive !== undefined) { target.minimum = undefined; target.exclusiveMinimum = minValueExclusive; } const maxValueExclusive = getMaxValueExclusive(program, type); if (maxValueExclusive !== undefined) { target.maximum = undefined; target.exclusiveMaximum = maxValueExclusive; } // apply json schema decorators const jsonSchemaModule = this._jsonSchemaModule; if (jsonSchemaModule) { applyTypeConstraint(jsonSchemaModule.getContains, "contains"); applyConstraint(jsonSchemaModule.getMinContains, "minContains"); applyConstraint(jsonSchemaModule.getMaxContains, "maxContains"); applyConstraint(jsonSchemaModule.getContentEncoding, "contentEncoding"); applyConstraint(jsonSchemaModule.getContentMediaType, "contentMediaType"); applyTypeConstraint(jsonSchemaModule.getContentSchema, "contentSchema"); const prefixItems = jsonSchemaModule.getPrefixItems(program, type); if (prefixItems) { const prefixItemsSchema = new ArrayBuilder(); for (const item of prefixItems.values) { prefixItemsSchema.push(this.emitter.emitTypeReference(item)); } setProperty(target, "prefixItems", prefixItemsSchema); } } this.#applySchemaExamples(program, type, target); } applyEncoding(typespecType, target) { return applyEncoding(this.emitter.getProgram(), typespecType, target, this._options); } applyModelIndexer(schema, model) { const shouldSeal = this.shouldSealSchema(model); if (!shouldSeal && !model.indexer) return; const unevaluatedPropertiesSchema = shouldSeal ? { not: {} } : this.emitter.emitTypeReference(model.indexer.value); setProperty(schema, "unevaluatedProperties", unevaluatedPropertiesSchema); } getRawBinarySchema() { return getRawBinarySchema(); } getSchemaForStdScalars(scalar) { // Raw binary data is handled separately when resolving a request/response body. // Open API 3.1 treats encoded binaries differently from Open API 3.0, so we need to handle // the Scalar 'bytes' special here. // @see https://spec.openapis.org/oas/v3.1.1.html#working-with-binary-data if (scalar.name === "bytes") { const contentType = this.emitter.getContext().contentType; return { type: "string", contentMediaType: contentType, contentEncoding: "base64" }; } return super.getSchemaForStdScalars(scalar); } enumSchema(en) { const program = this.emitter.getProgram(); if (en.members.size === 0) { reportDiagnostic(program, { code: "empty-enum", target: en }); return {}; } if (this._options.enumStrategy === "annotated") { return this.#annotatedEnumSchema(en); } const enumTypes = new Set(); const enumValues = new Set(); for (const member of en.members.values()) { enumTypes.add(typeof member.value === "number" ? "number" : "string"); enumValues.add(member.value ?? member.name); } const enumTypesArray = [...enumTypes]; const schema = { type: enumTypesArray.length === 1 ? enumTypesArray[0] : enumTypesArray, enum: [...enumValues], }; return this.applyConstraints(en, schema); } #annotatedEnumSchema(en) { const program = this.emitter.getProgram(); const oneOf = []; for (const member of en.members.values()) { const value = member.value ?? member.name; const subschema = { const: value }; const title = getSummary(program, member); if (title !== undefined) { subschema.title = title; } const description = getDoc(program, member); if (description !== undefined) { subschema.description = description; } oneOf.push(subschema); } return this.applyConstraints(en, { oneOf }); } combineRefWithConstraints(refSchema, constraints) { // Some constraints already compose the reference themselves (e.g. the XML module wraps // it in an `allOf`). In that case defer to the base behavior, otherwise we would emit // both a top-level `$ref` and an `allOf` pointing at the same schema. if ("allOf" in constraints) { return super.combineRefWithConstraints(refSchema, constraints); } // OpenAPI 3.1 allows keywords alongside `$ref` (JSON Schema 2020-12 semantics), // so the constraints are merged directly onto the reference instead of wrapping it // in an unnecessary `allOf`. const merged = new ObjectBuilder(refSchema); for (const [key, value] of Object.entries(constraints)) { setProperty(merged, key, value); } return merged; } unionSchema(union) { const program = this.emitter.getProgram(); const [discriminated] = getDiscriminatedUnion(program, union); if (discriminated) { return this.discriminatedUnion(discriminated); } if (union.variants.size === 0) { reportDiagnostic(program, { code: "empty-union", target: union }); return new ObjectBuilder({}); } const variants = Array.from(union.variants.values()); const literalVariantEnumByType = {}; const ofType = getOneOf(program, union) ? "oneOf" : "anyOf"; const schemaMembers = []; const isMultipart = this.getContentType().startsWith("multipart/"); // 1. Iterate over all the union variants to generate a schema for each one. for (const variant of variants) { // 2. Special handling for multipart - want to treat as binary if (isMultipart && isBytesKeptRaw(program, variant.type)) { schemaMembers.push({ schema: this.getRawBinarySchema(), type: variant.type }); continue; } // 3.a. Literal types are actual values (though not Value types) if (isLiteralType(variant.type)) { // Create schemas grouped by kind (boolean, string, numeric) // and add the literals seen to each respective `enum` array if (!literalVariantEnumByType[variant.type.kind]) { const enumValue = [variant.type.value]; literalVariantEnumByType[variant.type.kind] = enumValue; schemaMembers.push({ schema: { type: literalType(variant.type), enum: enumValue }, type: null, }); } else { literalVariantEnumByType[variant.type.kind].push(variant.type.value); } } else { // 3.b. Anything else, we get the schema for that type. const enumSchema = this.emitter.emitTypeReference(variant.type, { referenceContext: isMultipart ? { contentType: "application/json" } : {}, }); compilerAssert(enumSchema.kind === "code", "Unexpected enum schema. Should be kind: code"); schemaMembers.push({ schema: enumSchema.value, type: variant.type }); } } const wrapWithObjectBuilder = (schemaMember, { mergeUnionWideConstraints }) => { const schema = schemaMember.schema; const additionalProps = mergeUnionWideConstraints ? this.applyConstraints(union, {}) : {}; if (Object.keys(additionalProps).length === 0) { return new ObjectBuilder(schema); } else { // OpenAPI 3.1 allows sibling keywords next to `$ref`, so union-wide // constraints are merged directly onto the member schema. return this.combineRefWithConstraints(schema, additionalProps); } }; if (schemaMembers.length === 0) { compilerAssert(false, "Attempting to emit an empty union"); } if (schemaMembers.length === 1) { return wrapWithObjectBuilder(schemaMembers[0], { mergeUnionWideConstraints: true }); } const schema = { [ofType]: schemaMembers.map((m) => wrapWithObjectBuilder(m, { mergeUnionWideConstraints: false })), }; return this.applyConstraints(union, schema); } intrinsic(intrinsic, name) { switch (name) { case "unknown": return {}; case "null": return { type: "null" }; } reportDiagnostic(this.emitter.getProgram(), { code: "invalid-schema", format: { type: name }, target: intrinsic, }); return {}; } tupleLiteral(tuple) { return new ObjectBuilder({ type: "array", prefixItems: this.emitter.emitTupleLiteralValues(tuple), }); } tupleLiteralValues(tuple) { const values = new ArrayBuilder(); for (const value of tuple.values.values()) { values.push(this.emitter.emitType(value)); } return values; } } //# sourceMappingURL=schema-emitter-3-1.js.map