@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 kB
JavaScript
import { ArrayBuilder, createAssetEmitter, ObjectBuilder, Placeholder, } 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) {
target.set("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");
target.set(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));
}
target.set("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);
schema.set("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)) {
merged.set(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