import SwaggerParser from '@apidevtools/swagger-parser' import { readdir, readFile } from 'node:fs/promises' import path from 'node:path' import { fileURLToPath, pathToFileURL } from 'node:url' const OPENAPI_DIRECTORY = ['src', 'main', 'resources', 'openapi'].join('/') const OPENAPI_EXTENSIONS = new Set(['.json', '.yaml', '.yml']) const EXCLUDED_DIRECTORIES = new Set([ '.git', '.logs', 'dist', 'node_modules', 'output', 'outputs', 'reports', 'target' ]) export const repositoryRoot = path.resolve(fileURLToPath(new URL('../..', import.meta.url))) export async function discoverOpenApiFiles(root) { const files = [] async function walk(directory) { const entries = await readdir(directory, { withFileTypes: true }) for (const entry of entries) { if (entry.isDirectory() && EXCLUDED_DIRECTORIES.has(entry.name)) continue const absolute = path.join(directory, entry.name) if (entry.isDirectory()) { await walk(absolute) continue } const relative = path.relative(root, absolute).replaceAll(path.sep, '/') if (relative.includes(`/${OPENAPI_DIRECTORY}/`) && OPENAPI_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(relative) } } await walk(root) return files.sort() } export function parseServiceModules(serviceMap) { return [...serviceMap.matchAll(/^ ([a-z][a-z0-9-]+):$/gm)].map((match) => match[1]).sort() } export function summarizeCoverage(files, serviceModules) { const coveredServices = [...new Set(files.map((file) => file.split('/')[0]))].sort() const codeFirstServices = serviceModules.filter((service) => !coveredServices.includes(service)) return { coveredServices, codeFirstServices } } export async function validateRepositoryOpenApi(root = repositoryRoot) { const files = await discoverOpenApiFiles(root) if (files.length === 0) throw new Error('No static OpenAPI documents were discovered') const serviceMap = await readFile(path.join(root, 'deploy', 'service-map.yml'), 'utf8') const serviceModules = parseServiceModules(serviceMap.replaceAll('\r\n', '\n')) const coverage = summarizeCoverage(files, serviceModules) const failures = [] for (const relative of files) { try { await SwaggerParser.validate(path.join(root, relative)) } catch (error) { failures.push(`${relative}: ${error instanceof Error ? error.message : String(error)}`) } } if (failures.length > 0) { throw new Error( `Discovered ${files.length} documents across ${coverage.coveredServices.length}/${serviceModules.length} services; ` + `code-first gaps: ${coverage.codeFirstServices.join(', ')}.\n` + `OpenAPI validation failed for ${failures.length}/${files.length} documents:\n` + failures.map((failure) => `- ${failure}`).join('\n') ) } return { files, serviceModules, ...coverage } } async function main() { const result = await validateRepositoryOpenApi() process.stdout.write(`OpenAPI validation passed: ${result.files.length} documents across ${result.coveredServices.length}/${result.serviceModules.length} services.\n`) process.stdout.write(`Static OpenAPI services: ${result.coveredServices.join(', ')}\n`) process.stdout.write(`Code-first services without static documents: ${result.codeFirstServices.join(', ')}\n`) } if (import.meta.url === pathToFileURL(process.argv[1]).href) { main().catch((error) => { process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`) process.exitCode = 1 }) }