swagger_generator_flutter/lib/validators/schema_validator.dart

846 lines
25 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/// 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);
}
}
}
}
}
}