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