方案 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:
Max 2025-11-22 21:41:42 +08:00
parent a9de0e72d9
commit ceab0b6f19
9 changed files with 866 additions and 857 deletions

View File

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

View File

@ -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';

View File

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

View File

@ -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(),
);
}
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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';

View File

@ -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';

View File

@ -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();
}
}