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

214 lines 9.41 kB
import { createAssetEmitter, ObjectBuilder, Placeholder, setProperty, } from "@typespec/asset-emitter"; import { compilerAssert, getDiscriminatedUnion, getExamples, getMaxValueExclusive, getMinValueExclusive, isNullType, serializeValueAsJson, } from "@typespec/compiler"; import { $ } from "@typespec/compiler/typekit"; import { shouldInline } from "@typespec/openapi"; import { getOneOf } from "./decorators.js"; import { reportDiagnostic } from "./lib.js"; import { applyEncoding, getRawBinarySchema } from "./openapi-helpers-3-0.js"; import { Builders, OpenAPI3SchemaEmitterBase } from "./schema-emitter.js"; import { isBytesKeptRaw, isLiteralType, literalType } from "./util.js"; function createWrappedSchemaEmitterClass(metadataInfo, visibilityUsage, options, optionalDependencies) { return class extends OpenAPI3SchemaEmitter { constructor(emitter) { super(emitter, metadataInfo, visibilityUsage, options, optionalDependencies); } }; } export const createSchemaEmitter3_0 = ({ program, context, ...rest }) => { return createAssetEmitter(program, createWrappedSchemaEmitterClass(rest.metadataInfo, rest.visibilityUsage, rest.options, rest.optionalDependencies), context); }; /** * OpenAPI 3.0 schema emitter. Deals with emitting content of `components/schemas` section. */ export class OpenAPI3SchemaEmitter extends OpenAPI3SchemaEmitterBase { #applySchemaExamples(type, target) { const program = this.emitter.getProgram(); const examples = getExamples(program, type); if (examples.length > 0) { setProperty(target, "example", serializeValueAsJson(program, examples[0].value, type)); } } applyCustomConstraints(type, target, refSchema) { const program = this.emitter.getProgram(); const minValueExclusive = getMinValueExclusive(program, type); if (minValueExclusive !== undefined) { target.minimum = minValueExclusive; target.exclusiveMinimum = true; } const maxValueExclusive = getMaxValueExclusive(program, type); if (maxValueExclusive !== undefined) { target.maximum = maxValueExclusive; target.exclusiveMaximum = true; } this.#applySchemaExamples(type, target); } applyEncoding(typespecType, target) { return applyEncoding(this.emitter.getProgram(), typespecType, target, this._options); } getRawBinarySchema() { return getRawBinarySchema(); } 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); } if (enumTypes.size > 1) { reportDiagnostic(program, { code: "enum-unique-type", target: en }); } const schema = { type: enumTypes.values().next().value, 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 = []; let nullable = false; const isMultipart = this.getContentType().startsWith("multipart/"); for (const variant of variants) { if (isNullType(variant.type)) { nullable = true; continue; } if (isMultipart && isBytesKeptRaw(program, variant.type)) { schemaMembers.push({ schema: this.getRawBinarySchema(), type: variant.type }); continue; } if (isLiteralType(variant.type)) { 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 { 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 }) => { // we can just return the single schema member after applying nullable const schema = schemaMember.schema; const type = schemaMember.type; const additionalProps = mergeUnionWideConstraints ? this.applyConstraints(union, {}) : {}; if (mergeUnionWideConstraints && nullable) { additionalProps.nullable = true; } 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") { return new ObjectBuilder({ type: "object", allOf: Builders.array([schema]), ...additionalProps, }); } else if (type && type.kind === "Scalar") { const stdType = $(program).scalar.getStdBase(type); const outputType = stdType ? this.getSchemaForStdScalars(stdType).type : undefined; return new ObjectBuilder({ type: outputType, 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; } } }; const checkMerge = (schemaMembers) => { if (nullable) { for (const m of schemaMembers) { if (m.schema instanceof Placeholder || "$ref" in m.schema) { return true; } } } return false; }; if (schemaMembers.length === 0) { if (nullable) { // This union is equivalent to just `null` but OA3 has no way to specify // null as a value, so we throw an error. reportDiagnostic(program, { code: "union-null", target: union }); return new ObjectBuilder({}); } else { // completely empty union can maybe only happen with bugs? compilerAssert(false, "Attempting to emit an empty union"); } } if (schemaMembers.length === 1) { return wrapWithObjectBuilder(schemaMembers[0], { mergeUnionWideConstraints: true }); } const isMerge = checkMerge(schemaMembers); const schema = { [ofType]: schemaMembers.map((m) => wrapWithObjectBuilder(m, { mergeUnionWideConstraints: isMerge })), }; if (!isMerge && nullable) { schema.nullable = true; } return this.applyConstraints(union, schema); } intrinsic(intrinsic, name) { switch (name) { case "unknown": return {}; case "null": return { nullable: true }; } reportDiagnostic(this.emitter.getProgram(), { code: "invalid-schema", format: { type: name }, target: intrinsic, }); return {}; } } //# sourceMappingURL=schema-emitter-3-0.js.map