mirror of
https://github.com/zulip/zulip.git
synced 2025-10-23 04:52:12 +00:00
check-openapi: Convert to TypeScript.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
committed by
Anders Kaseorg
parent
29dd592f38
commit
0fc5f6994f
@@ -12,7 +12,7 @@ trim_trailing_whitespace = true
|
|||||||
binary_next_line = true
|
binary_next_line = true
|
||||||
switch_case_indent = true
|
switch_case_indent = true
|
||||||
|
|
||||||
[{*.{cjs,cts,js,json,mjs,mts,ts},check-openapi}]
|
[*.{cjs,cts,js,json,mjs,mts,ts}]
|
||||||
max_line_length = 100
|
max_line_length = 100
|
||||||
|
|
||||||
[*.{py,pyi}]
|
[*.{py,pyi}]
|
||||||
|
@@ -264,7 +264,7 @@ above.
|
|||||||
|
|
||||||
You can check your formatting using these helpful tools.
|
You can check your formatting using these helpful tools.
|
||||||
|
|
||||||
- `tools/check-openapi` will verify the syntax of `zerver/openapi/zulip.yaml`.
|
- `tools/check-openapi.ts` will verify the syntax of `zerver/openapi/zulip.yaml`.
|
||||||
- `tools/test-backend zerver/tests/test_openapi.py`; this test compares
|
- `tools/test-backend zerver/tests/test_openapi.py`; this test compares
|
||||||
your documentation against the code and can find many common
|
your documentation against the code and can find many common
|
||||||
mistakes in how arguments are declared.
|
mistakes in how arguments are declared.
|
||||||
|
@@ -14,9 +14,6 @@ import tseslint from "typescript-eslint";
|
|||||||
const compat = new FlatCompat({baseDirectory: import.meta.dirname});
|
const compat = new FlatCompat({baseDirectory: import.meta.dirname});
|
||||||
|
|
||||||
export default tseslint.config(
|
export default tseslint.config(
|
||||||
{
|
|
||||||
files: ["tools/check-openapi"],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
// This is intended for generated files and vendored third-party files.
|
// This is intended for generated files and vendored third-party files.
|
||||||
// For our source code, instead of adding files here, consider using
|
// For our source code, instead of adding files here, consider using
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import assert from "node:assert/strict";
|
||||||
import * as fs from "node:fs";
|
import * as fs from "node:fs";
|
||||||
import {parseArgs} from "node:util";
|
import {parseArgs} from "node:util";
|
||||||
|
|
||||||
@@ -9,7 +10,7 @@ import ExampleValidator from "openapi-examples-validator";
|
|||||||
import {format as prettierFormat} from "prettier";
|
import {format as prettierFormat} from "prettier";
|
||||||
import {CST, Composer, LineCounter, Parser, Scalar, YAMLMap, YAMLSeq, visit} from "yaml";
|
import {CST, Composer, LineCounter, Parser, Scalar, YAMLMap, YAMLSeq, visit} from "yaml";
|
||||||
|
|
||||||
const usage = "Usage: check-openapi [--fix] <file>...";
|
const usage = "Usage: check-openapi.ts [--fix] <file>...";
|
||||||
const {
|
const {
|
||||||
values: {fix, help},
|
values: {fix, help},
|
||||||
positionals: files,
|
positionals: files,
|
||||||
@@ -20,7 +21,7 @@ if (help) {
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkFile(file) {
|
async function checkFile(file: string): Promise<void> {
|
||||||
const yaml = await fs.promises.readFile(file, "utf8");
|
const yaml = await fs.promises.readFile(file, "utf8");
|
||||||
const lineCounter = new LineCounter();
|
const lineCounter = new LineCounter();
|
||||||
const tokens = [...new Parser(lineCounter.addNewLine).parse(yaml)];
|
const tokens = [...new Parser(lineCounter.addNewLine).parse(yaml)];
|
||||||
@@ -29,6 +30,7 @@ async function checkFile(file) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const [doc] = docs;
|
const [doc] = docs;
|
||||||
|
assert.ok(doc !== undefined);
|
||||||
if (doc.errors.length > 0) {
|
if (doc.errors.length > 0) {
|
||||||
for (const error of doc.errors) {
|
for (const error of doc.errors) {
|
||||||
console.error("%s: %s", file, error.message);
|
console.error("%s: %s", file, error.message);
|
||||||
@@ -43,12 +45,16 @@ async function checkFile(file) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let ok = true;
|
let ok = true;
|
||||||
const reformats = new Map();
|
const reformats = new Map<
|
||||||
const promises = [];
|
number,
|
||||||
|
{value: string; context: Parameters<typeof CST.setScalarValue>[2]}
|
||||||
|
>();
|
||||||
|
const promises: Promise<void>[] = [];
|
||||||
|
|
||||||
visit(doc, {
|
visit(doc, {
|
||||||
Map(_key, node) {
|
Map(_key, node) {
|
||||||
if (node.has("$ref") && node.items.length !== 1) {
|
if (node.has("$ref") && node.items.length !== 1) {
|
||||||
|
assert.ok(node.range);
|
||||||
const {line, col} = lineCounter.linePos(node.range[0]);
|
const {line, col} = lineCounter.linePos(node.range[0]);
|
||||||
console.error("%s:%d:%d: Siblings of $ref have no effect", file, line, col);
|
console.error("%s:%d:%d: Siblings of $ref have no effect", file, line, col);
|
||||||
ok = false;
|
ok = false;
|
||||||
@@ -58,6 +64,7 @@ async function checkFile(file) {
|
|||||||
node.has(combinator),
|
node.has(combinator),
|
||||||
);
|
);
|
||||||
if (node.has("nullable") && combinator !== undefined) {
|
if (node.has("nullable") && combinator !== undefined) {
|
||||||
|
assert.ok(node.range);
|
||||||
const {line, col} = lineCounter.linePos(node.range[0]);
|
const {line, col} = lineCounter.linePos(node.range[0]);
|
||||||
console.error(
|
console.error(
|
||||||
`%s:%d:%d: nullable has no effect as a sibling of ${combinator}`,
|
`%s:%d:%d: nullable has no effect as a sibling of ${combinator}`,
|
||||||
@@ -78,6 +85,7 @@ async function checkFile(file) {
|
|||||||
(subschema) => !(subschema instanceof YAMLMap && subschema.has("$ref")),
|
(subschema) => !(subschema instanceof YAMLMap && subschema.has("$ref")),
|
||||||
).length > 1
|
).length > 1
|
||||||
) {
|
) {
|
||||||
|
assert.ok(node.value.range);
|
||||||
const {line, col} = lineCounter.linePos(node.value.range[0]);
|
const {line, col} = lineCounter.linePos(node.value.range[0]);
|
||||||
console.error("%s:%d:%d: Too many inline allOf subschemas", file, line, col);
|
console.error("%s:%d:%d: Too many inline allOf subschemas", file, line, col);
|
||||||
ok = false;
|
ok = false;
|
||||||
@@ -89,25 +97,29 @@ async function checkFile(file) {
|
|||||||
node.value instanceof Scalar &&
|
node.value instanceof Scalar &&
|
||||||
typeof node.value.value === "string"
|
typeof node.value.value === "string"
|
||||||
) {
|
) {
|
||||||
|
const value = node.value;
|
||||||
|
const description = node.value.value;
|
||||||
promises.push(
|
promises.push(
|
||||||
(async () => {
|
(async () => {
|
||||||
let formatted = await prettierFormat(node.value.value, {
|
let formatted = await prettierFormat(description, {
|
||||||
parser: "markdown",
|
parser: "markdown",
|
||||||
});
|
});
|
||||||
if (
|
if (
|
||||||
![Scalar.BLOCK_FOLDED, Scalar.BLOCK_LITERAL].includes(node.value.type)
|
value.type !== Scalar.BLOCK_FOLDED &&
|
||||||
|
value.type !== Scalar.BLOCK_LITERAL
|
||||||
) {
|
) {
|
||||||
formatted = formatted.replace(/\n$/, "");
|
formatted = formatted.replace(/\n$/, "");
|
||||||
}
|
}
|
||||||
if (formatted !== node.value.value) {
|
if (formatted !== description) {
|
||||||
|
assert.ok(value.range);
|
||||||
if (fix) {
|
if (fix) {
|
||||||
reformats.set(node.value.range[0], {
|
reformats.set(value.range[0], {
|
||||||
value: formatted,
|
value: formatted,
|
||||||
context: {afterKey: true},
|
context: {afterKey: true},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
ok = false;
|
ok = false;
|
||||||
const {line, col} = lineCounter.linePos(node.value.range[0]);
|
const {line, col} = lineCounter.linePos(value.range[0]);
|
||||||
console.error(
|
console.error(
|
||||||
"%s:%d:%d: Format description with Prettier:",
|
"%s:%d:%d: Format description with Prettier:",
|
||||||
file,
|
file,
|
||||||
@@ -115,7 +127,7 @@ async function checkFile(file) {
|
|||||||
col,
|
col,
|
||||||
);
|
);
|
||||||
let diff = "";
|
let diff = "";
|
||||||
for (const part of Diff.diffLines(node.value.value, formatted)) {
|
for (const part of Diff.diffLines(description, formatted)) {
|
||||||
const prefix = part.added
|
const prefix = part.added
|
||||||
? "\u001B[32m+"
|
? "\u001B[32m+"
|
||||||
: part.removed
|
: part.removed
|
||||||
@@ -144,13 +156,18 @@ async function checkFile(file) {
|
|||||||
if (reformats.size > 0) {
|
if (reformats.size > 0) {
|
||||||
console.log("%s: Fixing problems", file);
|
console.log("%s: Fixing problems", file);
|
||||||
for (const token of tokens) {
|
for (const token of tokens) {
|
||||||
|
if (token.type === "document") {
|
||||||
CST.visit(token, ({value}) => {
|
CST.visit(token, ({value}) => {
|
||||||
if (CST.isScalar(value) && reformats.has(value.offset)) {
|
let reformat;
|
||||||
const reformat = reformats.get(value.offset);
|
if (
|
||||||
|
CST.isScalar(value) &&
|
||||||
|
(reformat = reformats.get(value.offset)) !== undefined
|
||||||
|
) {
|
||||||
CST.setScalarValue(value, reformat.value, reformat.context);
|
CST.setScalarValue(value, reformat.value, reformat.context);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
await fs.promises.writeFile(file, tokens.map((token) => CST.stringify(token)).join(""));
|
await fs.promises.writeFile(file, tokens.map((token) => CST.stringify(token)).join(""));
|
||||||
}
|
}
|
||||||
|
|
@@ -110,7 +110,7 @@ def run() -> None:
|
|||||||
)
|
)
|
||||||
linter_config.external_linter(
|
linter_config.external_linter(
|
||||||
"openapi",
|
"openapi",
|
||||||
["node", "tools/check-openapi"],
|
["node", "tools/check-openapi.ts"],
|
||||||
["yaml"],
|
["yaml"],
|
||||||
description="Validates our OpenAPI/Swagger API documentation (zerver/openapi/zulip.yaml) ",
|
description="Validates our OpenAPI/Swagger API documentation (zerver/openapi/zulip.yaml) ",
|
||||||
fix_arg="--fix",
|
fix_arg="--fix",
|
||||||
|
Reference in New Issue
Block a user