Scaffold full-stack solution and CI baseline
This commit is contained in:
93
scripts/generate-api-client.mjs
Normal file
93
scripts/generate-api-client.mjs
Normal file
@@ -0,0 +1,93 @@
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(scriptDirectory, "..");
|
||||
const openApiPath = path.join(repoRoot, "openapi", "RpgRoller.json");
|
||||
const outputPath = path.join(repoRoot, "RpgRoller", "wwwroot", "generated", "api-client.js");
|
||||
|
||||
function escapePathSegment(segment) {
|
||||
return segment.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
|
||||
}
|
||||
|
||||
function collectOperations(document) {
|
||||
const operations = [];
|
||||
for (const [pathKey, pathItem] of Object.entries(document.paths ?? {})) {
|
||||
for (const [method, operation] of Object.entries(pathItem ?? {})) {
|
||||
if (operation === null || typeof operation !== "object") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof operation.operationId !== "string" || operation.operationId.length === 0) {
|
||||
throw new Error(`Missing operationId for ${method.toUpperCase()} ${pathKey}`);
|
||||
}
|
||||
|
||||
operations.push({
|
||||
operationId: operation.operationId,
|
||||
method: method.toUpperCase(),
|
||||
path: pathKey
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (operations.length === 0) {
|
||||
throw new Error("OpenAPI document does not define any operations.");
|
||||
}
|
||||
|
||||
return operations.sort((left, right) => left.operationId.localeCompare(right.operationId));
|
||||
}
|
||||
|
||||
function buildClientSource(operations) {
|
||||
const operationEntries = operations
|
||||
.map((operation) => ` ${operation.operationId}: { method: "${operation.method}", path: "${escapePathSegment(operation.path)}" }`)
|
||||
.join(",\n");
|
||||
|
||||
const helper = `const apiOperations = {\n${operationEntries}\n};\n`;
|
||||
|
||||
const sendFunction = `
|
||||
async function send(operation, pathParams = {}) {
|
||||
let resolvedPath = operation.path;
|
||||
for (const [key, value] of Object.entries(pathParams)) {
|
||||
resolvedPath = resolvedPath.replace(\`{\${key}}\`, encodeURIComponent(String(value)));
|
||||
}
|
||||
|
||||
const response = await fetch(resolvedPath, {
|
||||
method: operation.method,
|
||||
headers: {
|
||||
"Accept": "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorPayload = await response.json().catch(() => ({ error: "Unknown API error." }));
|
||||
throw new Error(errorPayload.error ?? \`Request failed with status \${response.status}\`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
`.trim();
|
||||
|
||||
const exports = operations
|
||||
.map((operation) => {
|
||||
const params = [...operation.path.matchAll(/\{([^}]+)\}/g)].map((match) => match[1]);
|
||||
const signature = params.length === 0 ? "" : params.join(", ");
|
||||
const pathParams = params.length === 0
|
||||
? "{}"
|
||||
: `{ ${params.map((name) => `${name}: ${name}`).join(", ")} }`;
|
||||
|
||||
return `export async function ${operation.operationId}(${signature}) {\n return send(apiOperations.${operation.operationId}, ${pathParams});\n}`;
|
||||
})
|
||||
.join("\n\n");
|
||||
|
||||
return `/* This file is generated by scripts/generate-api-client.mjs. */\n\nexport { apiOperations };\n\n${helper}\n${sendFunction}\n\n${exports}\n`;
|
||||
}
|
||||
|
||||
const openApiText = await readFile(openApiPath, "utf8");
|
||||
const document = JSON.parse(openApiText);
|
||||
const operations = collectOperations(document);
|
||||
const clientSource = buildClientSource(operations);
|
||||
|
||||
await mkdir(path.dirname(outputPath), { recursive: true });
|
||||
await writeFile(outputPath, clientSource, "utf8");
|
||||
console.log(`Generated API client: ${path.relative(repoRoot, outputPath)}`);
|
||||
Reference in New Issue
Block a user