Refactor code structure for improved readability and maintainability; removed redundant code blocks and optimized function calls.
This commit is contained in:
139
scripts/api-example-coverage.mjs
Normal file
139
scripts/api-example-coverage.mjs
Normal file
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env node
|
||||
// Verifies every OpenAPI operation has at least one request example and one response example.
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { parse } from 'yaml';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const OAS_ROOT = path.join(ROOT, 'src', 'Api', 'StellaOps.Api.OpenApi');
|
||||
|
||||
async function main() {
|
||||
if (!fs.existsSync(OAS_ROOT)) {
|
||||
console.log('[api:examples] no OpenAPI directory found; skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await findYamlFiles(OAS_ROOT);
|
||||
if (files.length === 0) {
|
||||
console.log('[api:examples] no OpenAPI files found; skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
const failures = [];
|
||||
|
||||
for (const relative of files) {
|
||||
const fullPath = path.join(OAS_ROOT, relative);
|
||||
const content = fs.readFileSync(fullPath, 'utf8');
|
||||
let doc;
|
||||
try {
|
||||
doc = parse(content, { prettyErrors: true });
|
||||
} catch (err) {
|
||||
failures.push({ file: relative, path: '', method: '', reason: `YAML parse error: ${err.message}` });
|
||||
continue;
|
||||
}
|
||||
|
||||
const paths = doc?.paths || {};
|
||||
for (const [route, methods] of Object.entries(paths)) {
|
||||
for (const [method, operation] of Object.entries(methods || {})) {
|
||||
if (!isHttpMethod(method)) continue;
|
||||
|
||||
const hasRequestExample = operation?.requestBody ? hasExample(operation.requestBody) : true;
|
||||
const hasResponseExample = Object.values(operation?.responses || {}).some(resp => hasExample(resp));
|
||||
|
||||
if (!hasRequestExample || !hasResponseExample) {
|
||||
const missing = [];
|
||||
if (!hasRequestExample) missing.push('request');
|
||||
if (!hasResponseExample) missing.push('response');
|
||||
failures.push({ file: relative, path: route, method, reason: `missing ${missing.join(' & ')} example` });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error('[api:examples] found operations without examples:');
|
||||
for (const f of failures) {
|
||||
const locus = [f.file, f.path, f.method.toUpperCase()].filter(Boolean).join(' ');
|
||||
console.error(` - ${locus}: ${f.reason}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('[api:examples] all operations contain request and response examples');
|
||||
}
|
||||
|
||||
async function findYamlFiles(root) {
|
||||
const results = [];
|
||||
async function walk(dir) {
|
||||
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
await walk(full);
|
||||
} else if (entry.isFile() && entry.name.toLowerCase().endsWith('.yaml')) {
|
||||
results.push(path.relative(root, full));
|
||||
}
|
||||
}
|
||||
}
|
||||
await walk(root);
|
||||
return results;
|
||||
}
|
||||
|
||||
function isHttpMethod(method) {
|
||||
return ['get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'trace'].includes(method.toLowerCase());
|
||||
}
|
||||
|
||||
function hasExample(node) {
|
||||
if (!node) return false;
|
||||
|
||||
// request/response objects may include content -> mediaType -> schema/example/examples
|
||||
const content = node.content || {};
|
||||
for (const media of Object.values(content)) {
|
||||
if (!media) continue;
|
||||
if (media.example !== undefined) return true;
|
||||
if (media.examples && Object.keys(media.examples).length > 0) return true;
|
||||
if (media.schema && hasSchemaExample(media.schema)) return true;
|
||||
}
|
||||
|
||||
// response objects may have "examples" directly (non-standard but allowed by spectral rules)
|
||||
if (node.examples && Object.keys(node.examples).length > 0) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasSchemaExample(schema) {
|
||||
if (!schema) return false;
|
||||
if (schema.example !== undefined) return true;
|
||||
if (schema.examples && Array.isArray(schema.examples) && schema.examples.length > 0) return true;
|
||||
|
||||
// Recurse into allOf/oneOf/anyOf
|
||||
const composites = ['allOf', 'oneOf', 'anyOf'];
|
||||
for (const key of composites) {
|
||||
if (Array.isArray(schema[key])) {
|
||||
if (schema[key].some(hasSchemaExample)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
// For objects, check properties
|
||||
if (schema.type === 'object' && schema.properties) {
|
||||
for (const value of Object.values(schema.properties)) {
|
||||
if (hasSchemaExample(value)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
// For arrays, check items
|
||||
if (schema.type === 'array' && schema.items) {
|
||||
return hasSchemaExample(schema.items);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('[api:examples] fatal error', err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user