285 lines
7.2 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const YAML = require('js-yaml');
const { glob } = require('glob');
class OpenAPIValidator {
constructor() {
this.errors = [];
this.warnings = [];
this.stats = {
totalFiles: 0,
validFiles: 0,
invalidFiles: 0
};
}
// 验证YAML文件格式
validateYAMLSyntax(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf8');
YAML.load(content);
return { valid: true };
} catch (error) {
return {
valid: false,
error: `YAML语法错误: ${error.message}`
};
}
}
// 验证OpenAPI规范
validateOpenAPISpec(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf8');
const doc = YAML.load(content);
// 基本结构检查
if (filePath.endsWith('openapi.yaml')) {
return this.validateMainSpec(doc);
} else if (filePath.includes('/schemas/')) {
return this.validateSchemas(doc);
} else if (filePath.includes('/paths/')) {
return this.validatePaths(doc);
} else if (filePath.includes('/components/')) {
return this.validateComponents(doc);
}
return { valid: true };
} catch (error) {
return {
valid: false,
error: `规范验证错误: ${error.message}`
};
}
}
// 验证主文档
validateMainSpec(doc) {
const errors = [];
if (!doc.openapi) {
errors.push('缺少 openapi 版本号');
} else if (!doc.openapi.startsWith('3.0')) {
errors.push('openapi 版本应为 3.0.x');
}
if (!doc.info) {
errors.push('缺少 info 部分');
} else {
if (!doc.info.title) errors.push('缺少 info.title');
if (!doc.info.version) errors.push('缺少 info.version');
}
if (!doc.paths) {
errors.push('缺少 paths 部分');
}
return {
valid: errors.length === 0,
errors: errors
};
}
// 验证数据模型
validateSchemas(doc) {
const errors = [];
for (const [name, schema] of Object.entries(doc)) {
if (typeof schema !== 'object') continue;
// 检查必要字段
if (!schema.type && !schema.$ref && !schema.allOf && !schema.oneOf && !schema.anyOf) {
errors.push(`模型 ${name} 缺少 type 或引用定义`);
}
// 检查描述
if (schema.type === 'object' && schema.properties) {
for (const [propName, prop] of Object.entries(schema.properties)) {
if (!prop.description) {
errors.push(`模型 ${name}.${propName} 缺少描述`);
}
}
}
}
return {
valid: errors.length === 0,
errors: errors
};
}
// 验证路径定义
validatePaths(doc) {
const errors = [];
for (const [pathName, pathItem] of Object.entries(doc)) {
if (typeof pathItem !== 'object') continue;
for (const [method, operation] of Object.entries(pathItem)) {
if (!['get', 'post', 'put', 'patch', 'delete', 'head', 'options'].includes(method)) {
continue;
}
// 检查必要字段
if (!operation.tags) {
errors.push(`路径 ${pathName}.${method} 缺少 tags`);
}
if (!operation.summary) {
errors.push(`路径 ${pathName}.${method} 缺少 summary`);
}
if (!operation.description) {
errors.push(`路径 ${pathName}.${method} 缺少 description`);
}
if (!operation.operationId) {
errors.push(`路径 ${pathName}.${method} 缺少 operationId`);
}
if (!operation.responses) {
errors.push(`路径 ${pathName}.${method} 缺少 responses`);
}
}
}
return {
valid: errors.length === 0,
errors: errors
};
}
// 验证组件定义
validateComponents(doc) {
const errors = [];
// 基本组件结构检查
for (const [name, component] of Object.entries(doc)) {
if (typeof component !== 'object') continue;
// 检查是否有必要的属性
if (!component.description && !component.$ref) {
errors.push(`组件 ${name} 建议添加描述`);
}
}
return {
valid: errors.length === 0,
errors: errors
};
}
// 验证单个文件
async validateFile(filePath) {
console.log(`验证文件: ${filePath}`);
this.stats.totalFiles++;
// YAML语法验证
const syntaxResult = this.validateYAMLSyntax(filePath);
if (!syntaxResult.valid) {
this.errors.push(`${filePath}: ${syntaxResult.error}`);
this.stats.invalidFiles++;
return false;
}
// OpenAPI规范验证
const specResult = this.validateOpenAPISpec(filePath);
if (!specResult.valid) {
this.stats.invalidFiles++;
if (specResult.errors) {
specResult.errors.forEach(error => {
this.errors.push(`${filePath}: ${error}`);
});
} else {
this.errors.push(`${filePath}: ${specResult.error}`);
}
return false;
}
this.stats.validFiles++;
return true;
}
// 验证所有文档文件
async validateAll() {
console.log('🔍 开始验证 OpenAPI 文档...\n');
try {
// 查找所有YAML文件
const yamlFiles = await glob('docs/**/*.yaml', {
cwd: process.cwd(),
absolute: true
});
if (yamlFiles.length === 0) {
console.log('⚠️ 未找到任何 YAML 文件');
return false;
}
console.log(`找到 ${yamlFiles.length} 个 YAML 文件\n`);
// 验证每个文件
for (const file of yamlFiles) {
await this.validateFile(file);
}
return this.generateReport();
} catch (error) {
console.error('验证过程中出现错误:', error);
return false;
}
}
// 生成验证报告
generateReport() {
console.log('\n📊 验证报告');
console.log('='.repeat(50));
console.log(`总文件数: ${this.stats.totalFiles}`);
console.log(`有效文件: ${this.stats.validFiles}`);
console.log(`无效文件: ${this.stats.invalidFiles}`);
if (this.errors.length > 0) {
console.log('\n❌ 发现的错误:');
this.errors.forEach((error, index) => {
console.log(`${index + 1}. ${error}`);
});
}
if (this.warnings.length > 0) {
console.log('\n⚠ 警告信息:');
this.warnings.forEach((warning, index) => {
console.log(`${index + 1}. ${warning}`);
});
}
const isValid = this.errors.length === 0;
if (isValid) {
console.log('\n✅ 所有文档验证通过!');
} else {
console.log('\n❌ 文档验证失败,请修复上述错误。');
}
return isValid;
}
}
// 主执行函数
async function main() {
const validator = new OpenAPIValidator();
const isValid = await validator.validateAll();
// 设置退出码
process.exit(isValid ? 0 : 1);
}
// 如果直接运行此脚本
if (require.main === module) {
main().catch(error => {
console.error('验证脚本执行失败:', error);
process.exit(1);
});
}
module.exports = OpenAPIValidator;