#!/usr/bin/env node "use strict"; const fs = require("fs"); const Diff = require("diff"); 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); 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); } } }, }); 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); });