方案 C(激进)Phase 2:物理迁移(首批)
- parse:迁移 swagger_fetcher / swagger_data_parser 至 lib/pipeline/parse/impl/*,原位置保留兼容导出 - validate:迁移 schema_validator / enhanced_validator 至 lib/pipeline/validate/impl/*,原位置保留兼容导出 - pipeline 层维持 re-export,确保外部与内部导入路径均可用 质量门禁: - dart analyze:0 error / 0 warning(仅 info) - dart test:全部通过(203/203) 后续计划: - 视风险逐步评估 generate/render/output 的物理迁移(含 part 文件),保持每步可回滚
This commit is contained in:
parent
a9de0e72d9
commit
ceab0b6f19
|
|
@ -1,567 +1,5 @@
|
||||||
import 'dart:convert';
|
/// Backward-compat shim for SwaggerDataParser
|
||||||
|
library;
|
||||||
|
|
||||||
import 'package:swagger_generator_flutter/core/config.dart';
|
export 'package:swagger_generator_flutter/pipeline/parse/impl/swagger_data_parser.dart';
|
||||||
import 'package:swagger_generator_flutter/core/exceptions.dart';
|
|
||||||
import 'package:swagger_generator_flutter/core/models.dart';
|
|
||||||
import 'package:swagger_generator_flutter/parsers/swagger_fetcher.dart';
|
|
||||||
import 'package:swagger_generator_flutter/utils/cache_manager.dart';
|
|
||||||
import 'package:swagger_generator_flutter/utils/logger.dart';
|
|
||||||
import 'package:swagger_generator_flutter/utils/performance_monitor.dart';
|
|
||||||
import 'package:swagger_generator_flutter/utils/reference_resolver.dart';
|
|
||||||
import 'package:swagger_generator_flutter/utils/string_utils.dart';
|
|
||||||
|
|
||||||
/// Swagger数据解析器
|
|
||||||
/// 负责解析Swagger JSON文档并提取相关信息
|
|
||||||
class SwaggerDataParser {
|
|
||||||
SwaggerDataParser({SwaggerFetcher? fetcher})
|
|
||||||
: _fetcher = fetcher ?? SwaggerFetcher(),
|
|
||||||
_cacheManager = CacheManager(),
|
|
||||||
_performanceMonitor = PerformanceMonitor();
|
|
||||||
|
|
||||||
final SwaggerFetcher _fetcher;
|
|
||||||
final CacheManager _cacheManager;
|
|
||||||
final PerformanceMonitor _performanceMonitor;
|
|
||||||
|
|
||||||
// 缓存解析结果
|
|
||||||
final Map<String, SwaggerDocument> _cachedDocuments = {};
|
|
||||||
|
|
||||||
/// 获取并解析Swagger JSON文档
|
|
||||||
/// [url] 可选参数,如果不传则使用配置中的第一个 URL
|
|
||||||
Future<SwaggerDocument> fetchAndParseSwaggerDocument([String? url]) async {
|
|
||||||
final swaggerUrl = url ?? SwaggerConfig.swaggerJsonUrls.first;
|
|
||||||
|
|
||||||
// 如果有缓存,直接返回缓存结果
|
|
||||||
if (_cachedDocuments.containsKey(swaggerUrl)) {
|
|
||||||
appLogger.info('📦 使用缓存的文档: $swaggerUrl');
|
|
||||||
return _cachedDocuments[swaggerUrl]!;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _performanceMonitor.measure(
|
|
||||||
'fetchAndParseSwaggerDocument',
|
|
||||||
() async {
|
|
||||||
try {
|
|
||||||
// 使用 Fetcher 获取内容
|
|
||||||
final content = await _fetcher.fetch(swaggerUrl);
|
|
||||||
|
|
||||||
// 解析 JSON
|
|
||||||
final jsonData = json.decode(content) as Map<String, dynamic>;
|
|
||||||
|
|
||||||
// 解析文档
|
|
||||||
final document = await parseSwaggerDocument(jsonData, swaggerUrl);
|
|
||||||
|
|
||||||
// 更新缓存
|
|
||||||
_cachedDocuments[swaggerUrl] = document;
|
|
||||||
appLogger.info('✅ Swagger文档解析完成');
|
|
||||||
|
|
||||||
return document;
|
|
||||||
} on Object catch (e) {
|
|
||||||
if (e is SwaggerParseException) {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
throw SwaggerParseException(
|
|
||||||
'获取Swagger文档失败',
|
|
||||||
url: swaggerUrl,
|
|
||||||
details: e.toString(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 解析Swagger JSON文档
|
|
||||||
Future<SwaggerDocument> parseSwaggerDocument(
|
|
||||||
Map<String, dynamic> jsonData, [
|
|
||||||
String? sourceUrl,
|
|
||||||
]) async {
|
|
||||||
return _performanceMonitor.measure('parseSwaggerDocument', () async {
|
|
||||||
// 计算内容哈希作为缓存键
|
|
||||||
final contentHash = json.encode(jsonData).hashCode.toString();
|
|
||||||
final cacheKey = 'swagger_doc_$contentHash';
|
|
||||||
|
|
||||||
// 尝试从缓存获取
|
|
||||||
final cachedResult = _cacheManager.get<SwaggerDocument>(cacheKey);
|
|
||||||
if (cachedResult != null) {
|
|
||||||
if (sourceUrl != null) {
|
|
||||||
_cachedDocuments[sourceUrl] = cachedResult;
|
|
||||||
}
|
|
||||||
return cachedResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析文档基本信息
|
|
||||||
final info = jsonData['info'] as Map<String, dynamic>? ?? {};
|
|
||||||
final title = info['title'] as String? ?? 'API Documentation';
|
|
||||||
final version = info['version'] as String? ?? '1.0.0';
|
|
||||||
final description = info['description'] as String? ?? '';
|
|
||||||
|
|
||||||
// ✨ 分析 schema 使用情况(在解析 components 之前)
|
|
||||||
final schemaUsage = _analyzeSchemaUsage(jsonData);
|
|
||||||
|
|
||||||
// 解析 servers (OpenAPI 3.0)
|
|
||||||
final servers = _parseServers(jsonData);
|
|
||||||
|
|
||||||
// 解析 components (OpenAPI 3.0),传入使用情况分析结果
|
|
||||||
final components = _parseComponents(jsonData, schemaUsage);
|
|
||||||
|
|
||||||
// 解析tags信息 (用于获取控制器描述)
|
|
||||||
final tagsInfo = _parseTagsInfo(jsonData);
|
|
||||||
|
|
||||||
// 解析API路径
|
|
||||||
final paths = _parseApiPaths(jsonData);
|
|
||||||
|
|
||||||
// 解析API模型 (从 components 中提取)
|
|
||||||
final models = components.schemas;
|
|
||||||
|
|
||||||
// 解析API控制器 (传入tags信息)
|
|
||||||
final controllers = _parseApiControllers(paths, tagsInfo);
|
|
||||||
|
|
||||||
final document = SwaggerDocument(
|
|
||||||
title: title,
|
|
||||||
version: version,
|
|
||||||
description: description,
|
|
||||||
servers: servers,
|
|
||||||
components: components,
|
|
||||||
paths: paths,
|
|
||||||
models: models,
|
|
||||||
controllers: controllers,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 缓存结果
|
|
||||||
_cacheManager.put(cacheKey, document);
|
|
||||||
if (sourceUrl != null) {
|
|
||||||
_cachedDocuments[sourceUrl] = document;
|
|
||||||
}
|
|
||||||
|
|
||||||
return document;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 解析 servers 配置 (OpenAPI 3.0)
|
|
||||||
List<ApiServer> _parseServers(Map<String, dynamic> jsonData) {
|
|
||||||
final servers = <ApiServer>[];
|
|
||||||
|
|
||||||
try {
|
|
||||||
final serversJson = jsonData['servers'] as List<dynamic>?;
|
|
||||||
if (serversJson != null) {
|
|
||||||
for (final serverJson in serversJson) {
|
|
||||||
if (serverJson is Map<String, dynamic>) {
|
|
||||||
final server = ApiServer.fromJson(serverJson);
|
|
||||||
servers.add(server);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有 servers 配置,提供默认值
|
|
||||||
if (servers.isEmpty) {
|
|
||||||
servers.add(const ApiServer(url: '/'));
|
|
||||||
}
|
|
||||||
} on Object catch (e) {
|
|
||||||
appLogger.warning('⚠️ 解析servers配置时发生错误: $e');
|
|
||||||
// 提供默认服务器配置
|
|
||||||
servers.add(const ApiServer(url: '/'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return servers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 解析 components 配置 (OpenAPI 3.0)
|
|
||||||
ApiComponents _parseComponents(
|
|
||||||
Map<String, dynamic> jsonData,
|
|
||||||
Map<String, Set<ModelUsageType>> schemaUsage,
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
final componentsJson = jsonData['components'] as Map<String, dynamic>?;
|
|
||||||
if (componentsJson != null) {
|
|
||||||
// 使用引用解析器处理复杂嵌套和循环引用
|
|
||||||
final resolver = ReferenceResolver();
|
|
||||||
final resolvedSchemas = resolver.resolveModels(componentsJson);
|
|
||||||
|
|
||||||
// ✨ 根据使用情况更新模型的 usageType
|
|
||||||
final schemasWithUsageType = <String, ApiModel>{};
|
|
||||||
resolvedSchemas.forEach((name, model) {
|
|
||||||
final usages = schemaUsage[name] ?? {};
|
|
||||||
final ModelUsageType usageType;
|
|
||||||
|
|
||||||
if (usages.isEmpty) {
|
|
||||||
usageType = ModelUsageType.unknown;
|
|
||||||
} else if (usages.length == 1) {
|
|
||||||
usageType = usages.first;
|
|
||||||
} else {
|
|
||||||
// 既用于请求又用于响应
|
|
||||||
usageType = ModelUsageType.common;
|
|
||||||
}
|
|
||||||
|
|
||||||
schemasWithUsageType[name] = model.copyWithUsageType(usageType);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建 ApiComponents,但使用解析后的 schemas
|
|
||||||
final components = ApiComponents.fromJson(componentsJson);
|
|
||||||
return ApiComponents(
|
|
||||||
schemas: schemasWithUsageType,
|
|
||||||
responses: components.responses,
|
|
||||||
parameters: components.parameters,
|
|
||||||
examples: components.examples,
|
|
||||||
requestBodies: components.requestBodies,
|
|
||||||
headers: components.headers,
|
|
||||||
securitySchemes: components.securitySchemes,
|
|
||||||
links: components.links,
|
|
||||||
callbacks: components.callbacks,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} on Object catch (e) {
|
|
||||||
appLogger.warning('⚠️ 解析components配置时发生错误: $e');
|
|
||||||
}
|
|
||||||
|
|
||||||
return const ApiComponents();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 解析tags信息
|
|
||||||
Map<String, String> _parseTagsInfo(Map<String, dynamic> jsonData) {
|
|
||||||
final tagsInfo = <String, String>{};
|
|
||||||
|
|
||||||
try {
|
|
||||||
final tags = jsonData['tags'] as List<dynamic>?;
|
|
||||||
if (tags != null) {
|
|
||||||
for (final tag in tags) {
|
|
||||||
if (tag is Map<String, dynamic>) {
|
|
||||||
final name = tag['name'] as String?;
|
|
||||||
final description = tag['description'] as String?;
|
|
||||||
if (name != null && description != null) {
|
|
||||||
tagsInfo[name] = description;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} on Object catch (e) {
|
|
||||||
appLogger.warning('⚠️ 解析tags信息时发生错误: $e');
|
|
||||||
}
|
|
||||||
|
|
||||||
return tagsInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 解析API路径
|
|
||||||
Map<ApiPathKey, ApiPath> _parseApiPaths(Map<String, dynamic> jsonData) {
|
|
||||||
final paths = <ApiPathKey, ApiPath>{};
|
|
||||||
final pathsData = jsonData['paths'] as Map<String, dynamic>?;
|
|
||||||
|
|
||||||
if (pathsData == null) {
|
|
||||||
throw SwaggerParseException('未发现API路径定义');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
pathsData.forEach((pathKey, pathValue) {
|
|
||||||
if (pathValue is Map<String, dynamic>) {
|
|
||||||
pathValue.forEach((methodKey, methodValue) {
|
|
||||||
if (methodValue is Map<String, dynamic>) {
|
|
||||||
final method = HttpMethod.fromString(methodKey);
|
|
||||||
final apiPath = ApiPath.fromJson(
|
|
||||||
pathKey,
|
|
||||||
method,
|
|
||||||
methodValue,
|
|
||||||
);
|
|
||||||
final key = ApiPathKey.from(pathKey, method);
|
|
||||||
paths[key] = apiPath;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
throw SwaggerParseException('解析API路径失败', details: e.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
return paths;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 解析API控制器
|
|
||||||
Map<String, ApiController> _parseApiControllers(
|
|
||||||
Map<ApiPathKey, ApiPath> paths,
|
|
||||||
Map<String, String> tagsInfo,
|
|
||||||
) {
|
|
||||||
final controllers = <String, List<ApiPath>>{};
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 根据tags分组API路径
|
|
||||||
for (final apiPath in paths.values) {
|
|
||||||
for (final tag in apiPath.tags) {
|
|
||||||
controllers.putIfAbsent(tag, () => []).add(apiPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建控制器对象
|
|
||||||
final result = <String, ApiController>{};
|
|
||||||
controllers.forEach((name, pathList) {
|
|
||||||
// 从tags信息中获取描述
|
|
||||||
final swaggerDescription = tagsInfo[name];
|
|
||||||
|
|
||||||
// 使用通用的描述获取方法
|
|
||||||
final description = SwaggerConfig.getControllerDescription(
|
|
||||||
name,
|
|
||||||
swaggerDescription: swaggerDescription,
|
|
||||||
);
|
|
||||||
|
|
||||||
result[name] = ApiController(
|
|
||||||
name: name,
|
|
||||||
description: description,
|
|
||||||
paths: pathList,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
|
||||||
throw SwaggerParseException('解析API控制器失败', details: e.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 解析属性类型
|
|
||||||
String parsePropertyType(Map<String, dynamic> propData) {
|
|
||||||
try {
|
|
||||||
// 直接类型
|
|
||||||
if (propData['type'] != null) {
|
|
||||||
return propData['type'] as String;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 引用类型 ($ref)
|
|
||||||
if (propData[r'$ref'] != null) {
|
|
||||||
final ref = propData[r'$ref'] as String;
|
|
||||||
// 从 #/components/schemas/ModelName 或 #/definitions/ModelName 中提取类型名
|
|
||||||
final parts = ref.split('/');
|
|
||||||
if (parts.isNotEmpty) {
|
|
||||||
return parts.last;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 数组类型
|
|
||||||
if (propData['items'] != null) {
|
|
||||||
final items = propData['items'] as Map<String, dynamic>;
|
|
||||||
final itemType = parsePropertyType(items);
|
|
||||||
return 'array<$itemType>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认类型
|
|
||||||
return 'string';
|
|
||||||
} catch (e) {
|
|
||||||
throw SwaggerParseException('解析属性类型失败', details: e.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取Dart类型映射
|
|
||||||
String getDartType(String swaggerType, String? format) {
|
|
||||||
switch (swaggerType.toLowerCase()) {
|
|
||||||
case 'string':
|
|
||||||
switch (format?.toLowerCase()) {
|
|
||||||
case 'date':
|
|
||||||
case 'date-time':
|
|
||||||
return 'DateTime';
|
|
||||||
case 'byte':
|
|
||||||
case 'binary':
|
|
||||||
return 'String';
|
|
||||||
default:
|
|
||||||
return 'String';
|
|
||||||
}
|
|
||||||
case 'integer':
|
|
||||||
switch (format?.toLowerCase()) {
|
|
||||||
case 'int64':
|
|
||||||
return 'int';
|
|
||||||
case 'int32':
|
|
||||||
default:
|
|
||||||
return 'int';
|
|
||||||
}
|
|
||||||
case 'number':
|
|
||||||
switch (format?.toLowerCase()) {
|
|
||||||
case 'float':
|
|
||||||
case 'double':
|
|
||||||
return 'double';
|
|
||||||
default:
|
|
||||||
return 'double';
|
|
||||||
}
|
|
||||||
case 'boolean':
|
|
||||||
return 'bool';
|
|
||||||
case 'array':
|
|
||||||
return 'List<dynamic>';
|
|
||||||
case 'object':
|
|
||||||
return 'Map<String, dynamic>';
|
|
||||||
case 'file':
|
|
||||||
return 'String';
|
|
||||||
default:
|
|
||||||
// 检查是否为数组类型
|
|
||||||
if (swaggerType.startsWith('array<') && swaggerType.endsWith('>')) {
|
|
||||||
final itemType = swaggerType.substring(6, swaggerType.length - 1);
|
|
||||||
final dartItemType = getDartType(itemType, null);
|
|
||||||
return 'List<$dartItemType>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认为自定义类型
|
|
||||||
return StringUtils.generateClassName(swaggerType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 清除缓存
|
|
||||||
void clearCache() {
|
|
||||||
_cachedDocuments.clear();
|
|
||||||
_cacheManager.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取文档统计信息
|
|
||||||
Map<String, dynamic> getDocumentStats() {
|
|
||||||
if (_cachedDocuments.isEmpty) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用第一个缓存的文档
|
|
||||||
final doc = _cachedDocuments.values.first;
|
|
||||||
|
|
||||||
// 统计HTTP方法
|
|
||||||
final methodStats = <String, int>{};
|
|
||||||
for (final path in doc.paths.values) {
|
|
||||||
final method = path.method.value;
|
|
||||||
methodStats[method] = (methodStats[method] ?? 0) + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 统计模型类型
|
|
||||||
final modelStats = <String, int>{};
|
|
||||||
for (final model in doc.models.values) {
|
|
||||||
if (model.isEnum) {
|
|
||||||
modelStats['enum'] = (modelStats['enum'] ?? 0) + 1;
|
|
||||||
} else {
|
|
||||||
modelStats['class'] = (modelStats['class'] ?? 0) + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'title': doc.title,
|
|
||||||
'version': doc.version,
|
|
||||||
'paths': doc.paths.length,
|
|
||||||
'models': doc.models.length,
|
|
||||||
'controllers': doc.controllers.length,
|
|
||||||
'methods': methodStats,
|
|
||||||
'modelTypes': modelStats,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 分析 schemas 在 API 中的使用情况
|
|
||||||
/// 返回每个 schema 的用途类型集合
|
|
||||||
Map<String, Set<ModelUsageType>> _analyzeSchemaUsage(
|
|
||||||
Map<String, dynamic> jsonData,
|
|
||||||
) {
|
|
||||||
final schemaUsage = <String, Set<ModelUsageType>>{};
|
|
||||||
|
|
||||||
final pathsData = jsonData['paths'] as Map<String, dynamic>?;
|
|
||||||
if (pathsData == null) return schemaUsage;
|
|
||||||
|
|
||||||
pathsData.forEach((pathKey, pathValue) {
|
|
||||||
if (pathValue is! Map<String, dynamic>) return;
|
|
||||||
|
|
||||||
pathValue.forEach((methodKey, methodValue) {
|
|
||||||
if (methodValue is! Map<String, dynamic>) return;
|
|
||||||
|
|
||||||
// 分析 requestBody 中使用的 schemas
|
|
||||||
final requestBody = methodValue['requestBody'] as Map<String, dynamic>?;
|
|
||||||
if (requestBody != null) {
|
|
||||||
_extractSchemasFromContent(
|
|
||||||
requestBody['content'] as Map<String, dynamic>?,
|
|
||||||
schemaUsage,
|
|
||||||
ModelUsageType.request,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分析 parameters 中使用的 schemas
|
|
||||||
final parameters = methodValue['parameters'] as List<dynamic>?;
|
|
||||||
if (parameters != null) {
|
|
||||||
for (final param in parameters) {
|
|
||||||
if (param is Map<String, dynamic>) {
|
|
||||||
final schema = param['schema'] as Map<String, dynamic>?;
|
|
||||||
if (schema != null) {
|
|
||||||
_extractSchemaRef(schema, schemaUsage, ModelUsageType.request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分析 responses 中使用的 schemas
|
|
||||||
final responses = methodValue['responses'] as Map<String, dynamic>?;
|
|
||||||
if (responses != null) {
|
|
||||||
responses.forEach((statusCode, responseValue) {
|
|
||||||
if (responseValue is Map<String, dynamic>) {
|
|
||||||
_extractSchemasFromContent(
|
|
||||||
responseValue['content'] as Map<String, dynamic>?,
|
|
||||||
schemaUsage,
|
|
||||||
ModelUsageType.response,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return schemaUsage;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 从 content 中提取 schema 引用
|
|
||||||
void _extractSchemasFromContent(
|
|
||||||
Map<String, dynamic>? content,
|
|
||||||
Map<String, Set<ModelUsageType>> schemaUsage,
|
|
||||||
ModelUsageType usageType,
|
|
||||||
) {
|
|
||||||
if (content == null) return;
|
|
||||||
|
|
||||||
content.forEach((mediaType, mediaTypeValue) {
|
|
||||||
if (mediaTypeValue is Map<String, dynamic>) {
|
|
||||||
final schema = mediaTypeValue['schema'] as Map<String, dynamic>?;
|
|
||||||
if (schema != null) {
|
|
||||||
_extractSchemaRef(schema, schemaUsage, usageType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 递归提取 schema $ref 并记录用途
|
|
||||||
void _extractSchemaRef(
|
|
||||||
Map<String, dynamic> schema,
|
|
||||||
Map<String, Set<ModelUsageType>> schemaUsage,
|
|
||||||
ModelUsageType usageType,
|
|
||||||
) {
|
|
||||||
// 处理 $ref
|
|
||||||
final ref = schema[r'$ref'] as String?;
|
|
||||||
if (ref != null) {
|
|
||||||
// 从 #/components/schemas/ModelName 提取 ModelName
|
|
||||||
final schemaName = ref.split('/').last;
|
|
||||||
schemaUsage.putIfAbsent(schemaName, () => {}).add(usageType);
|
|
||||||
return; // $ref 存在时,不再处理其他字段
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理数组类型
|
|
||||||
if (schema['type'] == 'array') {
|
|
||||||
final items = schema['items'] as Map<String, dynamic>?;
|
|
||||||
if (items != null) {
|
|
||||||
_extractSchemaRef(items, schemaUsage, usageType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理 allOf/oneOf/anyOf
|
|
||||||
for (final key in ['allOf', 'oneOf', 'anyOf']) {
|
|
||||||
final schemas = schema[key] as List<dynamic>?;
|
|
||||||
if (schemas != null) {
|
|
||||||
for (final s in schemas) {
|
|
||||||
if (s is Map<String, dynamic>) {
|
|
||||||
_extractSchemaRef(s, schemaUsage, usageType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理对象属性
|
|
||||||
final properties = schema['properties'] as Map<String, dynamic>?;
|
|
||||||
if (properties != null) {
|
|
||||||
properties.forEach((propName, propSchema) {
|
|
||||||
if (propSchema is Map<String, dynamic>) {
|
|
||||||
_extractSchemaRef(propSchema, schemaUsage, usageType);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理 additionalProperties
|
|
||||||
final additionalProperties = schema['additionalProperties'];
|
|
||||||
if (additionalProperties is Map<String, dynamic>) {
|
|
||||||
_extractSchemaRef(additionalProperties, schemaUsage, usageType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,83 +1,4 @@
|
||||||
import 'dart:io';
|
/// Backward-compat shim for parser fetcher
|
||||||
|
library;
|
||||||
|
|
||||||
import 'package:http/http.dart' as http;
|
export 'package:swagger_generator_flutter/pipeline/parse/impl/swagger_fetcher.dart';
|
||||||
import 'package:swagger_generator_flutter/core/config.dart';
|
|
||||||
import 'package:swagger_generator_flutter/core/exceptions.dart';
|
|
||||||
import 'package:swagger_generator_flutter/utils/logger.dart';
|
|
||||||
import 'package:swagger_generator_flutter/utils/path_resolver.dart';
|
|
||||||
|
|
||||||
/// Swagger 数据获取器
|
|
||||||
/// 负责从本地文件或远程 URL 获取 Swagger 文档内容
|
|
||||||
class SwaggerFetcher {
|
|
||||||
/// 获取 Swagger 文档内容
|
|
||||||
/// [url] 可以是本地文件路径 (file://) 或远程 URL (http://, https://)
|
|
||||||
Future<String> fetch(String url) async {
|
|
||||||
appLogger.info('🔄 正在获取Swagger JSON文档: $url');
|
|
||||||
|
|
||||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
||||||
return _fetchFromUrl(url);
|
|
||||||
} else {
|
|
||||||
return _fetchFromFile(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 从远程 URL 获取
|
|
||||||
Future<String> _fetchFromUrl(String url) async {
|
|
||||||
try {
|
|
||||||
final response = await http.get(
|
|
||||||
Uri.parse(url),
|
|
||||||
headers: SwaggerConfig.httpHeaders,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
return response.body;
|
|
||||||
} else {
|
|
||||||
throw SwaggerParseException(
|
|
||||||
'HTTP请求失败',
|
|
||||||
url: url,
|
|
||||||
statusCode: response.statusCode,
|
|
||||||
details: 'HTTP响应状态码: ${response.statusCode}',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (e is SwaggerParseException) rethrow;
|
|
||||||
throw SwaggerParseException(
|
|
||||||
'获取远程Swagger文档失败',
|
|
||||||
url: url,
|
|
||||||
details: e.toString(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 从本地文件获取
|
|
||||||
Future<String> _fetchFromFile(String url) async {
|
|
||||||
try {
|
|
||||||
// 移除 file:// 前缀(如果有)
|
|
||||||
var filePath = url;
|
|
||||||
if (filePath.startsWith('file://')) {
|
|
||||||
filePath = filePath.substring(7);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 PathResolver 解析路径
|
|
||||||
final resolvedPath = PathResolver.resolvePath(filePath);
|
|
||||||
final file = File(resolvedPath);
|
|
||||||
|
|
||||||
if (!file.existsSync()) {
|
|
||||||
throw SwaggerParseException(
|
|
||||||
'本地文件不存在',
|
|
||||||
url: url,
|
|
||||||
details: '文件路径: $resolvedPath',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await file.readAsString();
|
|
||||||
} catch (e) {
|
|
||||||
if (e is SwaggerParseException) rethrow;
|
|
||||||
throw SwaggerParseException(
|
|
||||||
'读取本地Swagger文件失败',
|
|
||||||
url: url,
|
|
||||||
details: e.toString(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,567 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:swagger_generator_flutter/core/config.dart';
|
||||||
|
import 'package:swagger_generator_flutter/core/exceptions.dart';
|
||||||
|
import 'package:swagger_generator_flutter/core/models.dart';
|
||||||
|
import 'package:swagger_generator_flutter/parsers/swagger_fetcher.dart';
|
||||||
|
import 'package:swagger_generator_flutter/utils/cache_manager.dart';
|
||||||
|
import 'package:swagger_generator_flutter/utils/logger.dart';
|
||||||
|
import 'package:swagger_generator_flutter/utils/performance_monitor.dart';
|
||||||
|
import 'package:swagger_generator_flutter/utils/reference_resolver.dart';
|
||||||
|
import 'package:swagger_generator_flutter/utils/string_utils.dart';
|
||||||
|
|
||||||
|
/// Swagger数据解析器
|
||||||
|
/// 负责解析Swagger JSON文档并提取相关信息
|
||||||
|
class SwaggerDataParser {
|
||||||
|
SwaggerDataParser({SwaggerFetcher? fetcher})
|
||||||
|
: _fetcher = fetcher ?? SwaggerFetcher(),
|
||||||
|
_cacheManager = CacheManager(),
|
||||||
|
_performanceMonitor = PerformanceMonitor();
|
||||||
|
|
||||||
|
final SwaggerFetcher _fetcher;
|
||||||
|
final CacheManager _cacheManager;
|
||||||
|
final PerformanceMonitor _performanceMonitor;
|
||||||
|
|
||||||
|
// 缓存解析结果
|
||||||
|
final Map<String, SwaggerDocument> _cachedDocuments = {};
|
||||||
|
|
||||||
|
/// 获取并解析Swagger JSON文档
|
||||||
|
/// [url] 可选参数,如果不传则使用配置中的第一个 URL
|
||||||
|
Future<SwaggerDocument> fetchAndParseSwaggerDocument([String? url]) async {
|
||||||
|
final swaggerUrl = url ?? SwaggerConfig.swaggerJsonUrls.first;
|
||||||
|
|
||||||
|
// 如果有缓存,直接返回缓存结果
|
||||||
|
if (_cachedDocuments.containsKey(swaggerUrl)) {
|
||||||
|
appLogger.info('📦 使用缓存的文档: $swaggerUrl');
|
||||||
|
return _cachedDocuments[swaggerUrl]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _performanceMonitor.measure(
|
||||||
|
'fetchAndParseSwaggerDocument',
|
||||||
|
() async {
|
||||||
|
try {
|
||||||
|
// 使用 Fetcher 获取内容
|
||||||
|
final content = await _fetcher.fetch(swaggerUrl);
|
||||||
|
|
||||||
|
// 解析 JSON
|
||||||
|
final jsonData = json.decode(content) as Map<String, dynamic>;
|
||||||
|
|
||||||
|
// 解析文档
|
||||||
|
final document = await parseSwaggerDocument(jsonData, swaggerUrl);
|
||||||
|
|
||||||
|
// 更新缓存
|
||||||
|
_cachedDocuments[swaggerUrl] = document;
|
||||||
|
appLogger.info('✅ Swagger文档解析完成');
|
||||||
|
|
||||||
|
return document;
|
||||||
|
} on Object catch (e) {
|
||||||
|
if (e is SwaggerParseException) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
throw SwaggerParseException(
|
||||||
|
'获取Swagger文档失败',
|
||||||
|
url: swaggerUrl,
|
||||||
|
details: e.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解析Swagger JSON文档
|
||||||
|
Future<SwaggerDocument> parseSwaggerDocument(
|
||||||
|
Map<String, dynamic> jsonData, [
|
||||||
|
String? sourceUrl,
|
||||||
|
]) async {
|
||||||
|
return _performanceMonitor.measure('parseSwaggerDocument', () async {
|
||||||
|
// 计算内容哈希作为缓存键
|
||||||
|
final contentHash = json.encode(jsonData).hashCode.toString();
|
||||||
|
final cacheKey = 'swagger_doc_$contentHash';
|
||||||
|
|
||||||
|
// 尝试从缓存获取
|
||||||
|
final cachedResult = _cacheManager.get<SwaggerDocument>(cacheKey);
|
||||||
|
if (cachedResult != null) {
|
||||||
|
if (sourceUrl != null) {
|
||||||
|
_cachedDocuments[sourceUrl] = cachedResult;
|
||||||
|
}
|
||||||
|
return cachedResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析文档基本信息
|
||||||
|
final info = jsonData['info'] as Map<String, dynamic>? ?? {};
|
||||||
|
final title = info['title'] as String? ?? 'API Documentation';
|
||||||
|
final version = info['version'] as String? ?? '1.0.0';
|
||||||
|
final description = info['description'] as String? ?? '';
|
||||||
|
|
||||||
|
// ✨ 分析 schema 使用情况(在解析 components 之前)
|
||||||
|
final schemaUsage = _analyzeSchemaUsage(jsonData);
|
||||||
|
|
||||||
|
// 解析 servers (OpenAPI 3.0)
|
||||||
|
final servers = _parseServers(jsonData);
|
||||||
|
|
||||||
|
// 解析 components (OpenAPI 3.0),传入使用情况分析结果
|
||||||
|
final components = _parseComponents(jsonData, schemaUsage);
|
||||||
|
|
||||||
|
// 解析tags信息 (用于获取控制器描述)
|
||||||
|
final tagsInfo = _parseTagsInfo(jsonData);
|
||||||
|
|
||||||
|
// 解析API路径
|
||||||
|
final paths = _parseApiPaths(jsonData);
|
||||||
|
|
||||||
|
// 解析API模型 (从 components 中提取)
|
||||||
|
final models = components.schemas;
|
||||||
|
|
||||||
|
// 解析API控制器 (传入tags信息)
|
||||||
|
final controllers = _parseApiControllers(paths, tagsInfo);
|
||||||
|
|
||||||
|
final document = SwaggerDocument(
|
||||||
|
title: title,
|
||||||
|
version: version,
|
||||||
|
description: description,
|
||||||
|
servers: servers,
|
||||||
|
components: components,
|
||||||
|
paths: paths,
|
||||||
|
models: models,
|
||||||
|
controllers: controllers,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 缓存结果
|
||||||
|
_cacheManager.put(cacheKey, document);
|
||||||
|
if (sourceUrl != null) {
|
||||||
|
_cachedDocuments[sourceUrl] = document;
|
||||||
|
}
|
||||||
|
|
||||||
|
return document;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解析 servers 配置 (OpenAPI 3.0)
|
||||||
|
List<ApiServer> _parseServers(Map<String, dynamic> jsonData) {
|
||||||
|
final servers = <ApiServer>[];
|
||||||
|
|
||||||
|
try {
|
||||||
|
final serversJson = jsonData['servers'] as List<dynamic>?;
|
||||||
|
if (serversJson != null) {
|
||||||
|
for (final serverJson in serversJson) {
|
||||||
|
if (serverJson is Map<String, dynamic>) {
|
||||||
|
final server = ApiServer.fromJson(serverJson);
|
||||||
|
servers.add(server);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有 servers 配置,提供默认值
|
||||||
|
if (servers.isEmpty) {
|
||||||
|
servers.add(const ApiServer(url: '/'));
|
||||||
|
}
|
||||||
|
} on Object catch (e) {
|
||||||
|
appLogger.warning('⚠️ 解析servers配置时发生错误: $e');
|
||||||
|
// 提供默认服务器配置
|
||||||
|
servers.add(const ApiServer(url: '/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return servers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解析 components 配置 (OpenAPI 3.0)
|
||||||
|
ApiComponents _parseComponents(
|
||||||
|
Map<String, dynamic> jsonData,
|
||||||
|
Map<String, Set<ModelUsageType>> schemaUsage,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
final componentsJson = jsonData['components'] as Map<String, dynamic>?;
|
||||||
|
if (componentsJson != null) {
|
||||||
|
// 使用引用解析器处理复杂嵌套和循环引用
|
||||||
|
final resolver = ReferenceResolver();
|
||||||
|
final resolvedSchemas = resolver.resolveModels(componentsJson);
|
||||||
|
|
||||||
|
// ✨ 根据使用情况更新模型的 usageType
|
||||||
|
final schemasWithUsageType = <String, ApiModel>{};
|
||||||
|
resolvedSchemas.forEach((name, model) {
|
||||||
|
final usages = schemaUsage[name] ?? {};
|
||||||
|
final ModelUsageType usageType;
|
||||||
|
|
||||||
|
if (usages.isEmpty) {
|
||||||
|
usageType = ModelUsageType.unknown;
|
||||||
|
} else if (usages.length == 1) {
|
||||||
|
usageType = usages.first;
|
||||||
|
} else {
|
||||||
|
// 既用于请求又用于响应
|
||||||
|
usageType = ModelUsageType.common;
|
||||||
|
}
|
||||||
|
|
||||||
|
schemasWithUsageType[name] = model.copyWithUsageType(usageType);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建 ApiComponents,但使用解析后的 schemas
|
||||||
|
final components = ApiComponents.fromJson(componentsJson);
|
||||||
|
return ApiComponents(
|
||||||
|
schemas: schemasWithUsageType,
|
||||||
|
responses: components.responses,
|
||||||
|
parameters: components.parameters,
|
||||||
|
examples: components.examples,
|
||||||
|
requestBodies: components.requestBodies,
|
||||||
|
headers: components.headers,
|
||||||
|
securitySchemes: components.securitySchemes,
|
||||||
|
links: components.links,
|
||||||
|
callbacks: components.callbacks,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} on Object catch (e) {
|
||||||
|
appLogger.warning('⚠️ 解析components配置时发生错误: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
return const ApiComponents();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解析tags信息
|
||||||
|
Map<String, String> _parseTagsInfo(Map<String, dynamic> jsonData) {
|
||||||
|
final tagsInfo = <String, String>{};
|
||||||
|
|
||||||
|
try {
|
||||||
|
final tags = jsonData['tags'] as List<dynamic>?;
|
||||||
|
if (tags != null) {
|
||||||
|
for (final tag in tags) {
|
||||||
|
if (tag is Map<String, dynamic>) {
|
||||||
|
final name = tag['name'] as String?;
|
||||||
|
final description = tag['description'] as String?;
|
||||||
|
if (name != null && description != null) {
|
||||||
|
tagsInfo[name] = description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} on Object catch (e) {
|
||||||
|
appLogger.warning('⚠️ 解析tags信息时发生错误: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
return tagsInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解析API路径
|
||||||
|
Map<ApiPathKey, ApiPath> _parseApiPaths(Map<String, dynamic> jsonData) {
|
||||||
|
final paths = <ApiPathKey, ApiPath>{};
|
||||||
|
final pathsData = jsonData['paths'] as Map<String, dynamic>?;
|
||||||
|
|
||||||
|
if (pathsData == null) {
|
||||||
|
throw SwaggerParseException('未发现API路径定义');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
pathsData.forEach((pathKey, pathValue) {
|
||||||
|
if (pathValue is Map<String, dynamic>) {
|
||||||
|
pathValue.forEach((methodKey, methodValue) {
|
||||||
|
if (methodValue is Map<String, dynamic>) {
|
||||||
|
final method = HttpMethod.fromString(methodKey);
|
||||||
|
final apiPath = ApiPath.fromJson(
|
||||||
|
pathKey,
|
||||||
|
method,
|
||||||
|
methodValue,
|
||||||
|
);
|
||||||
|
final key = ApiPathKey.from(pathKey, method);
|
||||||
|
paths[key] = apiPath;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
throw SwaggerParseException('解析API路径失败', details: e.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解析API控制器
|
||||||
|
Map<String, ApiController> _parseApiControllers(
|
||||||
|
Map<ApiPathKey, ApiPath> paths,
|
||||||
|
Map<String, String> tagsInfo,
|
||||||
|
) {
|
||||||
|
final controllers = <String, List<ApiPath>>{};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 根据tags分组API路径
|
||||||
|
for (final apiPath in paths.values) {
|
||||||
|
for (final tag in apiPath.tags) {
|
||||||
|
controllers.putIfAbsent(tag, () => []).add(apiPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建控制器对象
|
||||||
|
final result = <String, ApiController>{};
|
||||||
|
controllers.forEach((name, pathList) {
|
||||||
|
// 从tags信息中获取描述
|
||||||
|
final swaggerDescription = tagsInfo[name];
|
||||||
|
|
||||||
|
// 使用通用的描述获取方法
|
||||||
|
final description = SwaggerConfig.getControllerDescription(
|
||||||
|
name,
|
||||||
|
swaggerDescription: swaggerDescription,
|
||||||
|
);
|
||||||
|
|
||||||
|
result[name] = ApiController(
|
||||||
|
name: name,
|
||||||
|
description: description,
|
||||||
|
paths: pathList,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
throw SwaggerParseException('解析API控制器失败', details: e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解析属性类型
|
||||||
|
String parsePropertyType(Map<String, dynamic> propData) {
|
||||||
|
try {
|
||||||
|
// 直接类型
|
||||||
|
if (propData['type'] != null) {
|
||||||
|
return propData['type'] as String;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 引用类型 ($ref)
|
||||||
|
if (propData[r'$ref'] != null) {
|
||||||
|
final ref = propData[r'$ref'] as String;
|
||||||
|
// 从 #/components/schemas/ModelName 或 #/definitions/ModelName 中提取类型名
|
||||||
|
final parts = ref.split('/');
|
||||||
|
if (parts.isNotEmpty) {
|
||||||
|
return parts.last;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数组类型
|
||||||
|
if (propData['items'] != null) {
|
||||||
|
final items = propData['items'] as Map<String, dynamic>;
|
||||||
|
final itemType = parsePropertyType(items);
|
||||||
|
return 'array<$itemType>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认类型
|
||||||
|
return 'string';
|
||||||
|
} catch (e) {
|
||||||
|
throw SwaggerParseException('解析属性类型失败', details: e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取Dart类型映射
|
||||||
|
String getDartType(String swaggerType, String? format) {
|
||||||
|
switch (swaggerType.toLowerCase()) {
|
||||||
|
case 'string':
|
||||||
|
switch (format?.toLowerCase()) {
|
||||||
|
case 'date':
|
||||||
|
case 'date-time':
|
||||||
|
return 'DateTime';
|
||||||
|
case 'byte':
|
||||||
|
case 'binary':
|
||||||
|
return 'String';
|
||||||
|
default:
|
||||||
|
return 'String';
|
||||||
|
}
|
||||||
|
case 'integer':
|
||||||
|
switch (format?.toLowerCase()) {
|
||||||
|
case 'int64':
|
||||||
|
return 'int';
|
||||||
|
case 'int32':
|
||||||
|
default:
|
||||||
|
return 'int';
|
||||||
|
}
|
||||||
|
case 'number':
|
||||||
|
switch (format?.toLowerCase()) {
|
||||||
|
case 'float':
|
||||||
|
case 'double':
|
||||||
|
return 'double';
|
||||||
|
default:
|
||||||
|
return 'double';
|
||||||
|
}
|
||||||
|
case 'boolean':
|
||||||
|
return 'bool';
|
||||||
|
case 'array':
|
||||||
|
return 'List<dynamic>';
|
||||||
|
case 'object':
|
||||||
|
return 'Map<String, dynamic>';
|
||||||
|
case 'file':
|
||||||
|
return 'String';
|
||||||
|
default:
|
||||||
|
// 检查是否为数组类型
|
||||||
|
if (swaggerType.startsWith('array<') && swaggerType.endsWith('>')) {
|
||||||
|
final itemType = swaggerType.substring(6, swaggerType.length - 1);
|
||||||
|
final dartItemType = getDartType(itemType, null);
|
||||||
|
return 'List<$dartItemType>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认为自定义类型
|
||||||
|
return StringUtils.generateClassName(swaggerType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 清除缓存
|
||||||
|
void clearCache() {
|
||||||
|
_cachedDocuments.clear();
|
||||||
|
_cacheManager.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取文档统计信息
|
||||||
|
Map<String, dynamic> getDocumentStats() {
|
||||||
|
if (_cachedDocuments.isEmpty) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用第一个缓存的文档
|
||||||
|
final doc = _cachedDocuments.values.first;
|
||||||
|
|
||||||
|
// 统计HTTP方法
|
||||||
|
final methodStats = <String, int>{};
|
||||||
|
for (final path in doc.paths.values) {
|
||||||
|
final method = path.method.value;
|
||||||
|
methodStats[method] = (methodStats[method] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计模型类型
|
||||||
|
final modelStats = <String, int>{};
|
||||||
|
for (final model in doc.models.values) {
|
||||||
|
if (model.isEnum) {
|
||||||
|
modelStats['enum'] = (modelStats['enum'] ?? 0) + 1;
|
||||||
|
} else {
|
||||||
|
modelStats['class'] = (modelStats['class'] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'title': doc.title,
|
||||||
|
'version': doc.version,
|
||||||
|
'paths': doc.paths.length,
|
||||||
|
'models': doc.models.length,
|
||||||
|
'controllers': doc.controllers.length,
|
||||||
|
'methods': methodStats,
|
||||||
|
'modelTypes': modelStats,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 分析 schemas 在 API 中的使用情况
|
||||||
|
/// 返回每个 schema 的用途类型集合
|
||||||
|
Map<String, Set<ModelUsageType>> _analyzeSchemaUsage(
|
||||||
|
Map<String, dynamic> jsonData,
|
||||||
|
) {
|
||||||
|
final schemaUsage = <String, Set<ModelUsageType>>{};
|
||||||
|
|
||||||
|
final pathsData = jsonData['paths'] as Map<String, dynamic>?;
|
||||||
|
if (pathsData == null) return schemaUsage;
|
||||||
|
|
||||||
|
pathsData.forEach((pathKey, pathValue) {
|
||||||
|
if (pathValue is! Map<String, dynamic>) return;
|
||||||
|
|
||||||
|
pathValue.forEach((methodKey, methodValue) {
|
||||||
|
if (methodValue is! Map<String, dynamic>) return;
|
||||||
|
|
||||||
|
// 分析 requestBody 中使用的 schemas
|
||||||
|
final requestBody = methodValue['requestBody'] as Map<String, dynamic>?;
|
||||||
|
if (requestBody != null) {
|
||||||
|
_extractSchemasFromContent(
|
||||||
|
requestBody['content'] as Map<String, dynamic>?,
|
||||||
|
schemaUsage,
|
||||||
|
ModelUsageType.request,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分析 parameters 中使用的 schemas
|
||||||
|
final parameters = methodValue['parameters'] as List<dynamic>?;
|
||||||
|
if (parameters != null) {
|
||||||
|
for (final param in parameters) {
|
||||||
|
if (param is Map<String, dynamic>) {
|
||||||
|
final schema = param['schema'] as Map<String, dynamic>?;
|
||||||
|
if (schema != null) {
|
||||||
|
_extractSchemaRef(schema, schemaUsage, ModelUsageType.request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分析 responses 中使用的 schemas
|
||||||
|
final responses = methodValue['responses'] as Map<String, dynamic>?;
|
||||||
|
if (responses != null) {
|
||||||
|
responses.forEach((statusCode, responseValue) {
|
||||||
|
if (responseValue is Map<String, dynamic>) {
|
||||||
|
_extractSchemasFromContent(
|
||||||
|
responseValue['content'] as Map<String, dynamic>?,
|
||||||
|
schemaUsage,
|
||||||
|
ModelUsageType.response,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return schemaUsage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从 content 中提取 schema 引用
|
||||||
|
void _extractSchemasFromContent(
|
||||||
|
Map<String, dynamic>? content,
|
||||||
|
Map<String, Set<ModelUsageType>> schemaUsage,
|
||||||
|
ModelUsageType usageType,
|
||||||
|
) {
|
||||||
|
if (content == null) return;
|
||||||
|
|
||||||
|
content.forEach((mediaType, mediaTypeValue) {
|
||||||
|
if (mediaTypeValue is Map<String, dynamic>) {
|
||||||
|
final schema = mediaTypeValue['schema'] as Map<String, dynamic>?;
|
||||||
|
if (schema != null) {
|
||||||
|
_extractSchemaRef(schema, schemaUsage, usageType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 递归提取 schema $ref 并记录用途
|
||||||
|
void _extractSchemaRef(
|
||||||
|
Map<String, dynamic> schema,
|
||||||
|
Map<String, Set<ModelUsageType>> schemaUsage,
|
||||||
|
ModelUsageType usageType,
|
||||||
|
) {
|
||||||
|
// 处理 $ref
|
||||||
|
final ref = schema[r'$ref'] as String?;
|
||||||
|
if (ref != null) {
|
||||||
|
// 从 #/components/schemas/ModelName 提取 ModelName
|
||||||
|
final schemaName = ref.split('/').last;
|
||||||
|
schemaUsage.putIfAbsent(schemaName, () => {}).add(usageType);
|
||||||
|
return; // $ref 存在时,不再处理其他字段
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理数组类型
|
||||||
|
if (schema['type'] == 'array') {
|
||||||
|
final items = schema['items'] as Map<String, dynamic>?;
|
||||||
|
if (items != null) {
|
||||||
|
_extractSchemaRef(items, schemaUsage, usageType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 allOf/oneOf/anyOf
|
||||||
|
for (final key in ['allOf', 'oneOf', 'anyOf']) {
|
||||||
|
final schemas = schema[key] as List<dynamic>?;
|
||||||
|
if (schemas != null) {
|
||||||
|
for (final s in schemas) {
|
||||||
|
if (s is Map<String, dynamic>) {
|
||||||
|
_extractSchemaRef(s, schemaUsage, usageType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理对象属性
|
||||||
|
final properties = schema['properties'] as Map<String, dynamic>?;
|
||||||
|
if (properties != null) {
|
||||||
|
properties.forEach((propName, propSchema) {
|
||||||
|
if (propSchema is Map<String, dynamic>) {
|
||||||
|
_extractSchemaRef(propSchema, schemaUsage, usageType);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 additionalProperties
|
||||||
|
final additionalProperties = schema['additionalProperties'];
|
||||||
|
if (additionalProperties is Map<String, dynamic>) {
|
||||||
|
_extractSchemaRef(additionalProperties, schemaUsage, usageType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:swagger_generator_flutter/core/config.dart';
|
||||||
|
import 'package:swagger_generator_flutter/core/exceptions.dart';
|
||||||
|
import 'package:swagger_generator_flutter/utils/logger.dart';
|
||||||
|
import 'package:swagger_generator_flutter/utils/path_resolver.dart';
|
||||||
|
|
||||||
|
/// Swagger 数据获取器
|
||||||
|
/// 负责从本地文件或远程 URL 获取 Swagger 文档内容
|
||||||
|
class SwaggerFetcher {
|
||||||
|
/// 获取 Swagger 文档内容
|
||||||
|
/// [url] 可以是本地文件路径 (file://) 或远程 URL (http://, https://)
|
||||||
|
Future<String> fetch(String url) async {
|
||||||
|
appLogger.info('🔄 正在获取Swagger JSON文档: $url');
|
||||||
|
|
||||||
|
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||||
|
return _fetchFromUrl(url);
|
||||||
|
} else {
|
||||||
|
return _fetchFromFile(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从远程 URL 获取
|
||||||
|
Future<String> _fetchFromUrl(String url) async {
|
||||||
|
try {
|
||||||
|
final response = await http.get(
|
||||||
|
Uri.parse(url),
|
||||||
|
headers: SwaggerConfig.httpHeaders,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
return response.body;
|
||||||
|
} else {
|
||||||
|
throw SwaggerParseException(
|
||||||
|
'HTTP请求失败',
|
||||||
|
url: url,
|
||||||
|
statusCode: response.statusCode,
|
||||||
|
details: 'HTTP响应状态码: ${response.statusCode}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e is SwaggerParseException) rethrow;
|
||||||
|
throw SwaggerParseException(
|
||||||
|
'获取远程Swagger文档失败',
|
||||||
|
url: url,
|
||||||
|
details: e.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从本地文件获取
|
||||||
|
Future<String> _fetchFromFile(String url) async {
|
||||||
|
try {
|
||||||
|
// 移除 file:// 前缀(如果有)
|
||||||
|
var filePath = url;
|
||||||
|
if (filePath.startsWith('file://')) {
|
||||||
|
filePath = filePath.substring(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 PathResolver 解析路径
|
||||||
|
final resolvedPath = PathResolver.resolvePath(filePath);
|
||||||
|
final file = File(resolvedPath);
|
||||||
|
|
||||||
|
if (!file.existsSync()) {
|
||||||
|
throw SwaggerParseException(
|
||||||
|
'本地文件不存在',
|
||||||
|
url: url,
|
||||||
|
details: '文件路径: $resolvedPath',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await file.readAsString();
|
||||||
|
} catch (e) {
|
||||||
|
if (e is SwaggerParseException) rethrow;
|
||||||
|
throw SwaggerParseException(
|
||||||
|
'读取本地Swagger文件失败',
|
||||||
|
url: url,
|
||||||
|
details: e.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
/// 增强的 OpenAPI 验证器(Pipeline Impl)
|
||||||
|
/// 集成详细的错误报告和修复建议
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:swagger_generator_flutter/core/error_reporter.dart';
|
||||||
|
import 'package:swagger_generator_flutter/core/models.dart';
|
||||||
|
import 'package:swagger_generator_flutter/validators/schema_validator.dart';
|
||||||
|
|
||||||
|
/// 增强的 OpenAPI 验证器
|
||||||
|
class EnhancedValidator {
|
||||||
|
EnhancedValidator({
|
||||||
|
SchemaValidator? schemaValidator,
|
||||||
|
bool includeWarnings = true,
|
||||||
|
}) : _schemaValidator = schemaValidator ?? SchemaValidator(),
|
||||||
|
_errorReporter = ErrorReporter(),
|
||||||
|
_includeWarnings = includeWarnings;
|
||||||
|
|
||||||
|
final SchemaValidator _schemaValidator;
|
||||||
|
final ErrorReporter _errorReporter;
|
||||||
|
final bool _includeWarnings;
|
||||||
|
|
||||||
|
/// 获取错误报告器
|
||||||
|
ErrorReporter get errorReporter => _errorReporter;
|
||||||
|
|
||||||
|
/// 验证 OpenAPI 文档
|
||||||
|
bool validateDocument(SwaggerDocument document) {
|
||||||
|
_errorReporter.clear();
|
||||||
|
|
||||||
|
// 使用基础验证器进行验证
|
||||||
|
final result = _schemaValidator.validateDocument(document);
|
||||||
|
|
||||||
|
// 转换错误
|
||||||
|
for (final error in result.errors) {
|
||||||
|
_errorReporter.reportError(
|
||||||
|
id: 'VALIDATION_ERROR',
|
||||||
|
title: 'Validation Error',
|
||||||
|
description: error.message,
|
||||||
|
severity: _mapSeverity(error.type),
|
||||||
|
category: ErrorCategory.validation,
|
||||||
|
jsonPath: error.path,
|
||||||
|
suggestions: error.suggestion != null
|
||||||
|
? [FixSuggestion(description: error.suggestion!)]
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换警告
|
||||||
|
if (_includeWarnings) {
|
||||||
|
for (final warning in result.warnings) {
|
||||||
|
_errorReporter.reportError(
|
||||||
|
id: 'VALIDATION_WARNING',
|
||||||
|
title: 'Validation Warning',
|
||||||
|
description: warning.message,
|
||||||
|
severity: ErrorSeverity.warning,
|
||||||
|
category: ErrorCategory.bestPractice,
|
||||||
|
jsonPath: warning.path,
|
||||||
|
suggestions:
|
||||||
|
warning.suggestion != null ? [FixSuggestion(description: warning.suggestion!)] : [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 额外的最佳实践检查
|
||||||
|
_checkBestPractices(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
return !_errorReporter.hasErrorsOrCritical;
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorSeverity _mapSeverity(ValidationErrorType type) {
|
||||||
|
switch (type) {
|
||||||
|
case ValidationErrorType.required:
|
||||||
|
case ValidationErrorType.type:
|
||||||
|
case ValidationErrorType.format:
|
||||||
|
case ValidationErrorType.reference:
|
||||||
|
case ValidationErrorType.constraint:
|
||||||
|
case ValidationErrorType.security:
|
||||||
|
case ValidationErrorType.compatibility:
|
||||||
|
return ErrorSeverity.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查最佳实践
|
||||||
|
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"]',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查操作 ID
|
||||||
|
document.paths.forEach((routeKey, path) {
|
||||||
|
if (path.operationId.isEmpty) {
|
||||||
|
final pathPattern = routeKey.pattern;
|
||||||
|
final method = path.method;
|
||||||
|
_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: 'paths["$pathPattern"][${method.value}].operationId',
|
||||||
|
suggestions: [
|
||||||
|
FixSuggestion(
|
||||||
|
description: 'Add a unique operationId',
|
||||||
|
codeExample: '"operationId": "${_generateOperationId(pathPattern, method)}"',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生成操作 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
/// Schema 验证器
|
||||||
|
/// 验证 OpenAPI 3.0 文档的完整性和正确性
|
||||||
|
library;
|
||||||
|
|
||||||
|
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';
|
||||||
|
import 'package:swagger_generator_flutter/validators/rules/component_rules.dart';
|
||||||
|
import 'package:swagger_generator_flutter/validators/rules/info_rules.dart';
|
||||||
|
import 'package:swagger_generator_flutter/validators/rules/path_rules.dart';
|
||||||
|
import 'package:swagger_generator_flutter/validators/rules/security_rules.dart';
|
||||||
|
import 'package:swagger_generator_flutter/validators/rules/server_rules.dart';
|
||||||
|
import 'package:swagger_generator_flutter/validators/rules/structure_rules.dart';
|
||||||
|
|
||||||
|
export 'package:swagger_generator_flutter/validators/core/validation_context.dart';
|
||||||
|
export 'package:swagger_generator_flutter/validators/core/validation_result.dart';
|
||||||
|
export 'package:swagger_generator_flutter/validators/core/validation_rule.dart';
|
||||||
|
|
||||||
|
/// Schema 验证器
|
||||||
|
class SchemaValidator {
|
||||||
|
SchemaValidator({
|
||||||
|
List<ValidationRule>? rules,
|
||||||
|
}) : _rules = rules ?? _defaultRules;
|
||||||
|
|
||||||
|
final List<ValidationRule> _rules;
|
||||||
|
|
||||||
|
static final List<ValidationRule> _defaultRules = [
|
||||||
|
InfoValidationRule(),
|
||||||
|
ServerValidationRule(),
|
||||||
|
PathValidationRule(),
|
||||||
|
ComponentValidationRule(),
|
||||||
|
SecurityValidationRule(),
|
||||||
|
StructureValidationRule(),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// 验证 OpenAPI 文档
|
||||||
|
ValidationResult validateDocument(
|
||||||
|
SwaggerDocument document, {
|
||||||
|
ValidationOptions options = const ValidationOptions(),
|
||||||
|
}) {
|
||||||
|
final context = ValidationContext(
|
||||||
|
document: document,
|
||||||
|
options: options,
|
||||||
|
);
|
||||||
|
|
||||||
|
final results = <ValidationResult>[];
|
||||||
|
for (final rule in _rules) {
|
||||||
|
results.add(rule.validate(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.merge();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/// Pipeline: validate
|
/// Pipeline: validate
|
||||||
/// Re-export schema validator for pipeline-oriented imports.
|
/// Re-export schema validator impl during Phase 2.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
export 'package:swagger_generator_flutter/validators/schema_validator.dart';
|
export 'package:swagger_generator_flutter/pipeline/validate/impl/schema_validator.dart';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,158 +1,4 @@
|
||||||
/// 增强的 OpenAPI 验证器
|
/// Backward-compat shim for EnhancedValidator
|
||||||
/// 集成详细的错误报告和修复建议
|
|
||||||
library;
|
library;
|
||||||
|
|
||||||
import 'package:swagger_generator_flutter/core/error_reporter.dart';
|
export 'package:swagger_generator_flutter/pipeline/validate/impl/enhanced_validator.dart';
|
||||||
import 'package:swagger_generator_flutter/core/models.dart';
|
|
||||||
import 'package:swagger_generator_flutter/validators/schema_validator.dart';
|
|
||||||
|
|
||||||
/// 增强的 OpenAPI 验证器
|
|
||||||
class EnhancedValidator {
|
|
||||||
EnhancedValidator({
|
|
||||||
SchemaValidator? schemaValidator,
|
|
||||||
bool includeWarnings = true,
|
|
||||||
}) : _schemaValidator = schemaValidator ?? SchemaValidator(),
|
|
||||||
_errorReporter = ErrorReporter(),
|
|
||||||
_includeWarnings = includeWarnings;
|
|
||||||
|
|
||||||
final SchemaValidator _schemaValidator;
|
|
||||||
final ErrorReporter _errorReporter;
|
|
||||||
final bool _includeWarnings;
|
|
||||||
|
|
||||||
/// 获取错误报告器
|
|
||||||
ErrorReporter get errorReporter => _errorReporter;
|
|
||||||
|
|
||||||
/// 验证 OpenAPI 文档
|
|
||||||
bool validateDocument(SwaggerDocument document) {
|
|
||||||
_errorReporter.clear();
|
|
||||||
|
|
||||||
// 使用基础验证器进行验证
|
|
||||||
final result = _schemaValidator.validateDocument(document);
|
|
||||||
|
|
||||||
// 转换错误
|
|
||||||
for (final error in result.errors) {
|
|
||||||
_errorReporter.reportError(
|
|
||||||
id: 'VALIDATION_ERROR',
|
|
||||||
title: 'Validation Error',
|
|
||||||
description: error.message,
|
|
||||||
severity: _mapSeverity(error.type),
|
|
||||||
category: ErrorCategory.validation,
|
|
||||||
jsonPath: error.path,
|
|
||||||
suggestions: error.suggestion != null
|
|
||||||
? [FixSuggestion(description: error.suggestion!)]
|
|
||||||
: [],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转换警告
|
|
||||||
if (_includeWarnings) {
|
|
||||||
for (final warning in result.warnings) {
|
|
||||||
_errorReporter.reportError(
|
|
||||||
id: 'VALIDATION_WARNING',
|
|
||||||
title: 'Validation Warning',
|
|
||||||
description: warning.message,
|
|
||||||
severity: ErrorSeverity.warning,
|
|
||||||
category: ErrorCategory.bestPractice,
|
|
||||||
jsonPath: warning.path,
|
|
||||||
suggestions: warning.suggestion != null
|
|
||||||
? [FixSuggestion(description: warning.suggestion!)]
|
|
||||||
: [],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 额外的最佳实践检查
|
|
||||||
_checkBestPractices(document);
|
|
||||||
}
|
|
||||||
|
|
||||||
return !_errorReporter.hasErrorsOrCritical;
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorSeverity _mapSeverity(ValidationErrorType type) {
|
|
||||||
switch (type) {
|
|
||||||
case ValidationErrorType.required:
|
|
||||||
case ValidationErrorType.type:
|
|
||||||
case ValidationErrorType.format:
|
|
||||||
case ValidationErrorType.reference:
|
|
||||||
case ValidationErrorType.constraint:
|
|
||||||
case ValidationErrorType.security:
|
|
||||||
case ValidationErrorType.compatibility:
|
|
||||||
return ErrorSeverity.error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 检查最佳实践
|
|
||||||
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"]',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查操作 ID
|
|
||||||
document.paths.forEach((routeKey, path) {
|
|
||||||
if (path.operationId.isEmpty) {
|
|
||||||
final pathPattern = routeKey.pattern;
|
|
||||||
final method = path.method;
|
|
||||||
_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: 'paths["$pathPattern"][${method.value}].operationId',
|
|
||||||
suggestions: [
|
|
||||||
FixSuggestion(
|
|
||||||
description: 'Add a unique operationId',
|
|
||||||
codeExample: '"operationId": '
|
|
||||||
'"${_generateOperationId(pathPattern, method)}"',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 生成操作 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,54 +1,5 @@
|
||||||
/// Schema 验证器
|
/// Backward-compat shim for schema validator
|
||||||
/// 验证 OpenAPI 3.0 文档的完整性和正确性
|
|
||||||
library;
|
library;
|
||||||
|
|
||||||
import 'package:swagger_generator_flutter/core/models.dart';
|
export 'package:swagger_generator_flutter/pipeline/validate/impl/schema_validator.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';
|
|
||||||
import 'package:swagger_generator_flutter/validators/rules/component_rules.dart';
|
|
||||||
import 'package:swagger_generator_flutter/validators/rules/info_rules.dart';
|
|
||||||
import 'package:swagger_generator_flutter/validators/rules/path_rules.dart';
|
|
||||||
import 'package:swagger_generator_flutter/validators/rules/security_rules.dart';
|
|
||||||
import 'package:swagger_generator_flutter/validators/rules/server_rules.dart';
|
|
||||||
import 'package:swagger_generator_flutter/validators/rules/structure_rules.dart';
|
|
||||||
|
|
||||||
export 'package:swagger_generator_flutter/validators/core/validation_context.dart';
|
|
||||||
export 'package:swagger_generator_flutter/validators/core/validation_result.dart';
|
|
||||||
export 'package:swagger_generator_flutter/validators/core/validation_rule.dart';
|
|
||||||
|
|
||||||
/// Schema 验证器
|
|
||||||
class SchemaValidator {
|
|
||||||
SchemaValidator({
|
|
||||||
List<ValidationRule>? rules,
|
|
||||||
}) : _rules = rules ?? _defaultRules;
|
|
||||||
|
|
||||||
final List<ValidationRule> _rules;
|
|
||||||
|
|
||||||
static final List<ValidationRule> _defaultRules = [
|
|
||||||
InfoValidationRule(),
|
|
||||||
ServerValidationRule(),
|
|
||||||
PathValidationRule(),
|
|
||||||
ComponentValidationRule(),
|
|
||||||
SecurityValidationRule(),
|
|
||||||
StructureValidationRule(),
|
|
||||||
];
|
|
||||||
|
|
||||||
/// 验证 OpenAPI 文档
|
|
||||||
ValidationResult validateDocument(
|
|
||||||
SwaggerDocument document, {
|
|
||||||
ValidationOptions options = const ValidationOptions(),
|
|
||||||
}) {
|
|
||||||
final context = ValidationContext(
|
|
||||||
document: document,
|
|
||||||
options: options,
|
|
||||||
);
|
|
||||||
|
|
||||||
final results = <ValidationResult>[];
|
|
||||||
for (final rule in _rules) {
|
|
||||||
results.add(rule.validate(context));
|
|
||||||
}
|
|
||||||
|
|
||||||
return results.merge();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue