84 lines
3.4 KiB
JavaScript
84 lines
3.4 KiB
JavaScript
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
|
||
|
|
})
|
||
|
|
}
|