846 lines
25 KiB
Dart
846 lines
25 KiB
Dart
/// Schema 验证器
|
||
/// 验证 OpenAPI 3.0 文档的完整性和正确性
|
||
library;
|
||
|
||
import '../core/models.dart';
|
||
|
||
/// Schema 验证结果
|
||
class ValidationResult {
|
||
final bool isValid;
|
||
final List<ValidationError> errors;
|
||
final List<ValidationWarning> warnings;
|
||
|
||
const ValidationResult({
|
||
required this.isValid,
|
||
this.errors = const [],
|
||
this.warnings = const [],
|
||
});
|
||
|
||
/// 创建成功的验证结果
|
||
factory ValidationResult.success(
|
||
{List<ValidationWarning> warnings = const []}) {
|
||
return ValidationResult(
|
||
isValid: true,
|
||
warnings: warnings,
|
||
);
|
||
}
|
||
|
||
/// 创建失败的验证结果
|
||
factory ValidationResult.failure(List<ValidationError> errors,
|
||
{List<ValidationWarning> warnings = const []}) {
|
||
return ValidationResult(
|
||
isValid: false,
|
||
errors: errors,
|
||
warnings: warnings,
|
||
);
|
||
}
|
||
|
||
/// 是否有警告
|
||
bool get hasWarnings => warnings.isNotEmpty;
|
||
|
||
/// 是否有错误
|
||
bool get hasErrors => errors.isNotEmpty;
|
||
}
|
||
|
||
/// 验证错误
|
||
class ValidationError {
|
||
final String path;
|
||
final String message;
|
||
final ValidationErrorType type;
|
||
final String? suggestion;
|
||
|
||
const ValidationError({
|
||
required this.path,
|
||
required this.message,
|
||
required this.type,
|
||
this.suggestion,
|
||
});
|
||
|
||
@override
|
||
String toString() {
|
||
final buffer = StringBuffer();
|
||
buffer.write('[$type] $path: $message');
|
||
if (suggestion != null) {
|
||
buffer.write(' (建议: $suggestion)');
|
||
}
|
||
return buffer.toString();
|
||
}
|
||
}
|
||
|
||
/// 验证警告
|
||
class ValidationWarning {
|
||
final String path;
|
||
final String message;
|
||
final String? suggestion;
|
||
|
||
const ValidationWarning({
|
||
required this.path,
|
||
required this.message,
|
||
this.suggestion,
|
||
});
|
||
|
||
@override
|
||
String toString() {
|
||
final buffer = StringBuffer();
|
||
buffer.write('[WARNING] $path: $message');
|
||
if (suggestion != null) {
|
||
buffer.write(' (建议: $suggestion)');
|
||
}
|
||
return buffer.toString();
|
||
}
|
||
}
|
||
|
||
/// 验证错误类型
|
||
enum ValidationErrorType {
|
||
required,
|
||
format,
|
||
type,
|
||
reference,
|
||
constraint,
|
||
compatibility,
|
||
security,
|
||
}
|
||
|
||
/// Schema 验证器
|
||
class SchemaValidator {
|
||
final List<ValidationError> _errors = [];
|
||
final List<ValidationWarning> _warnings = [];
|
||
|
||
/// 验证 OpenAPI 文档
|
||
ValidationResult validateDocument(SwaggerDocument document) {
|
||
_errors.clear();
|
||
_warnings.clear();
|
||
|
||
// 验证基本信息
|
||
_validateInfo(document);
|
||
|
||
// 验证服务器配置
|
||
_validateServers(document.servers);
|
||
|
||
// 验证路径
|
||
_validatePaths(document.paths);
|
||
|
||
// 验证组件
|
||
_validateComponents(document.components);
|
||
|
||
// 验证安全方案
|
||
_validateSecurity(document.security, document.components.securitySchemes);
|
||
|
||
return ValidationResult(
|
||
isValid: _errors.isEmpty,
|
||
errors: List.from(_errors),
|
||
warnings: List.from(_warnings),
|
||
);
|
||
}
|
||
|
||
/// 验证基本信息
|
||
void _validateInfo(SwaggerDocument document) {
|
||
if (document.title.isEmpty) {
|
||
_errors.add(const ValidationError(
|
||
path: 'info.title',
|
||
message: 'API 标题不能为空',
|
||
type: ValidationErrorType.required,
|
||
suggestion: '请提供有意义的 API 标题',
|
||
));
|
||
}
|
||
|
||
if (document.version.isEmpty) {
|
||
_errors.add(const ValidationError(
|
||
path: 'info.version',
|
||
message: 'API 版本不能为空',
|
||
type: ValidationErrorType.required,
|
||
suggestion: '请使用语义化版本号,如 "1.0.0"',
|
||
));
|
||
}
|
||
|
||
if (document.description.isEmpty) {
|
||
_warnings.add(const ValidationWarning(
|
||
path: 'info.description',
|
||
message: 'API 描述为空',
|
||
suggestion: '建议添加 API 的详细描述',
|
||
));
|
||
}
|
||
}
|
||
|
||
/// 验证服务器配置
|
||
void _validateServers(List<ApiServer> servers) {
|
||
if (servers.isEmpty) {
|
||
_warnings.add(const ValidationWarning(
|
||
path: 'servers',
|
||
message: '未定义服务器配置',
|
||
suggestion: '建议添加至少一个服务器配置',
|
||
));
|
||
return;
|
||
}
|
||
|
||
for (int i = 0; i < servers.length; i++) {
|
||
final server = servers[i];
|
||
final path = 'servers[$i]';
|
||
|
||
if (server.url.isEmpty) {
|
||
_errors.add(ValidationError(
|
||
path: '$path.url',
|
||
message: '服务器 URL 不能为空',
|
||
type: ValidationErrorType.required,
|
||
));
|
||
} else if (!_isValidUrl(server.url)) {
|
||
_errors.add(ValidationError(
|
||
path: '$path.url',
|
||
message: '服务器 URL 格式无效: ${server.url}',
|
||
type: ValidationErrorType.format,
|
||
suggestion: '请使用有效的 URL 格式,如 "https://api.example.com"',
|
||
));
|
||
}
|
||
|
||
// 验证服务器变量
|
||
server.variables.forEach((name, variable) {
|
||
if (variable.defaultValue.isEmpty) {
|
||
_errors.add(ValidationError(
|
||
path: '$path.variables.$name.default',
|
||
message: '服务器变量必须有默认值',
|
||
type: ValidationErrorType.required,
|
||
));
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
/// 验证路径
|
||
void _validatePaths(Map<String, ApiPath> paths) {
|
||
if (paths.isEmpty) {
|
||
_errors.add(const ValidationError(
|
||
path: 'paths',
|
||
message: 'API 文档必须包含至少一个路径',
|
||
type: ValidationErrorType.required,
|
||
));
|
||
return;
|
||
}
|
||
|
||
paths.forEach((pathPattern, path) {
|
||
final pathKey = 'paths["$pathPattern"][${path.method.value}]';
|
||
_validatePath(path, pathKey);
|
||
});
|
||
}
|
||
|
||
/// 验证单个路径
|
||
void _validatePath(ApiPath path, String pathKey) {
|
||
// 验证操作 ID
|
||
if (path.operationId.isEmpty) {
|
||
_warnings.add(ValidationWarning(
|
||
path: '$pathKey.operationId',
|
||
message: '缺少操作 ID',
|
||
suggestion: '建议为每个操作添加唯一的 operationId',
|
||
));
|
||
}
|
||
|
||
// 验证摘要和描述
|
||
if (path.summary.isEmpty) {
|
||
_warnings.add(ValidationWarning(
|
||
path: '$pathKey.summary',
|
||
message: '缺少操作摘要',
|
||
suggestion: '建议添加简短的操作描述',
|
||
));
|
||
}
|
||
|
||
// 验证参数
|
||
for (int i = 0; i < path.parameters.length; i++) {
|
||
_validateParameter(path.parameters[i], '$pathKey.parameters[$i]');
|
||
}
|
||
|
||
// 验证请求体
|
||
if (path.requestBody != null) {
|
||
_validateRequestBody(path.requestBody!, '$pathKey.requestBody');
|
||
}
|
||
|
||
// 验证响应
|
||
if (path.responses.isEmpty) {
|
||
_errors.add(ValidationError(
|
||
path: '$pathKey.responses',
|
||
message: '操作必须定义至少一个响应',
|
||
type: ValidationErrorType.required,
|
||
));
|
||
} else {
|
||
path.responses.forEach((code, response) {
|
||
_validateResponse(response, '$pathKey.responses["$code"]');
|
||
});
|
||
}
|
||
|
||
// 验证安全要求
|
||
for (int i = 0; i < path.security.length; i++) {
|
||
_validateSecurityRequirement(path.security[i], '$pathKey.security[$i]');
|
||
}
|
||
}
|
||
|
||
/// 验证参数
|
||
void _validateParameter(ApiParameter parameter, String path) {
|
||
if (parameter.name.isEmpty) {
|
||
_errors.add(ValidationError(
|
||
path: '$path.name',
|
||
message: '参数名称不能为空',
|
||
type: ValidationErrorType.required,
|
||
));
|
||
}
|
||
|
||
// 验证路径参数必须是必需的
|
||
if (parameter.location == ParameterLocation.path && !parameter.required) {
|
||
_errors.add(ValidationError(
|
||
path: '$path.required',
|
||
message: '路径参数必须是必需的',
|
||
type: ValidationErrorType.constraint,
|
||
));
|
||
}
|
||
|
||
// 验证参数类型
|
||
if (parameter.type == PropertyType.unknown) {
|
||
_warnings.add(ValidationWarning(
|
||
path: '$path.type',
|
||
message: '参数类型未知',
|
||
suggestion: '建议明确指定参数类型',
|
||
));
|
||
}
|
||
}
|
||
|
||
/// 验证请求体
|
||
void _validateRequestBody(ApiRequestBody requestBody, String path) {
|
||
if (requestBody.content.isEmpty) {
|
||
_errors.add(ValidationError(
|
||
path: '$path.content',
|
||
message: '请求体必须定义至少一种内容类型',
|
||
type: ValidationErrorType.required,
|
||
));
|
||
}
|
||
|
||
requestBody.content.forEach((mediaType, content) {
|
||
_validateMediaType(content, '$path.content["$mediaType"]', mediaType);
|
||
});
|
||
}
|
||
|
||
/// 验证响应
|
||
void _validateResponse(ApiResponse response, String path) {
|
||
if (response.description.isEmpty) {
|
||
_warnings.add(ValidationWarning(
|
||
path: '$path.description',
|
||
message: '响应缺少描述',
|
||
suggestion: '建议为响应添加描述',
|
||
));
|
||
}
|
||
|
||
response.content.forEach((mediaType, content) {
|
||
_validateMediaType(content, '$path.content["$mediaType"]', mediaType);
|
||
});
|
||
}
|
||
|
||
/// 验证媒体类型
|
||
void _validateMediaType(
|
||
ApiMediaType mediaType, String path, String contentType) {
|
||
// 验证 schema
|
||
if (mediaType.schema == null) {
|
||
_warnings.add(ValidationWarning(
|
||
path: '$path.schema',
|
||
message: '媒体类型缺少 schema 定义',
|
||
suggestion: '建议为媒体类型添加 schema',
|
||
));
|
||
}
|
||
|
||
// 验证编码(仅适用于 multipart 和 form data)
|
||
if (contentType.startsWith('multipart/') || contentType.contains('form')) {
|
||
if (mediaType.encoding.isEmpty) {
|
||
_warnings.add(ValidationWarning(
|
||
path: '$path.encoding',
|
||
message: '表单数据建议定义编码信息',
|
||
suggestion: '为文件上传字段添加 contentType 等编码信息',
|
||
));
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 验证组件
|
||
void _validateComponents(ApiComponents? components) {
|
||
if (components == null) return;
|
||
|
||
// 验证 schemas
|
||
components.schemas.forEach((name, model) {
|
||
_validateModel(model, 'components.schemas["$name"]');
|
||
});
|
||
|
||
// 验证安全方案
|
||
components.securitySchemes.forEach((name, scheme) {
|
||
_validateSecurityScheme(scheme, 'components.securitySchemes["$name"]');
|
||
});
|
||
}
|
||
|
||
/// 验证模型
|
||
void _validateModel(ApiModel model, String path) {
|
||
if (model.name.isEmpty) {
|
||
_errors.add(ValidationError(
|
||
path: '$path.name',
|
||
message: '模型名称不能为空',
|
||
type: ValidationErrorType.required,
|
||
));
|
||
}
|
||
|
||
// 验证属性
|
||
model.properties.forEach((name, property) {
|
||
_validateProperty(property, '$path.properties["$name"]');
|
||
});
|
||
|
||
// 验证必需字段
|
||
for (final requiredField in model.required) {
|
||
if (!model.properties.containsKey(requiredField)) {
|
||
_errors.add(ValidationError(
|
||
path: '$path.required',
|
||
message: '必需字段 "$requiredField" 在属性中未定义',
|
||
type: ValidationErrorType.reference,
|
||
));
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 验证属性
|
||
void _validateProperty(ApiProperty property, String path) {
|
||
if (property.name.isEmpty) {
|
||
_errors.add(ValidationError(
|
||
path: '$path.name',
|
||
message: '属性名称不能为空',
|
||
type: ValidationErrorType.required,
|
||
));
|
||
}
|
||
|
||
if (property.type == PropertyType.unknown) {
|
||
_warnings.add(ValidationWarning(
|
||
path: '$path.type',
|
||
message: '属性类型未知',
|
||
suggestion: '建议明确指定属性类型',
|
||
));
|
||
}
|
||
}
|
||
|
||
/// 验证安全方案
|
||
void _validateSecurity(List<ApiSecurityRequirement> security,
|
||
Map<String, ApiSecurityScheme> schemes) {
|
||
for (int i = 0; i < security.length; i++) {
|
||
_validateSecurityRequirement(security[i], 'security[$i]');
|
||
}
|
||
}
|
||
|
||
/// 验证安全要求
|
||
void _validateSecurityRequirement(
|
||
ApiSecurityRequirement requirement, String path) {
|
||
for (final schemeName in requirement.schemeNames) {
|
||
// 这里应该验证安全方案是否在 components.securitySchemes 中定义
|
||
// 但由于当前模型结构限制,我们只能添加警告
|
||
if (schemeName.isEmpty) {
|
||
_warnings.add(ValidationWarning(
|
||
path: path,
|
||
message: '安全方案名称为空',
|
||
suggestion: '请确保安全方案名称有效',
|
||
));
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 验证安全方案
|
||
void _validateSecurityScheme(ApiSecurityScheme scheme, String path) {
|
||
switch (scheme.type) {
|
||
case SecuritySchemeType.apiKey:
|
||
if (scheme.name == null || scheme.name!.isEmpty) {
|
||
_errors.add(ValidationError(
|
||
path: '$path.name',
|
||
message: 'API Key 安全方案必须指定参数名称',
|
||
type: ValidationErrorType.required,
|
||
));
|
||
}
|
||
break;
|
||
case SecuritySchemeType.http:
|
||
if (scheme.scheme == null || scheme.scheme!.isEmpty) {
|
||
_errors.add(ValidationError(
|
||
path: '$path.scheme',
|
||
message: 'HTTP 安全方案必须指定认证方案',
|
||
type: ValidationErrorType.required,
|
||
));
|
||
}
|
||
break;
|
||
case SecuritySchemeType.oauth2:
|
||
if (scheme.flows == null) {
|
||
_errors.add(ValidationError(
|
||
path: '$path.flows',
|
||
message: 'OAuth2 安全方案必须定义流程',
|
||
type: ValidationErrorType.required,
|
||
));
|
||
}
|
||
break;
|
||
case SecuritySchemeType.openIdConnect:
|
||
if (scheme.openIdConnectUrl == null ||
|
||
scheme.openIdConnectUrl!.isEmpty) {
|
||
_errors.add(ValidationError(
|
||
path: '$path.openIdConnectUrl',
|
||
message: 'OpenID Connect 安全方案必须指定 URL',
|
||
type: ValidationErrorType.required,
|
||
));
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
/// 验证 URL 格式
|
||
bool _isValidUrl(String url) {
|
||
try {
|
||
final uri = Uri.parse(url);
|
||
return uri.hasScheme && (uri.scheme == 'http' || uri.scheme == 'https');
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// 验证文档结构完整性
|
||
void validateDocumentStructure(SwaggerDocument document) {
|
||
_validateOpenApiVersion(document);
|
||
_validatePathStructure(document);
|
||
_validateComponentReferences(document);
|
||
_validateSecurityReferences(document);
|
||
_validateExampleConsistency(document);
|
||
_validateResponseStructure(document);
|
||
_validateParameterConsistency(document);
|
||
}
|
||
|
||
/// 验证 OpenAPI 版本
|
||
void _validateOpenApiVersion(SwaggerDocument document) {
|
||
// SwaggerDocument 没有直接的 openApiVersion 属性
|
||
// 这里我们假设它是 OpenAPI 3.0 兼容的
|
||
_warnings.add(const ValidationWarning(
|
||
path: 'openapi',
|
||
message: '无法验证 OpenAPI 版本',
|
||
suggestion: '确保使用 OpenAPI 3.0.x 或 3.1.x 版本',
|
||
));
|
||
}
|
||
|
||
/// 验证路径结构
|
||
void _validatePathStructure(SwaggerDocument document) {
|
||
final pathPatterns = document.paths.keys.toList();
|
||
|
||
// 检查路径冲突
|
||
for (int i = 0; i < pathPatterns.length; i++) {
|
||
for (int j = i + 1; j < pathPatterns.length; j++) {
|
||
if (_pathsConflict(pathPatterns[i], pathPatterns[j])) {
|
||
_errors.add(ValidationError(
|
||
path: 'paths',
|
||
message: '路径冲突: "${pathPatterns[i]}" 与 "${pathPatterns[j]}"',
|
||
type: ValidationErrorType.constraint,
|
||
suggestion: '确保路径模式不会产生歧义',
|
||
));
|
||
}
|
||
}
|
||
}
|
||
|
||
// 检查路径参数一致性
|
||
document.paths.forEach((pathPattern, path) {
|
||
final pathParams = _extractPathParameters(pathPattern);
|
||
|
||
final declaredParams = path.parameters
|
||
.where((p) => p.location == ParameterLocation.path)
|
||
.map((p) => p.name)
|
||
.toSet();
|
||
|
||
// 检查路径中的参数是否都有声明
|
||
for (final param in pathParams) {
|
||
if (!declaredParams.contains(param)) {
|
||
_errors.add(ValidationError(
|
||
path: 'paths["$pathPattern"][${path.method.value}].parameters',
|
||
message: '路径参数 "$param" 未在参数列表中声明',
|
||
type: ValidationErrorType.reference,
|
||
suggestion: '添加路径参数的声明',
|
||
));
|
||
}
|
||
}
|
||
|
||
// 检查声明的路径参数是否都在路径中使用
|
||
for (final param in declaredParams) {
|
||
if (!pathParams.contains(param)) {
|
||
_warnings.add(ValidationWarning(
|
||
path: 'paths["$pathPattern"][${path.method.value}].parameters',
|
||
message: '声明的路径参数 "$param" 未在路径中使用',
|
||
suggestion: '移除未使用的参数声明或修正路径',
|
||
));
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/// 验证组件引用
|
||
void _validateComponentReferences(SwaggerDocument document) {
|
||
final schemas = document.components.schemas.keys.toSet();
|
||
final securitySchemes = document.components.securitySchemes.keys.toSet();
|
||
|
||
// 收集所有引用
|
||
final schemaRefs = <String>{};
|
||
final securityRefs = <String>{};
|
||
|
||
_collectReferences(document, schemaRefs, securityRefs);
|
||
|
||
// 检查未定义的引用
|
||
for (final ref in schemaRefs) {
|
||
if (!schemas.contains(ref)) {
|
||
_errors.add(ValidationError(
|
||
path: 'components.schemas',
|
||
message: '引用的 schema "$ref" 未定义',
|
||
type: ValidationErrorType.reference,
|
||
suggestion: '定义缺失的 schema 或修正引用',
|
||
));
|
||
}
|
||
}
|
||
|
||
for (final ref in securityRefs) {
|
||
if (!securitySchemes.contains(ref)) {
|
||
_errors.add(ValidationError(
|
||
path: 'components.securitySchemes',
|
||
message: '引用的安全方案 "$ref" 未定义',
|
||
type: ValidationErrorType.reference,
|
||
suggestion: '定义缺失的安全方案或修正引用',
|
||
));
|
||
}
|
||
}
|
||
|
||
// 检查未使用的组件
|
||
for (final schema in schemas) {
|
||
if (!schemaRefs.contains(schema)) {
|
||
_warnings.add(ValidationWarning(
|
||
path: 'components.schemas["$schema"]',
|
||
message: 'Schema "$schema" 已定义但未被使用',
|
||
suggestion: '移除未使用的 schema 或添加引用',
|
||
));
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 验证安全方案引用
|
||
void _validateSecurityReferences(SwaggerDocument document) {
|
||
final definedSchemes = document.components.securitySchemes.keys.toSet();
|
||
|
||
// 检查全局安全要求
|
||
for (int i = 0; i < document.security.length; i++) {
|
||
final requirement = document.security[i];
|
||
for (final schemeName in requirement.schemeNames) {
|
||
if (!definedSchemes.contains(schemeName)) {
|
||
_errors.add(ValidationError(
|
||
path: 'security[$i]',
|
||
message: '引用的安全方案 "$schemeName" 未定义',
|
||
type: ValidationErrorType.reference,
|
||
suggestion: '在 components.securitySchemes 中定义该安全方案',
|
||
));
|
||
}
|
||
}
|
||
}
|
||
|
||
// 检查操作级别的安全要求
|
||
document.paths.forEach((pathPattern, path) {
|
||
for (int i = 0; i < path.security.length; i++) {
|
||
final requirement = path.security[i];
|
||
for (final schemeName in requirement.schemeNames) {
|
||
if (!definedSchemes.contains(schemeName)) {
|
||
_errors.add(ValidationError(
|
||
path: 'paths["$pathPattern"][${path.method.value}].security[$i]',
|
||
message: '引用的安全方案 "$schemeName" 未定义',
|
||
type: ValidationErrorType.reference,
|
||
suggestion: '在 components.securitySchemes 中定义该安全方案',
|
||
));
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/// 验证示例一致性
|
||
void _validateExampleConsistency(SwaggerDocument document) {
|
||
document.paths.forEach((pathPattern, path) {
|
||
// 验证请求体示例
|
||
if (path.requestBody != null) {
|
||
path.requestBody!.content.forEach((mediaType, content) {
|
||
_validateMediaTypeExamples(content,
|
||
'$pathPattern[${path.method.value}].requestBody.content["$mediaType"]');
|
||
});
|
||
}
|
||
|
||
// 验证响应示例
|
||
path.responses.forEach((code, response) {
|
||
response.content.forEach((mediaType, content) {
|
||
_validateMediaTypeExamples(content,
|
||
'$pathPattern[${path.method.value}].responses["$code"].content["$mediaType"]');
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
/// 验证媒体类型示例
|
||
void _validateMediaTypeExamples(ApiMediaType mediaType, String path) {
|
||
// 检查 example 和 examples 不能同时存在
|
||
if (mediaType.example != null && mediaType.examples.isNotEmpty) {
|
||
_warnings.add(ValidationWarning(
|
||
path: path,
|
||
message: 'example 和 examples 不应同时存在',
|
||
suggestion: '使用 examples 对象来提供多个示例',
|
||
));
|
||
}
|
||
|
||
// 验证示例格式
|
||
if (mediaType.example != null && mediaType.schema != null) {
|
||
// TODO: 根据 schema 验证 example 的格式
|
||
}
|
||
}
|
||
|
||
/// 验证响应结构
|
||
void _validateResponseStructure(SwaggerDocument document) {
|
||
document.paths.forEach((pathPattern, path) {
|
||
// 检查是否有成功响应
|
||
final hasSuccessResponse = path.responses.keys.any((code) {
|
||
final statusCode = int.tryParse(code) ?? 0;
|
||
return statusCode >= 200 && statusCode < 300;
|
||
});
|
||
|
||
if (!hasSuccessResponse) {
|
||
_warnings.add(ValidationWarning(
|
||
path: 'paths["$pathPattern"][${path.method.value}].responses',
|
||
message: '缺少成功响应 (2xx)',
|
||
suggestion: '添加至少一个成功响应',
|
||
));
|
||
}
|
||
|
||
// 检查错误响应
|
||
final hasErrorResponse = path.responses.keys.any((code) {
|
||
final statusCode = int.tryParse(code) ?? 0;
|
||
return statusCode >= 400;
|
||
});
|
||
|
||
if (!hasErrorResponse) {
|
||
_warnings.add(ValidationWarning(
|
||
path: 'paths["$pathPattern"][${path.method.value}].responses',
|
||
message: '建议添加错误响应 (4xx/5xx)',
|
||
suggestion: '添加常见的错误响应,如 400、401、404、500',
|
||
));
|
||
}
|
||
});
|
||
}
|
||
|
||
/// 验证参数一致性
|
||
void _validateParameterConsistency(SwaggerDocument document) {
|
||
final parameterNames = <String, Set<String>>{};
|
||
|
||
document.paths.forEach((pathPattern, path) {
|
||
for (final param in path.parameters) {
|
||
final key = '${param.location.name}:${param.name}';
|
||
parameterNames.putIfAbsent(pathPattern, () => <String>{});
|
||
|
||
if (parameterNames[pathPattern]!.contains(key)) {
|
||
_errors.add(ValidationError(
|
||
path: 'paths["$pathPattern"][${path.method.value}].parameters',
|
||
message: '重复的参数: ${param.name} (${param.location.name})',
|
||
type: ValidationErrorType.constraint,
|
||
suggestion: '确保参数名称在同一位置类型中唯一',
|
||
));
|
||
} else {
|
||
parameterNames[pathPattern]!.add(key);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/// 检查路径是否冲突
|
||
bool _pathsConflict(String path1, String path2) {
|
||
if (path1 == path2) return true;
|
||
|
||
// 将路径参数替换为通配符进行比较
|
||
final normalized1 = path1.replaceAll(RegExp(r'\{[^}]+\}'), '*');
|
||
final normalized2 = path2.replaceAll(RegExp(r'\{[^}]+\}'), '*');
|
||
|
||
return normalized1 == normalized2;
|
||
}
|
||
|
||
/// 提取路径参数
|
||
Set<String> _extractPathParameters(String path) {
|
||
final regex = RegExp(r'\{([^}]+)\}');
|
||
final matches = regex.allMatches(path);
|
||
return matches.map((match) => match.group(1)!).toSet();
|
||
}
|
||
|
||
/// 收集所有引用
|
||
void _collectReferences(SwaggerDocument document, Set<String> schemaRefs,
|
||
Set<String> securityRefs) {
|
||
// 从路径中收集引用
|
||
document.paths.forEach((pathPattern, path) {
|
||
// 从参数中收集引用
|
||
for (final _ in path.parameters) {
|
||
// TODO: 收集参数 schema 引用
|
||
}
|
||
|
||
// 从请求体中收集引用
|
||
if (path.requestBody != null) {
|
||
path.requestBody!.content.forEach((mediaType, content) {
|
||
_collectSchemaReferences(content.schema, schemaRefs);
|
||
});
|
||
}
|
||
|
||
// 从响应中收集引用
|
||
path.responses.forEach((code, response) {
|
||
response.content.forEach((mediaType, content) {
|
||
_collectSchemaReferences(content.schema, schemaRefs);
|
||
});
|
||
});
|
||
|
||
// 从安全要求中收集引用
|
||
for (final requirement in path.security) {
|
||
securityRefs.addAll(requirement.schemeNames);
|
||
}
|
||
});
|
||
|
||
// 从全局安全要求中收集引用
|
||
for (final requirement in document.security) {
|
||
securityRefs.addAll(requirement.schemeNames);
|
||
}
|
||
|
||
// 从组件中收集引用
|
||
document.components.schemas.forEach((name, model) {
|
||
for (final _ in model.properties.values) {
|
||
// TODO: 收集属性 schema 引用
|
||
}
|
||
});
|
||
}
|
||
|
||
/// 收集 Schema 引用
|
||
void _collectSchemaReferences(
|
||
Map<String, dynamic>? schema, Set<String> refs) {
|
||
if (schema == null) return;
|
||
|
||
// 检查 $ref
|
||
final ref = schema['\$ref'] as String?;
|
||
if (ref != null && ref.startsWith('#/components/schemas/')) {
|
||
final refName = ref.substring('#/components/schemas/'.length);
|
||
refs.add(refName);
|
||
}
|
||
|
||
// 递归检查嵌套 schema
|
||
if (schema['items'] is Map<String, dynamic>) {
|
||
_collectSchemaReferences(schema['items'] as Map<String, dynamic>, refs);
|
||
}
|
||
|
||
if (schema['properties'] is Map<String, dynamic>) {
|
||
final properties = schema['properties'] as Map<String, dynamic>;
|
||
properties.forEach((key, value) {
|
||
if (value is Map<String, dynamic>) {
|
||
_collectSchemaReferences(value, refs);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 检查组合 schema
|
||
for (final key in ['allOf', 'oneOf', 'anyOf']) {
|
||
if (schema[key] is List) {
|
||
final list = schema[key] as List;
|
||
for (final item in list) {
|
||
if (item is Map<String, dynamic>) {
|
||
_collectSchemaReferences(item, refs);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|