Files

84 lines
3.4 KiB
JavaScript
Raw Permalink Normal View History

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
})
}