修改未完成
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
"use strict";
|
||||
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const YAML = require("yaml");
|
||||
|
||||
const walk = (directory) => fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) =>
|
||||
entry.isDirectory() ? walk(path.join(directory, entry.name)) : [path.join(directory, entry.name)],
|
||||
);
|
||||
|
||||
const document = YAML.parse(fs.readFileSync("genealogy-app-openapi.yaml", "utf8"));
|
||||
assert.strictEqual(document.openapi, "3.0.3", "backend contract must be OpenAPI 3.0.3");
|
||||
const operations = new Set(
|
||||
Object.entries(document.paths).flatMap(([endpoint, pathItem]) =>
|
||||
Object.keys(pathItem)
|
||||
.filter((method) => ["get", "post", "put", "delete", "patch"].includes(method))
|
||||
.map((method) => `${method.toUpperCase()} ${endpoint}`),
|
||||
),
|
||||
);
|
||||
|
||||
const apiSource = fs.readFileSync("utils/api.js", "utf8");
|
||||
const appApiStart = apiSource.indexOf("export const appApi = {");
|
||||
assert.ok(appApiStart >= 0, "appApi owner is missing");
|
||||
const appApiSource = apiSource.slice(appApiStart);
|
||||
const methodMatches = [...appApiSource.matchAll(/^ async ([A-Za-z0-9_]+)\(/gm)];
|
||||
const methodSources = new Map(
|
||||
methodMatches.map((match, index) => [
|
||||
match[1],
|
||||
appApiSource.slice(match.index, methodMatches[index + 1]?.index),
|
||||
]),
|
||||
);
|
||||
|
||||
const normalizeEndpoint = (endpoint) => endpoint
|
||||
.replace(/\$\{[^}]+\}/g, "{parameter}")
|
||||
.replace(/\{[^/]+\}/g, "{parameter}");
|
||||
const resolveOpenApiEndpoint = (endpoint) => Object.keys(document.paths).find(
|
||||
(candidate) => normalizeEndpoint(candidate) === normalizeEndpoint(endpoint),
|
||||
);
|
||||
|
||||
const collectLiteralOperations = (source) => {
|
||||
const entries = [];
|
||||
const add = (method, endpoint) => {
|
||||
if (endpoint.includes("${relationPath}")) {
|
||||
for (const relationPath of ["parents", "spouses", "siblings", "children"]) {
|
||||
entries.push({ method, endpoint: endpoint.replace("${relationPath}", relationPath) });
|
||||
}
|
||||
return;
|
||||
}
|
||||
entries.push({ method, endpoint });
|
||||
};
|
||||
const literal = "([`'])";
|
||||
const endpoint = "(/genealogy[^`']*)";
|
||||
const readPattern = new RegExp(`readRemote(?:List|Object)\\(\\s*${literal}${endpoint}\\1`, "g");
|
||||
for (const match of source.matchAll(readPattern)) add("GET", match[2]);
|
||||
const writePattern = new RegExp(`writeRemote(?:Void|Object)\\(\\s*${literal}${endpoint}\\1\\s*,\\s*['\"](POST|PUT|DELETE|PATCH)['\"]`, "g");
|
||||
for (const match of source.matchAll(writePattern)) add(match[3], match[2]);
|
||||
const directPattern = new RegExp(`url:\\s*${literal}${endpoint}\\1\\s*,\\s*method:\\s*['\"](GET|POST|PUT|DELETE|PATCH)['\"]`, "g");
|
||||
for (const match of source.matchAll(directPattern)) add(match[3], match[2]);
|
||||
const conditionalMethodPattern = new RegExp(`url:\\s*${literal}${endpoint}\\1\\s*,\\s*method:\\s*[^?]+\\?\\s*['\"](POST|PUT|DELETE|PATCH)['\"]\\s*:\\s*['\"](POST|PUT|DELETE|PATCH)['\"]`, "g");
|
||||
for (const match of source.matchAll(conditionalMethodPattern)) {
|
||||
add(match[3], match[2]);
|
||||
add(match[4], match[2]);
|
||||
}
|
||||
return entries;
|
||||
};
|
||||
|
||||
const pageCalls = walk("pages")
|
||||
.filter((file) => file.endsWith(".vue"))
|
||||
.flatMap((file) => {
|
||||
const source = fs.readFileSync(file, "utf8");
|
||||
return [...source.matchAll(/appApi\.([A-Za-z0-9_]+)/g)].map((match) => ({
|
||||
file: file.replace(/\\/g, "/"),
|
||||
method: match[1],
|
||||
}));
|
||||
});
|
||||
const routedPages = new Set(
|
||||
JSON.parse(fs.readFileSync("pages.json", "utf8")).pages.map((page) => `${page.path}.vue`),
|
||||
);
|
||||
|
||||
const unresolvedMethods = [];
|
||||
const unmappedOperations = [];
|
||||
for (const method of [...new Set(pageCalls.map((call) => call.method))]) {
|
||||
const source = methodSources.get(method);
|
||||
if (!source) {
|
||||
unresolvedMethods.push(method);
|
||||
continue;
|
||||
}
|
||||
const references = collectLiteralOperations(source);
|
||||
if (!references.length) {
|
||||
unresolvedMethods.push(method);
|
||||
continue;
|
||||
}
|
||||
for (const reference of references) {
|
||||
const endpoint = resolveOpenApiEndpoint(reference.endpoint);
|
||||
if (!endpoint || !operations.has(`${reference.method} ${endpoint}`)) {
|
||||
unmappedOperations.push(`${method}: ${reference.method} ${reference.endpoint}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.deepStrictEqual(unresolvedMethods, [], "page call has no inspectable adapter endpoint owner");
|
||||
assert.deepStrictEqual(unmappedOperations, [], "page adapter endpoint diverges from current backend OpenAPI");
|
||||
process.stdout.write(`PAGE-OPENAPI-CONTRACT PASS routed=${routedPages.size} apiPages=${new Set(pageCalls.map((call) => call.file)).size} calls=${pageCalls.length}\n`);
|
||||
Reference in New Issue
Block a user