/// 增强的 OpenAPI 验证器 /// 集成详细的错误报告和修复建议 library; import '../core/error_reporter.dart'; import '../core/models.dart'; /// 增强的 OpenAPI 验证器 class EnhancedValidator { final ErrorReporter _errorReporter; final bool _includeWarnings; EnhancedValidator({ bool includeWarnings = true, }) : _errorReporter = ErrorReporter(), _includeWarnings = includeWarnings; /// 获取错误报告器 ErrorReporter get errorReporter => _errorReporter; /// 验证 OpenAPI 文档 bool validateDocument(SwaggerDocument document) { _errorReporter.clear(); // 基础结构验证 _validateBasicStructure(document); // 路径验证 _validatePaths(document); // 组件验证 _validateComponents(document); // 安全方案验证 _validateSecurity(document); // 最佳实践检查 if (_includeWarnings) { _checkBestPractices(document); } return !_errorReporter.hasErrorsOrCritical; } /// 验证基础结构 void _validateBasicStructure(SwaggerDocument document) { // 验证标题 if (document.title.isEmpty) { _errorReporter.reportError( id: 'MISSING_INFO_TITLE', title: 'Missing API Title', description: 'API title is required in the info object.', severity: ErrorSeverity.error, category: ErrorCategory.validation, jsonPath: 'info.title', suggestions: [ FixSuggestion( description: 'Add a descriptive title for your API', codeExample: '"title": "My API"', ), ], ); } // 验证版本 if (document.version.isEmpty) { _errorReporter.reportError( id: 'MISSING_INFO_VERSION', title: 'Missing API Version', description: 'API version is required in the info object.', severity: ErrorSeverity.error, category: ErrorCategory.validation, jsonPath: 'info.version', suggestions: [ FixSuggestion( description: 'Add a version number using semantic versioning', codeExample: '"version": "1.0.0"', documentationUrl: 'https://semver.org/', ), ], ); } // 验证描述 if (document.description.isEmpty && _includeWarnings) { _errorReporter.reportError( id: 'MISSING_INFO_DESCRIPTION', title: 'Missing API Description', description: 'API description helps users understand the purpose of your API.', severity: ErrorSeverity.warning, category: ErrorCategory.bestPractice, jsonPath: 'info.description', suggestions: [ FixSuggestion( description: 'Add a description explaining what your API does', codeExample: '"description": "This API provides user management functionality"', ), ], ); } // 验证服务器配置 if (document.servers.isEmpty && _includeWarnings) { _errorReporter.reportError( id: 'MISSING_SERVERS', title: 'Missing Server Configuration', description: 'Server configuration helps clients know where to send requests.', severity: ErrorSeverity.warning, category: ErrorCategory.bestPractice, jsonPath: 'servers', suggestions: [ FixSuggestion( description: 'Add at least one server configuration', codeExample: '"servers": [{"url": "https://api.example.com", "description": "Production server"}]', ), ], ); } } /// 验证路径 void _validatePaths(SwaggerDocument document) { if (document.paths.isEmpty) { _errorReporter.reportError( id: 'EMPTY_PATHS', title: 'Empty Paths Object', description: 'OpenAPI document must contain at least one path.', severity: ErrorSeverity.error, category: ErrorCategory.validation, jsonPath: 'paths', suggestions: [ FixSuggestion( description: 'Add at least one API endpoint', codeExample: '"/users": { "get": { "responses": { "200": { "description": "Success" } } } }', ), ], ); return; } document.paths.forEach((pathPattern, apiPath) { _validatePath(pathPattern, apiPath); }); } /// 验证单个路径 void _validatePath(String pathPattern, ApiPath apiPath) { final pathKey = 'paths["$pathPattern"][${apiPath.method.value}]'; // 验证路径格式 if (!pathPattern.startsWith('/')) { _errorReporter.reportError( id: 'INVALID_PATH_FORMAT', title: 'Invalid Path Format', description: 'Path must start with a forward slash.', severity: ErrorSeverity.error, category: ErrorCategory.syntax, jsonPath: pathKey, snippet: pathPattern, suggestions: [ FixSuggestion( description: 'Ensure path starts with /', codeExample: '"/$pathPattern" instead of "$pathPattern"', ), ], ); } // 验证响应 if (apiPath.responses.isEmpty) { _errorReporter.reportError( id: 'MISSING_OPERATION_RESPONSES', title: 'Missing Operation Responses', description: 'Every operation must define at least one response.', severity: ErrorSeverity.error, category: ErrorCategory.validation, jsonPath: '$pathKey.responses', suggestions: [ FixSuggestion( description: 'Add at least a default response', codeExample: '"responses": { "200": { "description": "Success" } }', ), ], ); } // 验证操作 ID if (apiPath.operationId.isEmpty && _includeWarnings) { _errorReporter.reportError( id: 'MISSING_OPERATION_ID', title: 'Missing Operation ID', description: 'Operation should have an operationId for better code generation.', severity: ErrorSeverity.warning, category: ErrorCategory.bestPractice, jsonPath: '$pathKey.operationId', suggestions: [ FixSuggestion( description: 'Add a unique operationId', codeExample: '"operationId": "${_generateOperationId(pathPattern, apiPath.method)}"', ), ], ); } // 验证摘要 if (apiPath.summary.isEmpty && _includeWarnings) { _errorReporter.reportError( id: 'MISSING_OPERATION_SUMMARY', title: 'Missing Operation Summary', description: 'Operation should have a summary for better documentation.', severity: ErrorSeverity.info, category: ErrorCategory.bestPractice, jsonPath: '$pathKey.summary', suggestions: [ FixSuggestion( description: 'Add a brief summary', codeExample: '"summary": "Get all users"', ), ], ); } // 验证参数 _validateParameters(apiPath.parameters, pathKey, pathPattern); // 验证响应 _validateResponses(apiPath.responses, pathKey); } /// 验证参数 void _validateParameters( List parameters, String pathKey, String pathPattern) { // 提取路径参数 final pathParams = _extractPathParameters(pathPattern); final declaredPathParams = parameters .where((p) => p.location == ParameterLocation.path) .map((p) => p.name) .toSet(); // 检查路径参数是否都有声明 for (final param in pathParams) { if (!declaredPathParams.contains(param)) { _errorReporter.reportError( id: 'UNDECLARED_PATH_PARAMETER', title: 'Undeclared Path Parameter', description: 'Path parameter "$param" is used in the path but not declared in parameters.', severity: ErrorSeverity.error, category: ErrorCategory.validation, jsonPath: '$pathKey.parameters', suggestions: [ FixSuggestion( description: 'Add parameter declaration', codeExample: '{"name": "$param", "in": "path", "required": true, "schema": {"type": "string"}}', ), ], ); } } // 验证每个参数 for (int i = 0; i < parameters.length; i++) { final param = parameters[i]; final paramPath = '$pathKey.parameters[$i]'; // 验证参数名 if (param.name.isEmpty) { _errorReporter.reportError( id: 'MISSING_PARAMETER_NAME', title: 'Missing Parameter Name', description: 'Parameter must have a name.', severity: ErrorSeverity.error, category: ErrorCategory.validation, jsonPath: '$paramPath.name', suggestions: [ FixSuggestion( description: 'Add a name for the parameter', codeExample: '"name": "userId"', ), ], ); } // 验证路径参数必须是必需的 if (param.location == ParameterLocation.path && !param.required) { _errorReporter.reportError( id: 'PATH_PARAMETER_NOT_REQUIRED', title: 'Path Parameter Not Required', description: 'Path parameters must be marked as required.', severity: ErrorSeverity.error, category: ErrorCategory.validation, jsonPath: '$paramPath.required', suggestions: [ FixSuggestion( description: 'Set required: true for path parameters', codeExample: '"required": true', ), ], ); } } } /// 验证响应 void _validateResponses(Map responses, String pathKey) { bool hasSuccessResponse = false; bool hasErrorResponse = false; responses.forEach((code, response) { final responsePath = '$pathKey.responses["$code"]'; final statusCode = int.tryParse(code) ?? 0; // 检查成功响应 if (statusCode >= 200 && statusCode < 300) { hasSuccessResponse = true; } // 检查错误响应 if (statusCode >= 400) { hasErrorResponse = true; } // 验证响应描述 if (response.description.isEmpty) { _errorReporter.reportError( id: 'MISSING_RESPONSE_DESCRIPTION', title: 'Missing Response Description', description: 'Response should have a description.', severity: ErrorSeverity.warning, category: ErrorCategory.bestPractice, jsonPath: '$responsePath.description', suggestions: [ FixSuggestion( description: 'Add a description for the response', codeExample: '"description": "Successful operation"', ), ], ); } }); // 检查是否有成功响应 if (!hasSuccessResponse && _includeWarnings) { _errorReporter.reportError( id: 'NO_SUCCESS_RESPONSE', title: 'No Success Response', description: 'Operation should define at least one success response (2xx).', severity: ErrorSeverity.warning, category: ErrorCategory.bestPractice, jsonPath: '$pathKey.responses', suggestions: [ FixSuggestion( description: 'Add a success response', codeExample: '"200": { "description": "Success" }', ), ], ); } // 检查是否有错误响应 if (!hasErrorResponse && _includeWarnings) { _errorReporter.reportError( id: 'NO_ERROR_RESPONSE', title: 'No Error Response', description: 'Consider adding error responses (4xx/5xx) for better API documentation.', severity: ErrorSeverity.info, category: ErrorCategory.bestPractice, jsonPath: '$pathKey.responses', suggestions: [ FixSuggestion( description: 'Add common error responses', codeExample: '"400": { "description": "Bad Request" }, "404": { "description": "Not Found" }', ), ], ); } } /// 验证组件 void _validateComponents(SwaggerDocument document) { // 验证 schemas document.components.schemas.forEach((name, model) { _validateSchema(name, model); }); // 验证安全方案 document.components.securitySchemes.forEach((name, scheme) { _validateSecurityScheme(name, scheme); }); } /// 验证 Schema void _validateSchema(String name, ApiModel model) { final schemaPath = 'components.schemas["$name"]'; // 验证模型名称 if (model.name.isEmpty) { _errorReporter.reportError( id: 'MISSING_SCHEMA_NAME', title: 'Missing Schema Name', description: 'Schema should have a name.', severity: ErrorSeverity.error, category: ErrorCategory.validation, jsonPath: schemaPath, suggestions: [ FixSuggestion( description: 'Ensure schema has a valid name', codeExample: 'Schema name should match the key in components.schemas', ), ], ); } // 检查属性数量 if (model.properties.length > 20 && _includeWarnings) { _errorReporter.reportError( id: 'LARGE_SCHEMA_OBJECT', title: 'Large Schema Object', description: 'Schema has many properties (${model.properties.length}), consider breaking it down.', severity: ErrorSeverity.info, category: ErrorCategory.performance, jsonPath: schemaPath, suggestions: [ FixSuggestion( description: 'Consider using composition with allOf', codeExample: '"allOf": [{ "\$ref": "#/components/schemas/BaseModel" }, { "type": "object", "properties": {...} }]', ), ], ); } } /// 验证安全方案 void _validateSecurityScheme(String name, ApiSecurityScheme scheme) { final schemePath = 'components.securitySchemes["$name"]'; switch (scheme.type) { case SecuritySchemeType.apiKey: if (scheme.name == null || scheme.name!.isEmpty) { _errorReporter.reportError( id: 'MISSING_API_KEY_NAME', title: 'Missing API Key Name', description: 'API Key security scheme must specify a parameter name.', severity: ErrorSeverity.error, category: ErrorCategory.security, jsonPath: '$schemePath.name', suggestions: [ FixSuggestion( description: 'Add name field for API key parameter', codeExample: '"name": "X-API-Key"', ), ], ); } break; case SecuritySchemeType.http: if (scheme.scheme == null || scheme.scheme!.isEmpty) { _errorReporter.reportError( id: 'MISSING_HTTP_SCHEME', title: 'Missing HTTP Scheme', description: 'HTTP security scheme must specify a scheme (basic, bearer, etc.).', severity: ErrorSeverity.error, category: ErrorCategory.security, jsonPath: '$schemePath.scheme', suggestions: [ FixSuggestion( description: 'Add scheme field', codeExample: '"scheme": "bearer"', ), ], ); } break; case SecuritySchemeType.oauth2: if (scheme.flows == null) { _errorReporter.reportError( id: 'MISSING_OAUTH2_FLOWS', title: 'Missing OAuth2 Flows', description: 'OAuth2 security scheme must define flows.', severity: ErrorSeverity.error, category: ErrorCategory.security, jsonPath: '$schemePath.flows', suggestions: [ FixSuggestion( description: 'Add flows configuration', codeExample: '"flows": { "authorizationCode": { "authorizationUrl": "...", "tokenUrl": "..." } }', ), ], ); } break; case SecuritySchemeType.openIdConnect: if (scheme.openIdConnectUrl == null || scheme.openIdConnectUrl!.isEmpty) { _errorReporter.reportError( id: 'MISSING_OPENID_URL', title: 'Missing OpenID Connect URL', description: 'OpenID Connect security scheme must specify a URL.', severity: ErrorSeverity.error, category: ErrorCategory.security, jsonPath: '$schemePath.openIdConnectUrl', suggestions: [ FixSuggestion( description: 'Add OpenID Connect URL', codeExample: '"openIdConnectUrl": "https://example.com/.well-known/openid_configuration"', ), ], ); } break; } } /// 验证安全配置 void _validateSecurity(SwaggerDocument document) { // 这里可以添加安全配置的验证逻辑 } /// 检查最佳实践 void _checkBestPractices(SwaggerDocument document) { // 检查是否使用了标签 final hasTaggedOperations = document.paths.values.any((path) => path.tags.isNotEmpty); if (!hasTaggedOperations) { _errorReporter.reportError( id: 'NO_OPERATION_TAGS', title: 'No Operation Tags', description: 'Consider using tags to organize your API operations.', severity: ErrorSeverity.info, category: ErrorCategory.bestPractice, jsonPath: 'paths', suggestions: [ FixSuggestion( description: 'Add tags to operations', codeExample: '"tags": ["users"]', ), ], ); } } /// 提取路径参数 Set _extractPathParameters(String path) { final regex = RegExp(r'\{([^}]+)\}'); final matches = regex.allMatches(path); return matches.map((match) => match.group(1)!).toSet(); } /// 生成操作 ID String _generateOperationId(String path, HttpMethod method) { final pathParts = path .split('/') .where((part) => part.isNotEmpty && !part.startsWith('{')) .toList(); final methodPrefix = method.value.toLowerCase(); if (pathParts.length >= 3) { // 移除 api/v1 前缀 pathParts.removeRange(0, 2); } final nameParts = pathParts.map((part) => _toPascalCase(part)).join(''); return '$methodPrefix$nameParts'; } /// 转换为 PascalCase String _toPascalCase(String input) { return input .split('_') .map((word) => word.isEmpty ? '' : word[0].toUpperCase() + word.substring(1).toLowerCase()) .join(''); } }