376 lines
11 KiB
Dart
376 lines
11 KiB
Dart
import 'package:swagger_generator_flutter/core/models.dart';
|
||
import 'package:swagger_generator_flutter/validators/core/validation_context.dart';
|
||
import 'package:swagger_generator_flutter/validators/core/validation_result.dart';
|
||
import 'package:swagger_generator_flutter/validators/core/validation_rule.dart';
|
||
|
||
/// 文档结构验证规则
|
||
class StructureValidationRule extends ValidationRule {
|
||
@override
|
||
String get id => 'structure_validation';
|
||
|
||
@override
|
||
String get name => '文档结构验证';
|
||
|
||
@override
|
||
ValidationResult validate(ValidationContext context) {
|
||
final document = context.document;
|
||
final errors = <ValidationError>[];
|
||
final warnings = <ValidationWarning>[];
|
||
|
||
_validatePathStructure(document, errors, warnings);
|
||
_validateComponentReferences(document, errors, warnings);
|
||
_validateSecurityReferences(document, errors, warnings);
|
||
|
||
if (context.options.validateExamples) {
|
||
_validateExampleConsistency(document, errors, warnings);
|
||
}
|
||
|
||
return ValidationResult(
|
||
isValid: errors.isEmpty,
|
||
errors: errors,
|
||
warnings: warnings,
|
||
);
|
||
}
|
||
|
||
void _validatePathStructure(
|
||
SwaggerDocument document,
|
||
List<ValidationError> errors,
|
||
List<ValidationWarning> warnings,
|
||
) {
|
||
final pathPatterns =
|
||
document.paths.keys.map((key) => key.pattern).toSet().toList();
|
||
|
||
// 检查路径冲突
|
||
for (var i = 0; i < pathPatterns.length; i++) {
|
||
for (var 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((routeKey, path) {
|
||
final pattern = routeKey.pattern;
|
||
final pathParams = _extractPathParameters(pattern);
|
||
|
||
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["$pattern"][${path.method.value}].parameters',
|
||
message: '路径参数 "$param" 未在参数列表中声明',
|
||
type: ValidationErrorType.reference,
|
||
suggestion: '添加路径参数的声明',
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// 检查声明的路径参数是否都在路径中使用
|
||
for (final param in declaredParams) {
|
||
if (!pathParams.contains(param)) {
|
||
warnings.add(
|
||
ValidationWarning(
|
||
path: 'paths["$pattern"][${path.method.value}].parameters',
|
||
message: '声明的路径参数 "$param" 未在路径中使用',
|
||
suggestion: '移除未使用的参数声明或修正路径',
|
||
),
|
||
);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
void _validateComponentReferences(
|
||
SwaggerDocument document,
|
||
List<ValidationError> errors,
|
||
List<ValidationWarning> warnings,
|
||
) {
|
||
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,
|
||
List<ValidationError> errors,
|
||
List<ValidationWarning> warnings,
|
||
) {
|
||
final definedSchemes = document.components.securitySchemes.keys.toSet();
|
||
|
||
// 检查全局安全要求
|
||
for (var 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((routeKey, path) {
|
||
final pattern = routeKey.pattern;
|
||
for (var 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["$pattern"][${path.method.value}].security[$i]',
|
||
message: '引用的安全方案 "$schemeName" 未定义',
|
||
type: ValidationErrorType.reference,
|
||
suggestion: '在 components.securitySchemes 中定义该安全方案',
|
||
),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
void _validateExampleConsistency(
|
||
SwaggerDocument document,
|
||
List<ValidationError> errors,
|
||
List<ValidationWarning> warnings,
|
||
) {
|
||
document.paths.forEach((routeKey, path) {
|
||
final pattern = routeKey.pattern;
|
||
// 验证请求体示例
|
||
if (path.requestBody != null) {
|
||
for (final entry in path.requestBody!.content.entries) {
|
||
_validateMediaTypeExamples(
|
||
entry.value,
|
||
'$pattern[${path.method.value}]'
|
||
'.requestBody.content["${entry.key}"]',
|
||
errors,
|
||
warnings,
|
||
);
|
||
}
|
||
}
|
||
|
||
// 验证响应示例
|
||
for (final responseEntry in path.responses.entries) {
|
||
for (final contentEntry in responseEntry.value.content.entries) {
|
||
_validateMediaTypeExamples(
|
||
contentEntry.value,
|
||
'$pattern[${path.method.value}]'
|
||
'.responses["${responseEntry.key}"].content["${contentEntry.key}"]',
|
||
errors,
|
||
warnings,
|
||
);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
void _validateMediaTypeExamples(
|
||
ApiMediaType mediaType,
|
||
String path,
|
||
List<ValidationError> errors,
|
||
List<ValidationWarning> warnings,
|
||
) {
|
||
// 检查 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) {
|
||
_validateExampleAgainstSchema(
|
||
mediaType.example,
|
||
mediaType.schema!,
|
||
'$path.example',
|
||
errors,
|
||
warnings,
|
||
);
|
||
}
|
||
}
|
||
|
||
void _validateExampleAgainstSchema(
|
||
dynamic example,
|
||
Map<String, dynamic> schema,
|
||
String path,
|
||
List<ValidationError> errors,
|
||
List<ValidationWarning> warnings,
|
||
) {
|
||
if (schema.containsKey(r'$ref')) {
|
||
// 引用在此处无法解析,跳过验证
|
||
return;
|
||
}
|
||
|
||
final nullable = schema['nullable'] == true;
|
||
if (example == null) {
|
||
if (!nullable) {
|
||
errors.add(
|
||
ValidationError(
|
||
path: path,
|
||
message: '示例值为 null,但 schema 不允许 null',
|
||
type: ValidationErrorType.type,
|
||
suggestion: '更新 example 或在 schema 中标记 nullable',
|
||
),
|
||
);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// TODO: 实现更完整的示例验证逻辑
|
||
}
|
||
|
||
bool _pathsConflict(String path1, String path2) {
|
||
// 简单的冲突检测:将参数替换为 {} 后比较
|
||
final p1 = path1.replaceAll(RegExp(r'\{[^}]+\}'), '{}');
|
||
final p2 = path2.replaceAll(RegExp(r'\{[^}]+\}'), '{}');
|
||
return p1 == p2;
|
||
}
|
||
|
||
Set<String> _extractPathParameters(String path) {
|
||
final regex = RegExp(r'\{(\w+)\}');
|
||
final matches = regex.allMatches(path);
|
||
return matches.map((match) => match.group(1)!).toSet();
|
||
}
|
||
|
||
void _collectReferences(
|
||
SwaggerDocument document,
|
||
Set<String> schemaRefs,
|
||
Set<String> securityRefs,
|
||
) {
|
||
// 收集路径中的引用
|
||
for (final path in document.paths.values) {
|
||
// ApiParameter 目前没有暴露 schema 属性,暂时无法收集参数中的引用
|
||
// for (final param in path.parameters) {
|
||
// if (param.schema != null) {
|
||
// _collectSchemaRefs(param.schema!, schemaRefs);
|
||
// }
|
||
// }
|
||
|
||
if (path.requestBody != null) {
|
||
for (final mediaType in path.requestBody!.content.values) {
|
||
if (mediaType.schema != null) {
|
||
_collectSchemaRefs(mediaType.schema!, schemaRefs);
|
||
}
|
||
}
|
||
}
|
||
|
||
for (final response in path.responses.values) {
|
||
for (final mediaType in response.content.values) {
|
||
if (mediaType.schema != null) {
|
||
_collectSchemaRefs(mediaType.schema!, schemaRefs);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 收集组件中的引用
|
||
for (final model in document.components.schemas.values) {
|
||
for (final property in model.properties.values) {
|
||
if (property.type == PropertyType.reference &&
|
||
property.reference != null) {
|
||
schemaRefs.add(property.reference!);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
void _collectSchemaRefs(
|
||
Map<String, dynamic> schema,
|
||
Set<String> refs,
|
||
) {
|
||
if (schema.containsKey(r'$ref')) {
|
||
final ref = schema[r'$ref'] as String;
|
||
// 假设引用格式为 #/components/schemas/Name
|
||
final parts = ref.split('/');
|
||
if (parts.length >= 4 &&
|
||
parts[1] == 'components' &&
|
||
parts[2] == 'schemas') {
|
||
refs.add(parts.last);
|
||
}
|
||
}
|
||
|
||
// 递归检查 items, properties 等
|
||
if (schema.containsKey('items')) {
|
||
_collectSchemaRefs(schema['items'] as Map<String, dynamic>, refs);
|
||
}
|
||
if (schema.containsKey('properties')) {
|
||
final props = schema['properties'] as Map<String, dynamic>;
|
||
for (final value in props.values) {
|
||
if (value is Map<String, dynamic>) {
|
||
_collectSchemaRefs(value, refs);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|