方案 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';
|
||||
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';
|
||||
export 'package:swagger_generator_flutter/pipeline/parse/impl/swagger_data_parser.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;
|
||||
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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
export 'package:swagger_generator_flutter/pipeline/parse/impl/swagger_fetcher.dart';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
/// Re-export schema validator for pipeline-oriented imports.
|
||||
/// Re-export schema validator impl during Phase 2.
|
||||
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;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
export 'package:swagger_generator_flutter/pipeline/validate/impl/enhanced_validator.dart';
|
||||
|
|
|
|||
|
|
@ -1,54 +1,5 @@
|
|||
/// Schema 验证器
|
||||
/// 验证 OpenAPI 3.0 文档的完整性和正确性
|
||||
/// Backward-compat shim for schema validator
|
||||
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/pipeline/validate/impl/schema_validator.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