2019-08-07 23:18:05 +02:00
|
|
|
#!/usr/bin/env node
|
2021-02-09 22:46:25 +01:00
|
|
|
|
2020-08-01 03:43:15 +02:00
|
|
|
"use strict";
|
2019-08-07 23:18:05 +02:00
|
|
|
|
2020-07-15 01:29:15 +02:00
|
|
|
const fs = require("fs");
|
2020-07-24 06:02:07 +02:00
|
|
|
|
2021-09-09 01:40:28 +02:00
|
|
|
const Diff = require("diff");
|
2020-07-15 01:29:15 +02:00
|
|
|
const ExampleValidator = require("openapi-examples-validator");
|
2021-09-09 01:34:30 +02:00
|
|
|
const Prettier = require("prettier");
|
2020-07-24 06:02:07 +02:00
|
|
|
const SwaggerParser = require("swagger-parser");
|
2021-09-10 01:06:35 +02:00
|
|
|
const {Composer, CST, LineCounter, Parser, Scalar, YAMLMap, YAMLSeq, visit} = require("yaml");
|
2021-09-09 02:02:42 +02:00
|
|
|
const yargs = require("yargs");
|
|
|
|
|
|
|
|
const {argv} = yargs.option("fix", {
|
|
|
|
type: "boolean",
|
|
|
|
description: "Automatically fix some problems",
|
|
|
|
});
|
2017-06-02 23:12:26 +02:00
|
|
|
|
2021-09-09 00:05:16 +02:00
|
|
|
async function checkFile(file) {
|
|
|
|
const yaml = await fs.promises.readFile(file, "utf8");
|
|
|
|
const lineCounter = new LineCounter();
|
2021-09-10 00:18:53 +02:00
|
|
|
const tokens = [...new Parser(lineCounter.addNewLine).parse(yaml)];
|
|
|
|
const docs = [...new Composer().compose(tokens)];
|
|
|
|
if (docs.length !== 1) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const [doc] = docs;
|
2021-09-09 00:05:16 +02:00
|
|
|
if (doc.errors.length > 0) {
|
|
|
|
for (const error of doc.errors) {
|
|
|
|
console.error("%s: %s", file, error.message);
|
2020-08-11 22:57:50 +02:00
|
|
|
}
|
2021-09-09 00:05:16 +02:00
|
|
|
process.exitCode = 1;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const root = doc.contents;
|
|
|
|
if (!(root instanceof YAMLMap && root.has("openapi"))) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let ok = true;
|
2021-09-10 01:06:35 +02:00
|
|
|
const reformats = new Map();
|
2021-09-09 00:05:16 +02:00
|
|
|
|
|
|
|
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);
|
2020-08-11 22:57:50 +02:00
|
|
|
ok = false;
|
|
|
|
}
|
2021-09-09 00:05:16 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
2021-09-09 01:34:30 +02:00
|
|
|
|
|
|
|
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) {
|
2021-09-10 01:06:35 +02:00
|
|
|
if (argv.fix) {
|
|
|
|
reformats.set(node.value.range[0], {
|
|
|
|
value: formatted,
|
|
|
|
context: {afterKey: true},
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
ok = false;
|
|
|
|
const {line, col} = lineCounter.linePos(node.value.range[0]);
|
|
|
|
console.error(
|
|
|
|
"%s:%d:%d: Format description with Prettier:",
|
|
|
|
file,
|
|
|
|
line,
|
|
|
|
col,
|
|
|
|
);
|
|
|
|
let diff = "";
|
|
|
|
for (const part of Diff.diffLines(node.value.value, formatted)) {
|
|
|
|
const prefix = part.added
|
|
|
|
? "\u001B[32m+"
|
|
|
|
: part.removed
|
|
|
|
? "\u001B[31m-"
|
|
|
|
: "\u001B[34m ";
|
|
|
|
diff += prefix;
|
|
|
|
diff += part.value.replace(/\n$/, "").replace(/\n/g, "\n" + prefix);
|
|
|
|
diff += "\n";
|
|
|
|
}
|
|
|
|
diff += "\u001B[0m";
|
|
|
|
console.error(diff);
|
2021-09-09 01:40:28 +02:00
|
|
|
}
|
2021-09-09 01:34:30 +02:00
|
|
|
}
|
|
|
|
}
|
2021-09-09 00:05:16 +02:00
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!ok) {
|
|
|
|
process.exitCode = 1;
|
|
|
|
}
|
2021-09-09 02:02:42 +02:00
|
|
|
if (reformats.size > 0) {
|
|
|
|
console.log("%s: Fixing problems", file);
|
2021-09-10 01:06:35 +02:00
|
|
|
for (const token of tokens) {
|
|
|
|
CST.visit(token, ({value}) => {
|
|
|
|
if (CST.isScalar(value) && reformats.has(value.offset)) {
|
|
|
|
const reformat = reformats.get(value.offset);
|
|
|
|
CST.setScalarValue(value, reformat.value, reformat.context);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2021-09-09 02:02:42 +02:00
|
|
|
await fs.promises.writeFile(file, tokens.map((token) => CST.stringify(token)).join(""));
|
|
|
|
}
|
2021-09-09 00:05:16 +02:00
|
|
|
|
|
|
|
try {
|
|
|
|
await SwaggerParser.validate(file);
|
|
|
|
} catch (error) {
|
|
|
|
if (!(error instanceof SyntaxError)) {
|
|
|
|
throw error;
|
2020-08-11 22:57:50 +02:00
|
|
|
}
|
2021-09-09 00:05:16 +02:00
|
|
|
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;
|
2020-08-11 22:57:50 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-28 04:42:06 +01:00
|
|
|
(async () => {
|
2021-09-09 02:02:42 +02:00
|
|
|
for (const file of argv._) {
|
2021-09-09 00:05:16 +02:00
|
|
|
await checkFile(file);
|
2017-06-02 23:12:26 +02:00
|
|
|
}
|
2020-05-07 00:04:22 +02:00
|
|
|
})().catch((error) => {
|
|
|
|
console.error(error);
|
|
|
|
process.exit(1);
|
|
|
|
});
|