285 lines
7.2 KiB
JavaScript
285 lines
7.2 KiB
JavaScript
#!/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;
|