openapi-typescript
Version:
Convert OpenAPI 3.0 & 3.1 schemas to TypeScript
195 lines (186 loc) • 5.26 kB
text/typescript
import {
BaseResolver,
bundle,
makeDocumentFromString,
type Config as RedoclyConfig,
Source,
type Document,
lintDocument,
} from "@redocly/openapi-core";
import { Readable } from "node:stream";
import { fileURLToPath } from "node:url";
import { OpenAPI3 } from "../types.js";
import { debug, error, warn } from "./utils.js";
export interface ValidateAndBundleOptions {
redoc: RedoclyConfig;
silent: boolean;
cwd?: URL;
}
interface ParseSchemaOptions {
absoluteRef: string;
resolver: BaseResolver;
}
export async function parseSchema(
schema: unknown,
{ absoluteRef, resolver }: ParseSchemaOptions,
): Promise<Document> {
if (!schema) {
throw new Error(`Can’t parse empty schema`);
}
if (schema instanceof URL) {
const result = await resolver.resolveDocument(null, absoluteRef, true);
if ("parsed" in result) {
return result;
}
throw result.originalError;
}
if (schema instanceof Readable) {
const contents = await new Promise<string>((resolve) => {
schema.resume();
schema.setEncoding("utf8");
let content = "";
schema.on("data", (chunk: string) => {
content += chunk;
});
schema.on("end", () => {
resolve(content.trim());
});
});
return parseSchema(contents, { absoluteRef, resolver });
}
if (schema instanceof Buffer) {
return parseSchema(schema.toString("utf8"), { absoluteRef, resolver });
}
if (typeof schema === "string") {
// URL
if (
schema.startsWith("http://") ||
schema.startsWith("https://") ||
schema.startsWith("file://")
) {
const url = new URL(schema);
return parseSchema(url, {
absoluteRef: url.protocol === "file:" ? fileURLToPath(url) : url.href,
resolver,
});
}
// JSON
if (schema[0] === "{") {
return {
source: new Source(absoluteRef, schema, "application/json"),
parsed: JSON.parse(schema),
};
}
// YAML
return makeDocumentFromString(schema, absoluteRef);
}
if (typeof schema === "object" && !Array.isArray(schema)) {
return {
source: new Source(
absoluteRef,
JSON.stringify(schema),
"application/json",
),
parsed: schema,
};
}
throw new Error(
`Expected string, object, or Buffer. Got ${
Array.isArray(schema) ? "Array" : typeof schema
}`,
);
}
/**
* Validate an OpenAPI schema and flatten into a single schema using Redocly CLI
*/
export async function validateAndBundle(
source: string | URL | OpenAPI3 | Readable | Buffer,
options: ValidateAndBundleOptions,
) {
const redocConfigT = performance.now();
debug("Loaded Redoc config", "redoc", performance.now() - redocConfigT);
const redocParseT = performance.now();
let absoluteRef = fileURLToPath(
new URL(options?.cwd ?? `file://${process.cwd()}/`),
);
if (source instanceof URL) {
absoluteRef =
source.protocol === "file:" ? fileURLToPath(source) : source.href;
}
const resolver = new BaseResolver(options.redoc.resolve);
const document = await parseSchema(source, {
absoluteRef,
resolver,
});
debug("Parsed schema", "redoc", performance.now() - redocParseT);
// 1. check for OpenAPI 3 or greater
const openapiVersion = parseFloat(document.parsed.openapi);
if (
document.parsed.swagger ||
!document.parsed.openapi ||
Number.isNaN(openapiVersion) ||
openapiVersion < 3 ||
openapiVersion >= 4
) {
if (document.parsed.swagger) {
throw new Error(
"Unsupported Swagger version: 2.x. Use OpenAPI 3.x instead.",
);
} else if (
document.parsed.openapi ||
openapiVersion < 3 ||
openapiVersion >= 4
) {
throw new Error(
`Unsupported OpenAPI version: ${document.parsed.openapi}`,
);
}
throw new Error("Unsupported schema format, expected `openapi: 3.x`");
}
// 2. lint
const redocLintT = performance.now();
const problems = await lintDocument({
document,
config: options.redoc.styleguide,
externalRefResolver: resolver,
});
if (problems.length) {
let errorMessage: string | undefined = undefined;
for (const problem of problems) {
if (problem.severity === "error") {
errorMessage = problem.message;
error(problem.message);
} else {
warn(problem.message, options.silent);
}
}
if (errorMessage) {
throw new Error(errorMessage);
}
}
debug("Linted schema", "lint", performance.now() - redocLintT);
// 3. bundle
const redocBundleT = performance.now();
const bundled = await bundle({
config: options.redoc,
dereference: false,
doc: document,
});
if (bundled.problems.length) {
let errorMessage: string | undefined = undefined;
for (const problem of bundled.problems) {
if (problem.severity === "error") {
errorMessage = problem.message;
error(problem.message);
throw new Error(problem.message);
} else {
warn(problem.message, options.silent);
}
}
if (errorMessage) {
throw new Error(errorMessage);
}
}
debug("Bundled schema", "bundle", performance.now() - redocBundleT);
return bundled.bundle.parsed;
}