/// Schema 验证器 /// 验证 OpenAPI 3.0 文档的完整性和正确性 library; import 'package:swagger_generator_flutter/core/models.dart'; /// Schema 验证结果 class ValidationResult { const ValidationResult({ required this.isValid, this.errors = const [], this.warnings = const [], }); /// 创建成功的验证结果 factory ValidationResult.success({ List warnings = const [], }) { return ValidationResult( isValid: true, warnings: warnings, ); } /// 创建失败的验证结果 factory ValidationResult.failure( List errors, { List warnings = const [], }) { return ValidationResult( isValid: false, errors: errors, warnings: warnings, ); } final bool isValid; final List errors; final List warnings; /// 是否有警告 bool get hasWarnings => warnings.isNotEmpty; /// 是否有错误 bool get hasErrors => errors.isNotEmpty; } /// 验证错误 class ValidationError { const ValidationError({ required this.path, required this.message, required this.type, this.suggestion, }); final String path; final String message; final ValidationErrorType type; final String? suggestion; @override String toString() { final buffer = StringBuffer()..write('[$type] $path: $message'); if (suggestion != null) { buffer.write(' (建议: $suggestion)'); } return buffer.toString(); } } /// 验证警告 class ValidationWarning { const ValidationWarning({ required this.path, required this.message, this.suggestion, }); final String path; final String message; final String? suggestion; @override String toString() { final buffer = StringBuffer()..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 _errors = []; final List _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 servers) { if (servers.isEmpty) { _warnings.add( const ValidationWarning( path: 'servers', message: '未定义服务器配置', suggestion: '建议添加至少一个服务器配置', ), ); return; } for (var 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 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 (var 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 (var 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 security, Map schemes, ) { for (var 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, ), ); } case SecuritySchemeType.http: if (scheme.scheme == null || scheme.scheme!.isEmpty) { _errors.add( ValidationError( path: '$path.scheme', message: 'HTTP 安全方案必须指定认证方案', type: ValidationErrorType.required, ), ); } case SecuritySchemeType.oauth2: if (scheme.flows == null) { _errors.add( ValidationError( path: '$path.flows', message: 'OAuth2 安全方案必须定义流程', type: ValidationErrorType.required, ), ); } case SecuritySchemeType.openIdConnect: if (scheme.openIdConnectUrl == null || scheme.openIdConnectUrl!.isEmpty) { _errors.add( ValidationError( path: '$path.openIdConnectUrl', message: 'OpenID Connect 安全方案必须指定 URL', type: ValidationErrorType.required, ), ); } } } /// 验证 URL 格式 bool _isValidUrl(String url) { try { final uri = Uri.parse(url); return uri.hasScheme && (uri.scheme == 'http' || uri.scheme == 'https'); } on Object { 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 (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((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 = {}; final securityRefs = {}; _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 (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((pathPattern, path) { 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["$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(max): 根据 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 = >{}; document.paths.forEach((pathPattern, path) { for (final param in path.parameters) { final key = '${param.location.name}:${param.name}'; parameterNames.putIfAbsent(pathPattern, () => {}); 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 _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 schemaRefs, Set securityRefs, ) { // 从路径中收集引用 document.paths.forEach((pathPattern, path) { // 从参数中收集引用 for (final _ in path.parameters) { // TODO(max): 收集参数 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(max): 收集属性 schema 引用 } }); } /// 收集 Schema 引用 void _collectSchemaReferences( Map? schema, Set refs, ) { if (schema == null) return; // 检查 $ref final ref = schema[r'$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) { _collectSchemaReferences(schema['items'] as Map, refs); } if (schema['properties'] is Map) { final properties = schema['properties'] as Map; properties.forEach((key, value) { if (value is Map) { _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) { _collectSchemaReferences(item, refs); } } } } } }