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

238 lines 11.1 kB
import { ArrayBuilder, createAssetEmitter, ObjectBuilder, Placeholder, setProperty, } from "@typespec/asset-emitter"; import { compilerAssert, getDiscriminatedUnion, getExamples, getMaxValueExclusive, getMinValueExclusive, serializeValueAsJson, } from "@typespec/compiler"; import { shouldInline } from "@typespec/openapi"; import { getOneOf } from "./decorators.js"; import { reportDiagnostic } from "./lib.js"; import { applyEncoding, getRawBinarySchema } from "./openapi-helpers-3-1.js"; import { Builders, 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) => serializeValueAsJson(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 {}; } 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); } 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 type = schemaMember.type; const additionalProps = mergeUnionWideConstraints ? this.applyConstraints(union, {}) : {}; if (Object.keys(additionalProps).length === 0) { return new ObjectBuilder(schema); } else { if ((schema instanceof Placeholder || "$ref" in schema) && !(type && shouldInline(program, type))) { if (type && (type.kind === "Model" || type.kind === "Scalar")) { return new ObjectBuilder({ type: "object", allOf: Builders.array([schema]), ...additionalProps, }); } else { return new ObjectBuilder({ allOf: Builders.array([schema]), ...additionalProps }); } } else { const merged = new ObjectBuilder(schema); for (const [key, value] of Object.entries(additionalProps)) { setProperty(merged, key, value); } return merged; } } }; 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