zulip/tools/check-openapi

103 lines
3.1 KiB
Plaintext
Raw Normal View History

#!/usr/bin/env node
"use strict";
const fs = require("fs");
const ExampleValidator = require("openapi-examples-validator");
const Prettier = require("prettier");
const SwaggerParser = require("swagger-parser");
const {LineCounter, Scalar, YAMLMap, YAMLSeq, parseDocument, visit} = require("yaml");
async function checkFile(file) {
const yaml = await fs.promises.readFile(file, "utf8");
const lineCounter = new LineCounter();
const doc = parseDocument(yaml, {lineCounter});
if (doc.errors.length > 0) {
for (const error of doc.errors) {
console.error("%s: %s", file, error.message);
}
process.exitCode = 1;
return;
}
const root = doc.contents;
if (!(root instanceof YAMLMap && root.has("openapi"))) {
return;
}
let ok = true;
visit(doc, {
Map(key, node) {
if (node.has("$ref") && node.items.length !== 1) {
const {line, col} = lineCounter.linePos(node.range[0]);
console.error("%s:%d:%d: Siblings of $ref have no effect", file, line, col);
ok = false;
}
},
Pair(key, node) {
if (
node.key instanceof Scalar &&
node.key.value === "allOf" &&
node.value instanceof YAMLSeq &&
node.value.items.filter(
(subschema) => !(subschema instanceof YAMLMap && subschema.has("$ref")),
).length > 1
) {
const {line, col} = lineCounter.linePos(node.value.range[0]);
console.error("%s:%d:%d: Too many inline allOf subschemas", file, line, col);
ok = false;
}
if (
node.key instanceof Scalar &&
node.key.value === "description" &&
node.value instanceof Scalar &&
typeof node.value.value === "string"
) {
let formatted = Prettier.format(node.value.value, {parser: "markdown"});
if (![Scalar.BLOCK_FOLDED, Scalar.BLOCK_LITERAL].includes(node.value.type)) {
formatted = formatted.replace(/\n$/, "");
}
if (formatted !== node.value.value) {
ok = false;
const {line, col} = lineCounter.linePos(node.value.range[0]);
console.error("%s:%d:%d: Format description with Prettier", file, line, col);
}
}
},
});
if (!ok) {
process.exitCode = 1;
}
try {
await SwaggerParser.validate(file);
} catch (error) {
if (!(error instanceof SyntaxError)) {
throw error;
}
console.error("%s: %s", file, error.message);
process.exitCode = 1;
}
const res = await ExampleValidator.validateFile(file);
if (!res.valid) {
for (const error of res.errors) {
console.error(error);
}
process.exitCode = 1;
}
}
(async () => {
for (const file of process.argv.slice(2)) {
await checkFile(file);
}
})().catch((error) => {
console.error(error);
process.exit(1);
});