swagger_generator_flutter/lib/validators/enhanced_validator.dart

594 lines
19 KiB
Dart

/// 增强的 OpenAPI 验证器
/// 集成详细的错误报告和修复建议
library;
import 'package:swagger_generator_flutter/core/error_reporter.dart';
import 'package:swagger_generator_flutter/core/models.dart';
/// 增强的 OpenAPI 验证器
class EnhancedValidator {
EnhancedValidator({
bool includeWarnings = true,
}) : _errorReporter = ErrorReporter(),
_includeWarnings = includeWarnings;
final ErrorReporter _errorReporter;
final bool _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: [
const 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: [
const 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: [
const 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: [
const 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: [
const FixSuggestion(
description: 'Add at least one API endpoint',
codeExample: '"/users": { "get": { "responses": { "200": '
'{ "description": "Success" } } } }',
),
],
);
return;
}
document.paths.forEach(_validatePath);
}
/// 验证单个路径
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: [
const 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: [
const FixSuggestion(
description: 'Add a brief summary',
codeExample: '"summary": "Get all users"',
),
],
);
}
// 验证参数
_validateParameters(apiPath.parameters, pathKey, pathPattern);
// 验证响应
_validateResponses(apiPath.responses, pathKey);
}
/// 验证参数
void _validateParameters(
List<ApiParameter> 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 (var 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: [
const 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: [
const FixSuggestion(
description: 'Set required: true for path parameters',
codeExample: '"required": true',
),
],
);
}
}
}
/// 验证响应
void _validateResponses(Map<String, ApiResponse> responses, String pathKey) {
var hasSuccessResponse = false;
var 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: [
const 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: [
const 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: [
const FixSuggestion(
description: 'Add common error responses',
codeExample: '"400": { "description": "Bad Request" }, '
'"404": { "description": "Not Found" }',
),
],
);
}
}
/// 验证组件
void _validateComponents(SwaggerDocument document) {
// 验证 schemas
document.components.schemas.forEach(_validateSchema);
// 验证安全方案
document.components.securitySchemes.forEach(_validateSecurityScheme);
}
/// 验证 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: [
const 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: [
const FixSuggestion(
description: 'Consider using composition with allOf',
codeExample:
r'"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: [
const FixSuggestion(
description: 'Add name field for API key parameter',
codeExample: '"name": "X-API-Key"',
),
],
);
}
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: [
const FixSuggestion(
description: 'Add scheme field',
codeExample: '"scheme": "bearer"',
),
],
);
}
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: [
const FixSuggestion(
description: 'Add flows configuration',
codeExample: '"flows": { "authorizationCode": '
'{ "authorizationUrl": "...", "tokenUrl": "..." } }',
),
],
);
}
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: [
const FixSuggestion(
description: 'Add OpenID Connect URL',
codeExample: '"openIdConnectUrl": '
'"https://example.com/.well-known/openid_configuration"',
),
],
);
}
}
}
/// 验证安全配置
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: [
const FixSuggestion(
description: 'Add tags to operations',
codeExample: '"tags": ["users"]',
),
],
);
}
}
/// 提取路径参数
Set<String> _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(_toPascalCase).join();
return '$methodPrefix$nameParts';
}
/// 转换为 PascalCase
String _toPascalCase(String input) {
return input
.split('_')
.map(
(word) => word.isEmpty
? ''
: word[0].toUpperCase() + word.substring(1).toLowerCase(),
)
.join();
}
}