diff --git a/bin/main.dart b/bin/main.dart new file mode 100644 index 0000000..c57040c --- /dev/null +++ b/bin/main.dart @@ -0,0 +1,82 @@ +#!/usr/bin/env dart + +import 'dart:io'; + +import '../lib/swagger_cli_new.dart'; + +/// Swagger CLI 工具主入口 +/// +/// 这是一个强大的 Swagger API 代码生成工具,可以: +/// - 解析 Swagger/OpenAPI 文档 +/// - 生成 Dart 模型类 +/// - 生成 API 端点常量 +/// - 生成完整的 API 文档 +/// - 提供类型安全的代码生成 +/// +/// 使用方法: +/// dart run swagger_cli [options] +/// +/// 可用命令: +/// - generate: 生成代码文件 +/// - help: 显示帮助信息 +/// - version: 显示版本信息 +Future main(List arguments) async { + // 检查是否有参数 + if (arguments.isEmpty) { + _showWelcome(); + arguments = ['help']; + } + + // 检查特殊命令 + if (arguments.contains('--version') || arguments.contains('-v')) { + _showVersion(); + return; + } + + // 使用新版本CLI + final cli = SwaggerCLI(); + final exitCode = await cli.run(arguments); + + // 设置退出代码 + exit(exitCode); +} + +/// 显示欢迎信息 +void _showWelcome() { + print(''); + print('🚀 欢迎使用 Swagger CLI 工具!'); + print(''); + print('这是一个强大的 Swagger API 代码生成工具,可以帮助您:'); + print(''); + print(' 📋 解析 Swagger/OpenAPI 文档'); + print(' 🛠️ 生成 Dart 模型类'); + print(' 📡 生成 API 端点常量'); + print(' 📚 生成完整的 API 文档'); + print(' 🔒 提供类型安全的代码生成'); + print(''); + print('使用 --help 查看详细帮助信息'); + print(''); +} + +/// 显示版本信息 +void _showVersion() { + print(''); + print('🚀 Swagger CLI 工具 v2.0.0'); + print(''); + print('构建信息:'); + print(' - Dart SDK: ${Platform.version}'); + print(' - 平台: ${Platform.operatingSystem}'); + print(' - 架构: ${Platform.version}'); + print(''); + print('特性:'); + print(' ✨ 现代化的命令行界面'); + print(' 🏗️ 模块化架构设计'); + print(' 🚀 高性能代码生成'); + print(' 🔍 智能类型验证'); + print(' 📊 性能监控和分析'); + print(' 💾 智能缓存机制'); + print(' 📝 丰富的文档生成'); + print(''); + print('更多信息请访问: https://github.com/yourorg/swagger_cli'); + print(''); +} diff --git a/build.yaml b/build.yaml new file mode 100644 index 0000000..0842dfb --- /dev/null +++ b/build.yaml @@ -0,0 +1,8 @@ +targets: + $default: + sources: + - "lib/**" + builders: + json_serializable: + generate_for: + - "lib/**" \ No newline at end of file diff --git a/lib/commands/base_command.dart b/lib/commands/base_command.dart new file mode 100644 index 0000000..01a6373 --- /dev/null +++ b/lib/commands/base_command.dart @@ -0,0 +1,356 @@ +import '../core/exceptions.dart'; + +/// 命令基类 +/// 实现命令模式,提供统一的命令接口 +abstract class BaseCommand { + /// 命令名称 + String get name; + + /// 命令描述 + String get description; + + /// 命令用法 + String get usage; + + /// 支持的选项 + List get options => []; + + /// 支持的参数 + List get arguments => []; + + /// 执行命令 + Future execute(List args); + + /// 显示帮助信息 + void showHelp() { + print(''); + print('命令: $name'); + print('描述: $description'); + print('用法: $usage'); + + if (arguments.isNotEmpty) { + print(''); + print('参数:'); + for (final arg in arguments) { + final required = arg.required ? '(必填)' : '(可选)'; + print(' ${arg.name} ${arg.description} $required'); + } + } + + if (options.isNotEmpty) { + print(''); + print('选项:'); + for (final option in options) { + final short = option.shortName != null ? '-${option.shortName}, ' : ''; + final defaultValue = option.defaultValue != null + ? ' (默认: ${option.defaultValue})' + : ''; + print(' $short--${option.name} ${option.description}$defaultValue'); + } + } + + print(''); + } + + /// 解析命令行参数 + ParsedArguments parseArguments(List args) { + final parser = ArgumentParser(this); + return parser.parse(args); + } + + /// 验证参数 + void validateArguments(ParsedArguments parsedArgs) { + // 验证必填参数 + for (final arg in arguments.where((a) => a.required)) { + if (!parsedArgs.hasArgument(arg.name)) { + throw CommandException('缺少必填参数: ${arg.name}'); + } + } + + // 验证必填选项 + for (final option in options.where((o) => o.required)) { + if (!parsedArgs.hasOption(option.name)) { + throw CommandException('缺少必填选项: --${option.name}'); + } + } + } + + /// 处理执行前的准备工作 + Future prepare(ParsedArguments parsedArgs) async { + // 子类可以重写此方法进行准备工作 + } + + /// 处理执行后的清理工作 + Future cleanup(ParsedArguments parsedArgs) async { + // 子类可以重写此方法进行清理工作 + } + + /// 错误处理 + void handleError(dynamic error, StackTrace stackTrace) { + if (error is CommandException) { + print('❌ 错误: ${error.message}'); + if (error.details != null) { + print('详细信息: ${error.details}'); + } + } else if (error is SwaggerException) { + print('❌ Swagger错误: ${error.message}'); + if (error.details != null) { + print('详细信息: ${error.details}'); + } + } else { + print('❌ 未知错误: $error'); + print('堆栈跟踪: $stackTrace'); + } + } + + /// 成功消息 + void success(String message) { + print('✅ $message'); + } + + /// 信息消息 + void info(String message) { + print('ℹ️ $message'); + } + + /// 警告消息 + void warning(String message) { + print('⚠️ $message'); + } + + /// 进度消息 + void progress(String message) { + print('🔄 $message'); + } +} + +/// 命令选项 +class CommandOption { + final String name; + final String? shortName; + final String description; + final bool required; + final dynamic defaultValue; + final OptionType type; + + const CommandOption({ + required this.name, + this.shortName, + required this.description, + this.required = false, + this.defaultValue, + this.type = OptionType.flag, + }); +} + +/// 命令参数 +class CommandArgument { + final String name; + final String description; + final bool required; + final dynamic defaultValue; + + const CommandArgument({ + required this.name, + required this.description, + this.required = true, + this.defaultValue, + }); +} + +/// 选项类型 +enum OptionType { flag, string, int, double, list } + +/// 解析后的参数 +class ParsedArguments { + final Map _options = {}; + final Map _arguments = {}; + + /// 设置选项值 + void setOption(String name, dynamic value) { + _options[name] = value; + } + + /// 设置参数值 + void setArgument(String name, dynamic value) { + _arguments[name] = value; + } + + /// 获取选项值 + T? getOption(String name) { + return _options[name] as T?; + } + + /// 获取参数值 + T? getArgument(String name) { + return _arguments[name] as T?; + } + + /// 检查是否有选项 + bool hasOption(String name) { + return _options.containsKey(name); + } + + /// 检查是否有参数 + bool hasArgument(String name) { + return _arguments.containsKey(name); + } + + /// 获取所有选项 + Map get options => Map.unmodifiable(_options); + + /// 获取所有参数 + Map get arguments => Map.unmodifiable(_arguments); +} + +/// 参数解析器 +class ArgumentParser { + final BaseCommand command; + + ArgumentParser(this.command); + + /// 解析参数 + ParsedArguments parse(List args) { + final result = ParsedArguments(); + final argQueue = List.from(args); + + int argumentIndex = 0; + + while (argQueue.isNotEmpty) { + final current = argQueue.removeAt(0); + + if (current.startsWith('--')) { + // 长选项 + _parseLongOption(current, argQueue, result); + } else if (current.startsWith('-') && current.length > 1) { + // 短选项 + _parseShortOption(current, argQueue, result); + } else { + // 位置参数 + if (argumentIndex < command.arguments.length) { + final arg = command.arguments[argumentIndex]; + result.setArgument(arg.name, current); + argumentIndex++; + } else { + throw CommandException('未知参数: $current'); + } + } + } + + // 设置默认值 + _setDefaultValues(result); + + return result; + } + + /// 解析长选项 + void _parseLongOption( + String current, + List argQueue, + ParsedArguments result, + ) { + String optionName; + String? optionValue; + + if (current.contains('=')) { + final parts = current.split('='); + optionName = parts[0].substring(2); + optionValue = parts.sublist(1).join('='); + } else { + optionName = current.substring(2); + } + + final option = command.options.firstWhere( + (o) => o.name == optionName, + orElse: () => throw CommandException('未知选项: --$optionName'), + ); + + if (option.type == OptionType.flag) { + result.setOption(optionName, true); + } else { + if (optionValue == null) { + if (argQueue.isEmpty) { + throw CommandException('选项 --$optionName 需要一个值'); + } + optionValue = argQueue.removeAt(0); + } + + final convertedValue = _convertValue(optionValue, option.type); + result.setOption(optionName, convertedValue); + } + } + + /// 解析短选项 + void _parseShortOption( + String current, + List argQueue, + ParsedArguments result, + ) { + final shortName = current.substring(1); + + final option = command.options.firstWhere( + (o) => o.shortName == shortName, + orElse: () => throw CommandException('未知选项: -$shortName'), + ); + + if (option.type == OptionType.flag) { + result.setOption(option.name, true); + } else { + if (argQueue.isEmpty) { + throw CommandException('选项 -$shortName 需要一个值'); + } + + final optionValue = argQueue.removeAt(0); + final convertedValue = _convertValue(optionValue, option.type); + result.setOption(option.name, convertedValue); + } + } + + /// 转换值类型 + dynamic _convertValue(String value, OptionType type) { + switch (type) { + case OptionType.string: + return value; + case OptionType.int: + return int.tryParse(value) ?? + (throw CommandException('无效的整数值: $value')); + case OptionType.double: + return double.tryParse(value) ?? + (throw CommandException('无效的浮点数值: $value')); + case OptionType.list: + return value.split(',').map((s) => s.trim()).toList(); + case OptionType.flag: + return true; + } + } + + /// 设置默认值 + void _setDefaultValues(ParsedArguments result) { + // 设置选项默认值 + for (final option in command.options) { + if (!result.hasOption(option.name) && option.defaultValue != null) { + result.setOption(option.name, option.defaultValue); + } + } + + // 设置参数默认值 + for (final argument in command.arguments) { + if (!result.hasArgument(argument.name) && argument.defaultValue != null) { + result.setArgument(argument.name, argument.defaultValue); + } + } + } +} + +/// 命令异常 +class CommandException implements Exception { + final String message; + final String? details; + + const CommandException(this.message, {this.details}); + + @override + String toString() { + return 'CommandException: $message${details != null ? ' ($details)' : ''}'; + } +} diff --git a/lib/commands/generate_command.dart b/lib/commands/generate_command.dart new file mode 100644 index 0000000..d63ded6 --- /dev/null +++ b/lib/commands/generate_command.dart @@ -0,0 +1,312 @@ +import '../core/models.dart'; +import '../generators/documentation_generator.dart'; +import '../generators/endpoint_code_generator.dart'; +import '../generators/model_code_generator.dart'; +import '../generators/retrofit_api_generator.dart'; +import '../parsers/swagger_data_parser.dart'; +import '../utils/file_utils.dart'; +import 'base_command.dart'; + +/// Generate命令 +/// 用于生成各种代码文件 +class GenerateCommand extends BaseCommand { + @override + String get name => 'generate'; + + @override + String get description => '生成API代码文件(模型、端点、文档等)'; + + @override + String get usage => 'dart swagger_cli.dart generate [options]'; + + @override + List get options => [ + const CommandOption( + name: 'endpoints', + shortName: 'e', + description: '生成API端点常量', + type: OptionType.flag, + ), + const CommandOption( + name: 'models', + shortName: 'm', + description: '生成数据模型', + type: OptionType.flag, + ), + const CommandOption( + name: 'docs', + shortName: 'd', + description: '生成API文档', + type: OptionType.flag, + ), + const CommandOption( + name: 'api', + shortName: 'r', + description: '生成Retrofit风格API接口', + type: OptionType.flag, + ), + const CommandOption( + name: 'split-by-tags', + shortName: 't', + description: '按tags分组生成多个API文件', + type: OptionType.flag, + ), + const CommandOption( + name: 'all', + shortName: 'a', + description: '生成所有文件(默认)', + type: OptionType.flag, + ), + const CommandOption( + name: 'simple', + shortName: 's', + description: '使用简洁版模型生成', + type: OptionType.flag, + ), + const CommandOption( + name: 'output-dir', + shortName: 'o', + description: '输出目录', + type: OptionType.string, + defaultValue: 'generator', + ), + ]; + + @override + Future execute(List args) async { + try { + final parsedArgs = parseArguments(args); + validateArguments(parsedArgs); + + await prepare(parsedArgs); + + // 解析Swagger文档 + progress('正在解析Swagger文档...'); + final parser = SwaggerDataParser(); + final document = await parser.fetchAndParseSwaggerDocument(); + + // 解析生成选项 + final options = _parseGenerateOptions(parsedArgs); + final outputDir = + parsedArgs.getOption('output-dir') ?? 'generator'; + final fullOutputDir = FileUtils.getProjectRootGeneratorDir(); + + progress('输出目录: $fullOutputDir'); + + // 确保输出目录存在 + await FileUtils.ensureDirectoryExists(fullOutputDir); + + int generatedFiles = 0; + + // 生成端点代码 + if (options.generateEndpoints) { + progress('正在生成API端点常量...'); + final generator = EndpointCodeGenerator(document); + final code = generator.generate(); + + final filePath = '$fullOutputDir/api_paths.dart'; + await FileUtils.writeFile(filePath, code); + success('API端点常量已保存到: $filePath'); + generatedFiles++; + } + + // 生成模型代码 + if (options.generateModels) { + progress('正在生成数据模型...'); + final generator = ModelCodeGenerator( + document, + useSimpleModels: options.useSimpleModels, + ); + + final modelsDir = '$fullOutputDir/api_models'; + await FileUtils.ensureDirectoryExists(modelsDir); + + final modelFiles = generator.generateSeparateModelFiles(); + + for (final entry in modelFiles.entries) { + final filePath = '$modelsDir/${entry.key}'; + await FileUtils.writeFile(filePath, entry.value); + success('模型文件已保存到: $filePath'); + generatedFiles++; + } + } + + // 生成 Retrofit 风格的 API 接口 + if (options.generateApi) { + if (options.splitByTags) { + progress('正在按tags分组生成Retrofit风格API接口...'); + final generator = RetrofitApiGenerator( + document, + className: 'ApiClient', + useRetrofit: true, + useDio: true, + splitByTags: true, + ); + + // 确保参数实体类已生成 + generator.ensureParameterEntitiesGenerated(); + + // 生成按 tags 分组的多个 API 文件 + final tagApiFiles = generator.generateApiFilesByTags(); + + final apiDir = '$fullOutputDir/api'; + await FileUtils.ensureDirectoryExists(apiDir); + + for (final entry in tagApiFiles.entries) { + final fileName = entry.key; + final code = entry.value; + final filePath = '$apiDir/$fileName'; + await FileUtils.writeFile(filePath, code); + success('API接口文件已保存到: $filePath'); + generatedFiles++; + } + + // 生成主 API 文件 + final mainCode = generator.generateMainApiFile(); + final mainFilePath = '$apiDir/api_client.dart'; + await FileUtils.writeFile(mainFilePath, mainCode); + success('主API接口文件已保存到: $mainFilePath'); + generatedFiles++; + + // 生成参数实体类文件 + final parameterEntityFiles = generator.generateParameterEntityFiles(); + if (parameterEntityFiles.isNotEmpty) { + final modelsDir = '$fullOutputDir/api_models'; + await FileUtils.ensureDirectoryExists(modelsDir); + + for (final entry in parameterEntityFiles.entries) { + final filePath = '$modelsDir/${entry.key}'; + await FileUtils.writeFile(filePath, entry.value); + success('参数实体类文件已保存到: $filePath'); + generatedFiles++; + } + } + } else { + progress('正在生成Retrofit风格API接口...'); + final generator = RetrofitApiGenerator( + document, + className: 'ApiClient', + useRetrofit: true, + useDio: true, + splitByTags: false, + ); + + // 确保参数实体类已生成 + generator.ensureParameterEntitiesGenerated(); + + final code = generator.generate(); + + final apiDir = '$fullOutputDir/api'; + await FileUtils.ensureDirectoryExists(apiDir); + + final filePath = '$apiDir/api.dart'; + await FileUtils.writeFile(filePath, code); + success('Retrofit API接口已保存到: $filePath'); + generatedFiles++; + + // 生成参数实体类文件 + final parameterEntityFiles = generator.generateParameterEntityFiles(); + if (parameterEntityFiles.isNotEmpty) { + final modelsDir = '$fullOutputDir/api_models'; + await FileUtils.ensureDirectoryExists(modelsDir); + + for (final entry in parameterEntityFiles.entries) { + final filePath = '$modelsDir/${entry.key}'; + await FileUtils.writeFile(filePath, entry.value); + success('参数实体类文件已保存到: $filePath'); + generatedFiles++; + } + } + } + } + + // 生成文档 + if (options.generateDocs) { + progress('正在生成API文档...'); + final generator = DocumentationGenerator(document); + final code = generator.generate(); + + final filePath = '$fullOutputDir/api_documentation.md'; + await FileUtils.writeFile(filePath, code); + success('API文档已保存到: $filePath'); + generatedFiles++; + } + + // 生成摘要 + _generateSummary(document, fullOutputDir); + + success('代码生成完成!共生成 $generatedFiles 个文件'); + return 0; + } catch (e) { + print('❌ 生成失败: $e'); + return 1; + } + } + + /// 解析生成选项 + GenerateOptions _parseGenerateOptions(ParsedArguments args) { + final hasAnyFlag = args.hasOption('endpoints') || + args.hasOption('models') || + args.hasOption('docs') || + args.hasOption('api'); + + return GenerateOptions( + generateEndpoints: hasAnyFlag + ? (args.getOption('endpoints') ?? false) + : (args.getOption('all') ?? true), + generateModels: hasAnyFlag + ? (args.getOption('models') ?? false) + : (args.getOption('all') ?? true), + generateDocs: hasAnyFlag + ? (args.getOption('docs') ?? false) + : (args.getOption('all') ?? true), + generateApi: hasAnyFlag + ? (args.getOption('api') ?? false) + : (args.getOption('all') ?? true), + useSimpleModels: args.getOption('simple') ?? false, + splitByTags: args.getOption('split-by-tags') ?? false, + ); + } + + /// 生成摘要信息 + void _generateSummary(SwaggerDocument document, String outputDir) { + final summary = StringBuffer(); + summary.writeln('# 代码生成摘要'); + summary.writeln(''); + summary.writeln('**API标题**: ${document.title}'); + summary.writeln('**API版本**: ${document.version}'); + summary.writeln('**生成时间**: ${DateTime.now().toIso8601String()}'); + summary.writeln(''); + summary.writeln('## 统计信息'); + summary.writeln('- 控制器数量: ${document.controllers.length}'); + summary.writeln('- API路径数量: ${document.paths.length}'); + summary.writeln('- 数据模型数量: ${document.models.length}'); + summary.writeln(''); + summary.writeln('## 控制器列表'); + document.controllers.forEach((name, controller) { + summary.writeln( + '- **$name**: ${controller.description} (${controller.paths.length} 个路径)'); + }); + + FileUtils.writeFile('$outputDir/SUMMARY.md', summary.toString()); + } +} + +/// 生成选项 +class GenerateOptions { + final bool generateEndpoints; + final bool generateModels; + final bool generateDocs; + final bool generateApi; + final bool useSimpleModels; + final bool splitByTags; + + const GenerateOptions({ + required this.generateEndpoints, + required this.generateModels, + required this.generateDocs, + required this.generateApi, + required this.useSimpleModels, + required this.splitByTags, + }); +} diff --git a/lib/core/config.dart b/lib/core/config.dart new file mode 100644 index 0000000..798603b --- /dev/null +++ b/lib/core/config.dart @@ -0,0 +1,62 @@ +/// Swagger配置管理 +/// 集中管理所有Swagger相关的配置项 +class SwaggerConfig { + /// Swagger JSON文档的URL + static const String swaggerJsonUrl = + 'https://quanxue-test-api.w.23544.com:8843/swagger/v1/swagger.json'; + + /// 基础API URL + static const String baseUrl = 'https://quanxue-test-api.w.23544.com:8843'; + + /// API版本 + static const String apiVersion = '/api/v1'; + + /// 默认生成器输出目录 + static const String defaultGeneratorDir = 'generator'; + + /// 默认API文件目录 + static const String defaultApiDir = 'api'; + + /// 默认模型文件目录 + static const String defaultModelsDir = 'api_models'; + + /// 默认端点文件名 + static const String defaultEndpointsFile = 'generated_api_paths.dart'; + + /// 默认文档文件名 + static const String defaultDocumentationFile = + 'generated_api_documentation.md'; + + /// HTTP请求头配置 + static const Map httpHeaders = { + 'Accept': 'application/json', + 'User-Agent': 'Flutter-SwaggerParser/1.0', + }; + + /// 生成选项配置 + static const Map defaultGenerateOptions = { + 'generateEndpoints': true, + 'generateModels': true, + 'generateDocs': true, + 'useSimpleModels': false, + 'separateModelFiles': true, + }; + + /// 获取完整的API基础URL + static String get fullApiUrl => '$baseUrl$apiVersion'; + + /// 获取控制器描述 + /// 优先使用Swagger文档中的描述,否则使用控制器名称 + static String getControllerDescription( + String controllerName, { + String? swaggerDescription, + }) { + // 1. 使用Swagger文档中的描述 + if (swaggerDescription != null && swaggerDescription.isNotEmpty) { + return swaggerDescription; + } + + // 2. 使用控制器名称 + return controllerName; + } +} diff --git a/lib/core/exceptions.dart b/lib/core/exceptions.dart new file mode 100644 index 0000000..8be7129 --- /dev/null +++ b/lib/core/exceptions.dart @@ -0,0 +1,608 @@ +import 'dart:io'; + +/// Swagger CLI 基础异常类 +abstract class SwaggerException implements Exception { + final String message; + final String? details; + final DateTime timestamp; + + SwaggerException(this.message, {this.details}) : timestamp = DateTime.now(); + + @override + String toString() { + if (details != null) { + return '$runtimeType: $message\n详细信息: $details'; + } + return '$runtimeType: $message'; + } +} + +/// Swagger解析异常 +class SwaggerParseException extends SwaggerException { + final String? url; + final int? statusCode; + final String? operation; + + SwaggerParseException( + super.message, { + super.details, + this.url, + this.statusCode, + this.operation, + }); + + @override + String toString() { + final buffer = StringBuffer(); + buffer.writeln('SwaggerParseException: $message'); + + if (url != null) { + buffer.writeln('URL: $url'); + } + + if (statusCode != null) { + buffer.writeln('状态码: $statusCode'); + } + + if (operation != null) { + buffer.writeln('操作: $operation'); + } + + if (details != null) { + buffer.writeln('详细信息: $details'); + } + + return buffer.toString().trim(); + } +} + +/// 代码生成异常 +class CodeGenerationException extends SwaggerException { + final String? generatorType; + final String? modelName; + final String? phase; + + CodeGenerationException( + super.message, { + super.details, + this.generatorType, + this.modelName, + this.phase, + }); + + @override + String toString() { + final buffer = StringBuffer(); + buffer.writeln('CodeGenerationException: $message'); + + if (generatorType != null) { + buffer.writeln('生成器类型: $generatorType'); + } + + if (modelName != null) { + buffer.writeln('模型名称: $modelName'); + } + + if (phase != null) { + buffer.writeln('生成阶段: $phase'); + } + + if (details != null) { + buffer.writeln('详细信息: $details'); + } + + return buffer.toString().trim(); + } +} + +/// 文件操作异常 +class FileOperationException extends SwaggerException { + final String? filePath; + final String? operation; + final int? errorCode; + + FileOperationException( + super.message, { + super.details, + this.filePath, + this.operation, + this.errorCode, + }); + + @override + String toString() { + final buffer = StringBuffer(); + buffer.writeln('FileOperationException: $message'); + + if (filePath != null) { + buffer.writeln('文件路径: $filePath'); + } + + if (operation != null) { + buffer.writeln('操作: $operation'); + } + + if (errorCode != null) { + buffer.writeln('错误代码: $errorCode'); + } + + if (details != null) { + buffer.writeln('详细信息: $details'); + } + + return buffer.toString().trim(); + } +} + +/// 命令异常 +class CommandException extends SwaggerException { + final String? commandName; + final List? arguments; + final int? exitCode; + + CommandException( + super.message, { + super.details, + this.commandName, + this.arguments, + this.exitCode, + }); + + @override + String toString() { + final buffer = StringBuffer(); + buffer.writeln('CommandException: $message'); + + if (commandName != null) { + buffer.writeln('命令: $commandName'); + } + + if (arguments != null && arguments!.isNotEmpty) { + buffer.writeln('参数: ${arguments!.join(' ')}'); + } + + if (exitCode != null) { + buffer.writeln('退出代码: $exitCode'); + } + + if (details != null) { + buffer.writeln('详细信息: $details'); + } + + return buffer.toString().trim(); + } +} + +/// 验证异常 +class ValidationException extends SwaggerException { + final String? field; + final dynamic value; + final String? rule; + + ValidationException( + super.message, { + super.details, + this.field, + this.value, + this.rule, + }); + + @override + String toString() { + final buffer = StringBuffer(); + buffer.writeln('ValidationException: $message'); + + if (field != null) { + buffer.writeln('字段: $field'); + } + + if (value != null) { + buffer.writeln('值: $value'); + } + + if (rule != null) { + buffer.writeln('验证规则: $rule'); + } + + if (details != null) { + buffer.writeln('详细信息: $details'); + } + + return buffer.toString().trim(); + } +} + +/// 配置异常 +class ConfigurationException extends SwaggerException { + final String? configKey; + final dynamic configValue; + final String? source; + + ConfigurationException( + super.message, { + super.details, + this.configKey, + this.configValue, + this.source, + }); + + @override + String toString() { + final buffer = StringBuffer(); + buffer.writeln('ConfigurationException: $message'); + + if (configKey != null) { + buffer.writeln('配置键: $configKey'); + } + + if (configValue != null) { + buffer.writeln('配置值: $configValue'); + } + + if (source != null) { + buffer.writeln('来源: $source'); + } + + if (details != null) { + buffer.writeln('详细信息: $details'); + } + + return buffer.toString().trim(); + } +} + +/// 网络异常 +class NetworkException extends SwaggerException { + final String? url; + final int? statusCode; + final String? method; + final Duration? timeout; + + NetworkException( + super.message, { + super.details, + this.url, + this.statusCode, + this.method, + this.timeout, + }); + + @override + String toString() { + final buffer = StringBuffer(); + buffer.writeln('NetworkException: $message'); + + if (url != null) { + buffer.writeln('URL: $url'); + } + + if (method != null) { + buffer.writeln('方法: $method'); + } + + if (statusCode != null) { + buffer.writeln('状态码: $statusCode'); + } + + if (timeout != null) { + buffer.writeln('超时: ${timeout!.inSeconds}秒'); + } + + if (details != null) { + buffer.writeln('详细信息: $details'); + } + + return buffer.toString().trim(); + } +} + +/// 缓存异常 +class CacheException extends SwaggerException { + final String? cacheKey; + final String? operation; + final String? cacheType; + + CacheException( + super.message, { + super.details, + this.cacheKey, + this.operation, + this.cacheType, + }); + + @override + String toString() { + final buffer = StringBuffer(); + buffer.writeln('CacheException: $message'); + + if (cacheKey != null) { + buffer.writeln('缓存键: $cacheKey'); + } + + if (operation != null) { + buffer.writeln('操作: $operation'); + } + + if (cacheType != null) { + buffer.writeln('缓存类型: $cacheType'); + } + + if (details != null) { + buffer.writeln('详细信息: $details'); + } + + return buffer.toString().trim(); + } +} + +/// 性能异常 +class PerformanceException extends SwaggerException { + final String? operation; + final Duration? duration; + final Duration? threshold; + + PerformanceException( + super.message, { + super.details, + this.operation, + this.duration, + this.threshold, + }); + + @override + String toString() { + final buffer = StringBuffer(); + buffer.writeln('PerformanceException: $message'); + + if (operation != null) { + buffer.writeln('操作: $operation'); + } + + if (duration != null) { + buffer.writeln('耗时: ${duration!.inMilliseconds}ms'); + } + + if (threshold != null) { + buffer.writeln('阈值: ${threshold!.inMilliseconds}ms'); + } + + if (details != null) { + buffer.writeln('详细信息: $details'); + } + + return buffer.toString().trim(); + } +} + +/// 类型异常 +class TypeException extends SwaggerException { + final String? propertyName; + final String? expectedType; + final String? actualType; + final dynamic value; + + TypeException( + super.message, { + super.details, + this.propertyName, + this.expectedType, + this.actualType, + this.value, + }); + + @override + String toString() { + final buffer = StringBuffer(); + buffer.writeln('TypeException: $message'); + + if (propertyName != null) { + buffer.writeln('属性名: $propertyName'); + } + + if (expectedType != null) { + buffer.writeln('期望类型: $expectedType'); + } + + if (actualType != null) { + buffer.writeln('实际类型: $actualType'); + } + + if (value != null) { + buffer.writeln('值: $value'); + } + + if (details != null) { + buffer.writeln('详细信息: $details'); + } + + return buffer.toString().trim(); + } +} + +/// 异常处理器 +class ExceptionHandler { + static final Map _handlers = {}; + + /// 注册异常处理器 + static void register( + void Function(T exception) handler, + ) { + _handlers[T] = (exception) => handler(exception as T); + } + + /// 处理异常 + static void handle(SwaggerException exception) { + final handler = _handlers[exception.runtimeType]; + if (handler != null) { + handler(exception); + } else { + // 默认处理 + _defaultHandler(exception); + } + } + + /// 默认异常处理 + static void _defaultHandler(SwaggerException exception) { + print('🚨 异常: ${exception.toString()}'); + print('时间: ${exception.timestamp.toIso8601String()}'); + print(''); + } + + /// 记录异常到文件 + static Future logException( + SwaggerException exception, { + String? logFilePath, + }) async { + try { + final logFile = logFilePath != null + ? File(logFilePath) + : File('swagger_cli_errors.log'); + + final logEntry = [ + '[${'=' * 50}]', + '时间: ${exception.timestamp.toIso8601String()}', + '类型: ${exception.runtimeType}', + '消息: ${exception.message}', + if (exception.details != null) '详细信息: ${exception.details}', + '堆栈跟踪: ${StackTrace.current}', + '', + ].join('\n'); + + await logFile.writeAsString(logEntry, mode: FileMode.append); + } catch (e) { + print('记录异常到文件失败: $e'); + } + } + + /// 清理异常处理器 + static void clear() { + _handlers.clear(); + } +} + +/// 异常工厂 +class ExceptionFactory { + /// 创建解析异常 + static SwaggerParseException createParseException( + String message, { + String? url, + int? statusCode, + String? operation, + dynamic cause, + }) { + return SwaggerParseException( + message, + details: cause?.toString(), + url: url, + statusCode: statusCode, + operation: operation, + ); + } + + /// 创建代码生成异常 + static CodeGenerationException createCodeGenerationException( + String message, { + String? generatorType, + String? modelName, + String? phase, + dynamic cause, + }) { + return CodeGenerationException( + message, + details: cause?.toString(), + generatorType: generatorType, + modelName: modelName, + phase: phase, + ); + } + + /// 创建文件操作异常 + static FileOperationException createFileOperationException( + String message, { + String? filePath, + String? operation, + int? errorCode, + dynamic cause, + }) { + return FileOperationException( + message, + details: cause?.toString(), + filePath: filePath, + operation: operation, + errorCode: errorCode, + ); + } + + /// 创建验证异常 + static ValidationException createValidationException( + String message, { + String? field, + dynamic value, + String? rule, + dynamic cause, + }) { + return ValidationException( + message, + details: cause?.toString(), + field: field, + value: value, + rule: rule, + ); + } + + /// 创建网络异常 + static NetworkException createNetworkException( + String message, { + String? url, + int? statusCode, + String? method, + Duration? timeout, + dynamic cause, + }) { + return NetworkException( + message, + details: cause?.toString(), + url: url, + statusCode: statusCode, + method: method, + timeout: timeout, + ); + } + + /// 从标准异常创建 + static SwaggerException fromStandardException( + Exception exception, { + String? context, + }) { + if (exception is FileSystemException) { + return FileOperationException( + '文件系统错误', + details: exception.message, + filePath: exception.path, + operation: context, + ); + } else if (exception is SocketException) { + return NetworkException( + '网络连接错误', + details: exception.message, + url: context, + ); + } else if (exception is FormatException) { + return SwaggerParseException( + '格式错误', + details: exception.message, + operation: context, + ); + } else { + return GeneralSwaggerException( + '未知错误', + details: exception.toString(), + ); + } + } +} + +/// 通用Swagger异常(当无法确定具体类型时使用) +class GeneralSwaggerException extends SwaggerException { + GeneralSwaggerException(super.message, {super.details}); +} diff --git a/lib/core/models.dart b/lib/core/models.dart new file mode 100644 index 0000000..48b2ebb --- /dev/null +++ b/lib/core/models.dart @@ -0,0 +1,502 @@ +/// Swagger数据模型定义 +/// 提供类型安全的数据结构 +library; + +/// HTTP方法枚举 +/// 表示常见的 RESTful API 方法。 +enum HttpMethod { + /// GET 方法 + get('GET'), + + /// POST 方法 + post('POST'), + + /// PUT 方法 + put('PUT'), + + /// DELETE 方法 + delete('DELETE'), + + /// PATCH 方法 + patch('PATCH'), + + /// HEAD 方法 + head('HEAD'), + + /// OPTIONS 方法 + options('OPTIONS'); + + /// 枚举值对应的字符串 + const HttpMethod(this.value); + final String value; + + /// 通过字符串获取 HttpMethod 枚举 + static HttpMethod fromString(String value) { + return HttpMethod.values.firstWhere( + (method) => method.value == value.toUpperCase(), + orElse: () => HttpMethod.get, + ); + } +} + +/// 属性类型枚举 +/// 用于描述 API 属性的数据类型。 +enum PropertyType { + /// 字符串类型 + string('string'), + + /// 整数类型 + integer('integer'), + + /// 浮点数类型 + number('number'), + + /// 布尔类型 + boolean('boolean'), + + /// 数组类型 + array('array'), + + /// 对象类型 + object('object'), + + /// 文件类型 + file('file'), + + /// 日期类型 + date('date'), + + /// 日期时间类型 + dateTime('date-time'), + + /// 引用类型 + reference('reference'), + + /// 枚举类型 + enumType('enum'); + + /// 枚举值对应的字符串 + const PropertyType(this.value); + final String value; + + /// 通过字符串获取 PropertyType 枚举 + static PropertyType fromString(String value) { + return PropertyType.values.firstWhere( + (type) => type.value == value.toLowerCase(), + orElse: () => PropertyType.string, + ); + } +} + +/// 参数位置枚举 +/// 用于描述 API 参数在请求中的位置。 +enum ParameterLocation { + /// 查询参数 + query('query'), + + /// 路径参数 + path('path'), + + /// 请求头参数 + header('header'), + + /// 请求体参数 + body('body'), + + /// 表单参数 + form('formData'), + + /// Cookie 参数 + cookie('cookie'); + + /// 枚举值对应的字符串 + const ParameterLocation(this.value); + final String value; + + /// 通过字符串获取 ParameterLocation 枚举 + static ParameterLocation fromString(String value) { + return ParameterLocation.values.firstWhere( + (location) => location.value == value.toLowerCase(), + orElse: () => ParameterLocation.query, + ); + } +} + +/// Swagger文档信息 +/// 描述整个 API 的元数据和结构。 +class SwaggerDocument { + /// 文档标题 + final String title; + + /// 版本号 + final String version; + + /// 文档描述 + final String description; + + /// 主机名 + final String host; + + /// 基础路径 + final String basePath; + + /// 支持的协议 + final List schemes; + + /// 支持的请求类型 + final List consumes; + + /// 支持的响应类型 + final List produces; + + /// 路径定义 + final Map paths; + + /// 数据模型定义 + final Map models; + + /// 控制器定义 + final Map controllers; + + /// 构造函数 + const SwaggerDocument({ + required this.title, + required this.version, + required this.description, + required this.host, + required this.basePath, + required this.schemes, + required this.consumes, + required this.produces, + required this.paths, + required this.models, + required this.controllers, + }); + + /// 从JSON创建SwaggerDocument + factory SwaggerDocument.fromJson(Map json) { + final info = json['info'] as Map? ?? {}; + return SwaggerDocument( + title: info['title'] as String? ?? 'API', + version: info['version'] as String? ?? '1.0.0', + description: info['description'] as String? ?? '', + host: json['host'] as String? ?? '', + basePath: json['basePath'] as String? ?? '', + schemes: List.from(json['schemes'] ?? ['https']), + consumes: List.from(json['consumes'] ?? ['application/json']), + produces: List.from(json['produces'] ?? ['application/json']), + paths: {}, + models: {}, + controllers: {}, + ); + } +} + +/// API路径信息 +class ApiPath { + final String path; + final HttpMethod method; + final String summary; + final String description; + final String operationId; + final List tags; + final List parameters; + final Map responses; + final ApiRequestBody? requestBody; + final bool deprecated; + + const ApiPath({ + required this.path, + required this.method, + required this.summary, + required this.description, + required this.operationId, + required this.tags, + required this.parameters, + required this.responses, + this.requestBody, + this.deprecated = false, + }); + + /// 从JSON创建ApiPath + factory ApiPath.fromJson( + String path, + String method, + Map json, + ) { + return ApiPath( + path: path, + method: HttpMethod.fromString(method), + summary: json['summary'] as String? ?? '', + description: json['description'] as String? ?? '', + operationId: json['operationId'] as String? ?? '', + tags: List.from(json['tags'] ?? []), + parameters: (json['parameters'] as List?) + ?.map((p) => ApiParameter.fromJson(p as Map)) + .toList() ?? + [], + responses: (json['responses'] as Map?)?.map( + (code, response) => MapEntry( + code, + ApiResponse.fromJson(code, response as Map), + ), + ) ?? + {}, + requestBody: json['requestBody'] != null + ? ApiRequestBody.fromJson(json['requestBody'] as Map) + : null, + deprecated: json['deprecated'] as bool? ?? false, + ); + } +} + +/// API参数信息 +class ApiParameter { + final String name; + final ParameterLocation location; + final bool required; + final PropertyType type; + final String description; + final String? format; + final dynamic example; + final dynamic defaultValue; + + const ApiParameter({ + required this.name, + required this.location, + required this.required, + required this.type, + required this.description, + this.format, + this.example, + this.defaultValue, + }); + + /// 从JSON创建ApiParameter + factory ApiParameter.fromJson(Map json) { + final schema = json['schema'] as Map?; + final type = + schema?['type'] as String? ?? json['type'] as String? ?? 'string'; + + return ApiParameter( + name: json['name'] as String? ?? '', + location: ParameterLocation.fromString(json['in'] as String? ?? 'query'), + required: json['required'] as bool? ?? false, + type: PropertyType.fromString(type), + description: json['description'] as String? ?? '', + format: schema?['format'] as String? ?? json['format'] as String?, + example: json['example'], + defaultValue: json['default'], + ); + } +} + +/// API响应信息 +class ApiResponse { + final String code; + final String description; + final Map? schema; + final Map? content; + + const ApiResponse({ + required this.code, + required this.description, + this.schema, + this.content, + }); + + /// 从JSON创建ApiResponse + factory ApiResponse.fromJson(String code, Map json) { + return ApiResponse( + code: code, + description: json['description'] as String? ?? '', + schema: json['schema'] as Map?, + content: json['content'] as Map?, + ); + } +} + +/// API请求体信息 +class ApiRequestBody { + final String description; + final bool required; + final Map? content; + + const ApiRequestBody({ + required this.description, + required this.required, + this.content, + }); + + /// 从JSON创建ApiRequestBody + factory ApiRequestBody.fromJson(Map json) { + return ApiRequestBody( + description: json['description'] as String? ?? '', + required: json['required'] as bool? ?? false, + content: json['content'] as Map?, + ); + } +} + +/// API模型信息 +class ApiModel { + final String name; + final String description; + final Map properties; + final List required; + final bool isEnum; + final List enumValues; + final PropertyType? enumType; + + const ApiModel({ + required this.name, + required this.description, + required this.properties, + required this.required, + this.isEnum = false, + this.enumValues = const [], + this.enumType, + }); + + /// 从JSON创建ApiModel + factory ApiModel.fromJson(String name, Map json) { + final isEnum = json['enum'] != null; + final enumValues = isEnum ? List.from(json['enum']) : []; + final properties = json['properties'] as Map? ?? {}; + List required; + if (json.containsKey('required')) { + required = List.from(json['required']); + } else { + // 没有 required 字段时,凡 nullable != true 的都视为 required + required = properties.entries + .where((e) => !(e.value['nullable'] as bool? ?? false)) + .map((e) => e.key) + .toList(); + } + + return ApiModel( + name: name, + description: json['description'] as String? ?? '', + required: required, + isEnum: isEnum, + enumValues: enumValues, + enumType: isEnum + ? PropertyType.fromString(json['type'] as String? ?? 'string') + : null, + properties: properties.map( + (propName, propData) => MapEntry( + propName, + ApiProperty.fromJson( + propName, + propData as Map, + required, + ), + ), + ), + ); + } +} + +/// API属性信息 +class ApiProperty { + final String name; + final PropertyType type; + final String? format; + final String description; + final bool required; + final bool nullable; + final dynamic example; + final dynamic defaultValue; + final String? reference; + final ApiModel? items; // 用于数组类型 + + const ApiProperty({ + required this.name, + required this.type, + this.format, + required this.description, + required this.required, + this.nullable = false, + this.example, + this.defaultValue, + this.reference, + this.items, + }); + + /// 从JSON创建ApiProperty + factory ApiProperty.fromJson( + String name, + Map json, + List requiredFields, + ) { + final type = PropertyType.fromString(json['type'] as String? ?? 'string'); + String? reference; + ApiModel? items; + + // 处理引用类型 + if (json['\$ref'] != null) { + final ref = json['\$ref'] as String; + reference = ref.split('/').last; + } + + // 处理数组类型的 items + if (type == PropertyType.array && json['items'] != null) { + final itemsJson = json['items'] as Map; + + // 如果 items 是引用类型 + if (itemsJson['\$ref'] != null) { + final itemRef = itemsJson['\$ref'] as String; + final itemRefName = itemRef.split('/').last; + items = ApiModel( + name: itemRefName, + description: '', + properties: {}, + required: [], + isEnum: false, + ); + } else { + // 如果 items 是基本类型 + final itemType = + PropertyType.fromString(itemsJson['type'] as String? ?? 'string'); + items = ApiModel( + name: itemType.value, + description: '', + properties: {}, + required: [], + isEnum: false, + ); + } + } + + return ApiProperty( + name: name, + type: reference != null ? PropertyType.reference : type, + format: json['format'] as String?, + description: json['description'] as String? ?? '', + required: requiredFields.contains(name), + nullable: json['nullable'] as bool? ?? false, + example: json['example'], + defaultValue: json['default'], + reference: reference, + items: items, + ); + } +} + +/// API控制器信息 +class ApiController { + final String name; + final String description; + final List paths; + + const ApiController({ + required this.name, + required this.description, + required this.paths, + }); + + /// 从路径列表创建ApiController + factory ApiController.fromPaths(String name, List paths) { + return ApiController(name: name, description: name, paths: paths); + } +} diff --git a/lib/generators/base_generator.dart b/lib/generators/base_generator.dart new file mode 100644 index 0000000..717375a --- /dev/null +++ b/lib/generators/base_generator.dart @@ -0,0 +1,361 @@ +import '../core/config.dart'; +import '../core/exceptions.dart'; +import '../core/models.dart'; +import '../utils/string_utils.dart'; + +/// 代码生成器基类 +/// 定义通用的接口和功能 +abstract class BaseGenerator { + /// 生成器类型 + String get generatorType; + + /// 生成代码 + String generate(); + + /// 生成文件头注释 + String generateFileHeader(String description) { + return StringUtils.generateFileHeader( + description, + SwaggerConfig.swaggerJsonUrl, + ); + } + + /// 生成类型安全的代码 + String generateTypeCheckedCode(String code) { + // 基础类型检查和验证 + try { + if (code.trim().isEmpty) { + throw CodeGenerationException( + '生成的代码不能为空', + generatorType: generatorType, + ); + } + + return code; + } catch (e) { + throw CodeGenerationException( + '代码生成失败', + details: e.toString(), + generatorType: generatorType, + ); + } + } + + /// 验证生成的代码是否符合Dart语法规范 + bool validateDartCode(String code) { + // 基础验证规则 + final validationRules = [ + // 检查是否有未闭合的大括号 + (String code) => _countMatches(code, '{') == _countMatches(code, '}'), + // 检查是否有未闭合的小括号 + (String code) => _countMatches(code, '(') == _countMatches(code, ')'), + // 检查是否有未闭合的方括号 + (String code) => _countMatches(code, '[') == _countMatches(code, ']'), + // 检查是否有基本的类声明 + (String code) => + code.contains('class ') || + code.contains('enum ') || + code.contains('const '), + ]; + + for (final rule in validationRules) { + if (!rule(code)) { + return false; + } + } + + return true; + } + + /// 计算字符出现次数 + int _countMatches(String text, String pattern) { + return pattern.allMatches(text).length; + } +} + +/// 模型代码生成器基类 +abstract class ModelGenerator extends BaseGenerator { + final SwaggerDocument document; + final bool useSimpleModels; + + ModelGenerator(this.document, {this.useSimpleModels = false}); + + @override + String get generatorType => 'ModelGenerator'; + + /// 生成单个模型的代码 + String generateModelCode(ApiModel model); + + /// 生成枚举代码 + String generateEnumCode(ApiModel model) { + if (!model.isEnum) { + throw CodeGenerationException('模型不是枚举类型', generatorType: generatorType); + } + + final className = StringUtils.generateClassName(model.name); + final enumType = model.enumType?.value ?? 'string'; + final buffer = StringBuffer(); + + // 生成文件头 + buffer.writeln(generateFileHeader('${model.name} 枚举定义')); + buffer.writeln(''); + + // 生成枚举类 + if (model.description.isNotEmpty) { + buffer.writeln(StringUtils.generateComment(model.description)); + } + + buffer.writeln('enum $className {'); + + // 生成枚举值 + for (int i = 0; i < model.enumValues.length; i++) { + final value = model.enumValues[i]; + final enumName = StringUtils.generateEnumValueName(value, i); + + if (enumType == 'integer' || enumType == 'number') { + buffer.writeln(' $enumName($value),'); + } else { + buffer.writeln(' $enumName(\'$value\'),'); + } + } + + // 移除最后一个逗号 + final content = buffer.toString().trimRight(); + buffer.clear(); + buffer.writeln(content.substring(0, content.lastIndexOf(','))); + buffer.writeln(';'); + buffer.writeln(''); + + // 生成构造函数和方法 + buffer.writeln(' const $className(this.value);'); + buffer.writeln( + ' final ${enumType == 'integer' || enumType == 'number' ? 'int' : 'String'} value;', + ); + buffer.writeln(''); + + // 生成 fromValue 方法 + buffer.writeln(' static $className fromValue(dynamic value) {'); + buffer.writeln(' for (final enumValue in $className.values) {'); + buffer.writeln(' if (enumValue.value == value) {'); + buffer.writeln(' return enumValue;'); + buffer.writeln(' }'); + buffer.writeln(' }'); + buffer.writeln(' throw ArgumentError(\'Unknown enum value: \$value\');'); + buffer.writeln(' }'); + buffer.writeln(''); + + // 生成 fromJson 方法 + buffer.writeln(' factory $className.fromJson(dynamic json) {'); + buffer.writeln(' return fromValue(json);'); + buffer.writeln(' }'); + buffer.writeln(''); + + // 生成 toJson 方法 + buffer.writeln(' dynamic toJson() => value;'); + buffer.writeln(''); + + buffer.writeln('}'); + + return generateTypeCheckedCode(buffer.toString()); + } + + // 已移动到 StringUtils.generateEnumValueName + + /// 获取导入的类型列表 + Set getImportedTypes(ApiModel model) { + final importedTypes = {}; + + model.properties.forEach((_, property) { + if (property.type == PropertyType.reference && + property.reference != null) { + importedTypes.add(property.reference!); + } else if (property.type == PropertyType.array) { + // 处理数组类型的引用 + if (property.items != null) { + final itemType = _getItemType(property.items!); + // 如果是引用类型(不是基本类型),则添加到导入列表 + if (itemType != 'String' && + itemType != 'int' && + itemType != 'double' && + itemType != 'bool' && + itemType != 'dynamic') { + importedTypes.add(property.items!.name); + } + } + } + }); + + return importedTypes; + } + + /// 生成属性的Dart类型 + String getDartPropertyType(ApiProperty property) { + switch (property.type) { + case PropertyType.string: + switch (property.format) { + case 'date': + case 'date-time': + return 'DateTime'; + default: + return 'String'; + } + case PropertyType.integer: + return 'int'; + case PropertyType.number: + return 'double'; + case PropertyType.boolean: + return 'bool'; + case PropertyType.array: + // 根据数组元素类型推导具体类型 + if (property.items != null) { + final itemType = _getItemType(property.items!); + return 'List<$itemType>'; + } + return 'List'; + case PropertyType.object: + return 'Map'; + case PropertyType.reference: + return property.reference != null + ? StringUtils.generateClassName(property.reference!) + : 'dynamic'; + default: + return 'dynamic'; + } + } + + /// 获取数组元素的类型 + String _getItemType(ApiModel items) { + // 如果是引用类型,直接返回类名 + if (items.name != 'string' && + items.name != 'integer' && + items.name != 'number' && + items.name != 'boolean') { + return StringUtils.generateClassName(items.name); + } + + // 如果是基本类型,转换为对应的Dart类型 + switch (items.name) { + case 'string': + return 'String'; + case 'integer': + return 'int'; + case 'number': + return 'double'; + case 'boolean': + return 'bool'; + default: + return 'dynamic'; + } + } +} + +/// 选项配置类 +class GeneratorOptions { + final bool generateEndpoints; + final bool generateModels; + final bool generateDocs; + final bool useSimpleModels; + final bool separateModelFiles; + final String modelsDirectory; + final String outputDirectory; + final String endpointsFileName; + final String docsFileName; + + const GeneratorOptions({ + this.generateEndpoints = true, + this.generateModels = true, + this.generateDocs = true, + this.useSimpleModels = false, + this.separateModelFiles = true, + this.modelsDirectory = 'models', + this.outputDirectory = 'generator', + this.endpointsFileName = 'api_paths.dart', + this.docsFileName = 'api_documentation.md', + }); + + /// 从命令行参数创建选项 + factory GeneratorOptions.fromArgs(List args) { + bool generateEndpoints = false; + bool generateModels = false; + bool generateDocs = false; + bool useSimpleModels = false; + bool separateModelFiles = true; + String modelsDirectory = 'models'; + String outputDirectory = 'generator'; + String endpointsFileName = 'api_paths.dart'; + String docsFileName = 'api_documentation.md'; + + bool hasSpecificOption = false; + + for (int i = 0; i < args.length; i++) { + final arg = args[i]; + + switch (arg) { + case '--endpoints': + generateEndpoints = true; + hasSpecificOption = true; + break; + case '--models': + generateModels = true; + hasSpecificOption = true; + break; + case '--docs': + generateDocs = true; + hasSpecificOption = true; + break; + case '--all': + generateEndpoints = true; + generateModels = true; + generateDocs = true; + hasSpecificOption = true; + break; + case '--simple': + useSimpleModels = true; + break; + case '--models-dir': + if (i + 1 < args.length) { + modelsDirectory = args[i + 1]; + i++; // 跳过下一个参数 + } + break; + case '--output-dir': + if (i + 1 < args.length) { + outputDirectory = args[i + 1]; + i++; // 跳过下一个参数 + } + break; + case '--endpoints-file': + if (i + 1 < args.length) { + endpointsFileName = args[i + 1]; + i++; // 跳过下一个参数 + } + break; + case '--docs-file': + if (i + 1 < args.length) { + docsFileName = args[i + 1]; + i++; // 跳过下一个参数 + } + break; + } + } + + // 如果没有指定特定选项,默认生成所有文件 + if (!hasSpecificOption) { + generateEndpoints = true; + generateModels = true; + generateDocs = true; + } + + return GeneratorOptions( + generateEndpoints: generateEndpoints, + generateModels: generateModels, + generateDocs: generateDocs, + useSimpleModels: useSimpleModels, + separateModelFiles: separateModelFiles, + modelsDirectory: modelsDirectory, + outputDirectory: outputDirectory, + endpointsFileName: endpointsFileName, + docsFileName: docsFileName, + ); + } +} diff --git a/lib/generators/documentation_generator.dart b/lib/generators/documentation_generator.dart new file mode 100644 index 0000000..e171573 --- /dev/null +++ b/lib/generators/documentation_generator.dart @@ -0,0 +1,702 @@ +import '../core/models.dart'; +import '../utils/string_utils.dart'; +import 'base_generator.dart'; + +/// 文档生成器 +/// 负责生成API文档 +class DocumentationGenerator extends BaseGenerator { + final SwaggerDocument document; + final bool includeExamples; + final bool includeSchemas; + final bool includeResponses; + final String? customTitle; + + DocumentationGenerator( + this.document, { + this.includeExamples = true, + this.includeSchemas = true, + this.includeResponses = true, + this.customTitle, + }); + + @override + String get generatorType => 'DocumentationGenerator'; + + @override + String generate() { + final buffer = StringBuffer(); + + // 生成文档头部 + _generateHeader(buffer); + + // 生成目录 + _generateTableOfContents(buffer); + + // 生成API概述 + _generateApiOverview(buffer); + + // 生成认证信息 + _generateAuthenticationInfo(buffer); + + // 生成API端点文档 + _generateEndpointsDocumentation(buffer); + + // 生成数据模型文档 + if (includeSchemas) { + _generateSchemasDocumentation(buffer); + } + + // 生成错误代码文档 + _generateErrorCodesDocumentation(buffer); + + // 生成示例代码 + if (includeExamples) { + _generateExamplesDocumentation(buffer); + } + + // 生成更新日志 + _generateChangeLog(buffer); + + return generateTypeCheckedCode(buffer.toString()); + } + + /// 生成文档头部 + void _generateHeader(StringBuffer buffer) { + final title = customTitle ?? document.title; + + buffer.writeln('# $title'); + buffer.writeln(''); + + if (document.description.isNotEmpty) { + buffer.writeln('${document.description}'); + buffer.writeln(''); + } + + buffer.writeln('**版本**: ${document.version}'); + buffer.writeln('**基础URL**: ${_getBaseUrl()}'); + buffer.writeln('**生成时间**: ${DateTime.now().toIso8601String()}'); + buffer.writeln(''); + + // 生成徽章 + buffer.writeln( + '![API版本](https://img.shields.io/badge/API-${document.version}-blue.svg)'); + buffer.writeln('![状态](https://img.shields.io/badge/状态-活跃-green.svg)'); + buffer.writeln(''); + } + + /// 生成目录 + void _generateTableOfContents(StringBuffer buffer) { + buffer.writeln('## 📋 目录'); + buffer.writeln(''); + buffer.writeln('- [API概述](#api概述)'); + buffer.writeln('- [认证](#认证)'); + buffer.writeln('- [API端点](#api端点)'); + + // 按控制器分组的端点 + final controllerGroups = _groupPathsByController(); + for (final controllerName in controllerGroups.keys) { + final anchor = controllerName.toLowerCase().replaceAll(' ', '-'); + buffer.writeln(' - [$controllerName](#$anchor)'); + } + + if (includeSchemas) { + buffer.writeln('- [数据模型](#数据模型)'); + } + + buffer.writeln('- [错误代码](#错误代码)'); + + if (includeExamples) { + buffer.writeln('- [示例代码](#示例代码)'); + } + + buffer.writeln('- [更新日志](#更新日志)'); + buffer.writeln(''); + } + + /// 生成API概述 + void _generateApiOverview(StringBuffer buffer) { + buffer.writeln('## 🚀 API概述'); + buffer.writeln(''); + + // 统计信息 + final stats = _generateStats(); + buffer.writeln('### 📊 统计信息'); + buffer.writeln(''); + buffer.writeln('- **总端点数**: ${stats['totalEndpoints']}'); + buffer.writeln('- **控制器数**: ${stats['controllersCount']}'); + buffer.writeln('- **数据模型数**: ${stats['modelsCount']}'); + buffer.writeln(''); + + // HTTP方法统计 + final methodStats = stats['methodStats'] as Map; + buffer.writeln('### 🔗 HTTP方法分布'); + buffer.writeln(''); + for (final entry in methodStats.entries) { + final method = entry.key; + final count = entry.value; + final percentage = + ((count / stats['totalEndpoints']) * 100).toStringAsFixed(1); + buffer.writeln('- **$method**: $count个 ($percentage%)'); + } + buffer.writeln(''); + + // 支持的格式 + buffer.writeln('### 📝 支持的格式'); + buffer.writeln(''); + buffer.writeln('**请求格式**:'); + for (final format in document.consumes) { + buffer.writeln('- `$format`'); + } + buffer.writeln(''); + + buffer.writeln('**响应格式**:'); + for (final format in document.produces) { + buffer.writeln('- `$format`'); + } + buffer.writeln(''); + } + + /// 生成认证信息 + void _generateAuthenticationInfo(StringBuffer buffer) { + buffer.writeln('## 🔐 认证'); + buffer.writeln(''); + buffer.writeln('本API使用以下认证方式:'); + buffer.writeln(''); + buffer.writeln('### Bearer Token'); + buffer.writeln(''); + buffer.writeln('在请求头中包含Authorization字段:'); + buffer.writeln(''); + buffer.writeln('```'); + buffer.writeln('Authorization: Bearer YOUR_TOKEN_HERE'); + buffer.writeln('```'); + buffer.writeln(''); + buffer.writeln('### 获取Token'); + buffer.writeln(''); + buffer.writeln('请使用登录接口获取访问令牌。'); + buffer.writeln(''); + } + + /// 生成端点文档 + void _generateEndpointsDocumentation(StringBuffer buffer) { + buffer.writeln('## 📡 API端点'); + buffer.writeln(''); + + final controllerGroups = _groupPathsByController(); + + for (final entry in controllerGroups.entries) { + final controllerName = entry.key; + final paths = entry.value; + + buffer.writeln('### $controllerName'); + buffer.writeln(''); + + // 按HTTP方法和路径排序 + paths.sort((a, b) { + final methodOrder = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']; + final aIndex = methodOrder.indexOf(a.method.value); + final bIndex = methodOrder.indexOf(b.method.value); + + if (aIndex != bIndex) { + return aIndex.compareTo(bIndex); + } + + return a.path.compareTo(b.path); + }); + + for (final path in paths) { + _generateEndpointDocumentation(buffer, path); + } + + buffer.writeln(''); + } + } + + /// 生成单个端点文档 + void _generateEndpointDocumentation(StringBuffer buffer, ApiPath path) { + // 端点标题 + final title = path.summary.isNotEmpty ? path.summary : path.operationId; + buffer.writeln('#### ${path.method.value} ${path.path}'); + buffer.writeln(''); + + if (title.isNotEmpty) { + buffer.writeln('**$title**'); + buffer.writeln(''); + } + + // 描述 + if (path.description.isNotEmpty) { + buffer.writeln(path.description); + buffer.writeln(''); + } + + // 标签 + if (path.tags.isNotEmpty) { + buffer.writeln('**标签**: ${path.tags.join(', ')}'); + buffer.writeln(''); + } + + // 参数 + if (path.parameters.isNotEmpty) { + buffer.writeln('**参数**:'); + buffer.writeln(''); + + // 按参数位置分组 + final paramGroups = >{}; + for (final param in path.parameters) { + paramGroups.putIfAbsent(param.location, () => []).add(param); + } + + for (final entry in paramGroups.entries) { + final location = entry.key; + final params = entry.value; + + buffer.writeln('*${_getLocationName(location)}参数*:'); + buffer.writeln(''); + + buffer.writeln('| 参数名 | 类型 | 必填 | 描述 | 示例 |'); + buffer.writeln('|--------|------|------|------|------|'); + + for (final param in params) { + final required = param.required ? '✅' : '❌'; + final example = param.example?.toString() ?? '-'; + final description = + param.description.isNotEmpty ? param.description : '-'; + + buffer.writeln( + '| ${param.name} | ${param.type.value} | $required | $description | $example |'); + } + + buffer.writeln(''); + } + } + + // 响应 + if (includeResponses && path.responses.isNotEmpty) { + buffer.writeln('**响应**:'); + buffer.writeln(''); + + for (final entry in path.responses.entries) { + final code = entry.key; + final response = entry.value; + + buffer.writeln('*HTTP $code*:'); + if (response.description.isNotEmpty) { + buffer.writeln('- ${response.description}'); + } + buffer.writeln(''); + } + } + + // 示例 + if (includeExamples) { + _generateEndpointExample(buffer, path); + } + + buffer.writeln('---'); + buffer.writeln(''); + } + + /// 生成端点示例 + void _generateEndpointExample(StringBuffer buffer, ApiPath path) { + buffer.writeln('**示例**:'); + buffer.writeln(''); + + // cURL示例 + buffer.writeln('```bash'); + buffer.write('curl -X ${path.method.value} '); + buffer.write('${_getBaseUrl()}${path.path}'); + + if (path.parameters.any((p) => p.location == ParameterLocation.header)) { + buffer.write(' \\'); + buffer.writeln(''); + buffer.write(' -H "Authorization: Bearer YOUR_TOKEN"'); + } + + if (path.method == HttpMethod.post || path.method == HttpMethod.put) { + buffer.write(' \\'); + buffer.writeln(''); + buffer.write(' -H "Content-Type: application/json"'); + buffer.write(' \\'); + buffer.writeln(''); + buffer.write(' -d \'{"key": "value"}\''); + } + + buffer.writeln(''); + buffer.writeln('```'); + buffer.writeln(''); + + // Dart示例 + buffer.writeln('```dart'); + buffer.writeln('import \'dart:convert\';'); + buffer.writeln('import \'package:http/http.dart\' as http;'); + buffer.writeln(''); + buffer.writeln('class ApiClient {'); + buffer.writeln(' static const String baseUrl = \'${_getBaseUrl()}\';'); + buffer.writeln(' String? _token;'); + buffer.writeln(''); + buffer.writeln(' void setToken(String token) {'); + buffer.writeln(' _token = token;'); + buffer.writeln(' }'); + buffer.writeln(''); + buffer.writeln(' Map get _headers => {'); + buffer.writeln(' \'Content-Type\': \'application/json\','); + buffer.writeln( + ' if (_token != null) \'Authorization\': \'Bearer \$_token\','); + buffer.writeln(' };'); + buffer.writeln(''); + buffer + .writeln(' Future> get(String endpoint) async {'); + buffer.writeln(' final response = await http.get('); + buffer.writeln(' Uri.parse(\'\$baseUrl\$endpoint\'),'); + buffer.writeln(' headers: _headers,'); + buffer.writeln(' );'); + buffer.writeln(''); + buffer.writeln(' if (response.statusCode == 200) {'); + buffer.writeln(' return jsonDecode(response.body);'); + buffer.writeln(' } else {'); + buffer.writeln( + ' throw Exception(\'Failed to load data: \${response.statusCode}\');'); + buffer.writeln(' }'); + buffer.writeln(' }'); + buffer.writeln(''); + buffer.writeln( + ' Future> post(String endpoint, Map data) async {'); + buffer.writeln(' final response = await http.post('); + buffer.writeln(' Uri.parse(\'\$baseUrl\$endpoint\'),'); + buffer.writeln(' headers: _headers,'); + buffer.writeln(' body: jsonEncode(data),'); + buffer.writeln(' );'); + buffer.writeln(''); + buffer.writeln( + ' if (response.statusCode == 200 || response.statusCode == 201) {'); + buffer.writeln(' return jsonDecode(response.body);'); + buffer.writeln(' } else {'); + buffer.writeln( + ' throw Exception(\'Failed to post data: \${response.statusCode}\');'); + buffer.writeln(' }'); + buffer.writeln(' }'); + buffer.writeln('}'); + buffer.writeln('```'); + buffer.writeln(''); + } + + /// 生成数据模型文档 + void _generateSchemasDocumentation(StringBuffer buffer) { + if (document.models.isEmpty) return; + + buffer.writeln('## 📋 数据模型'); + buffer.writeln(''); + + final sortedModels = document.models.values.toList() + ..sort((a, b) => a.name.compareTo(b.name)); + + for (final model in sortedModels) { + _generateModelDocumentation(buffer, model); + } + } + + /// 生成模型文档 + void _generateModelDocumentation(StringBuffer buffer, ApiModel model) { + buffer.writeln('### ${model.name}'); + buffer.writeln(''); + + if (model.description.isNotEmpty) { + buffer.writeln(model.description); + buffer.writeln(''); + } + + if (model.isEnum) { + buffer.writeln('**枚举值**:'); + buffer.writeln(''); + + for (final value in model.enumValues) { + buffer.writeln('- `$value`'); + } + + buffer.writeln(''); + } else { + buffer.writeln('**属性**:'); + buffer.writeln(''); + + if (model.properties.isNotEmpty) { + buffer.writeln('| 属性名 | 类型 | 必填 | 描述 |'); + buffer.writeln('|--------|------|------|------|'); + + for (final entry in model.properties.entries) { + final propName = entry.key; + final prop = entry.value; + + final required = model.required.contains(propName) ? '✅' : '❌'; + final type = _getPropertyTypeDescription(prop); + final description = + prop.description.isNotEmpty ? prop.description : '-'; + + buffer.writeln('| $propName | $type | $required | $description |'); + } + } + + buffer.writeln(''); + } + + // JSON示例 + if (includeExamples) { + buffer.writeln('**JSON示例**:'); + buffer.writeln(''); + buffer.writeln('```json'); + buffer.writeln(_generateModelExample(model)); + buffer.writeln('```'); + buffer.writeln(''); + } + + buffer.writeln('---'); + buffer.writeln(''); + } + + /// 生成错误代码文档 + void _generateErrorCodesDocumentation(StringBuffer buffer) { + buffer.writeln('## ❌ 错误代码'); + buffer.writeln(''); + + // 标准HTTP状态码 + final errorCodes = { + '400': '请求参数错误', + '401': '未授权访问', + '403': '禁止访问', + '404': '资源不存在', + '405': '方法不允许', + '422': '参数验证失败', + '500': '服务器内部错误', + '502': '网关错误', + '503': '服务不可用', + }; + + buffer.writeln('| 状态码 | 描述 |'); + buffer.writeln('|--------|------|'); + + for (final entry in errorCodes.entries) { + buffer.writeln('| ${entry.key} | ${entry.value} |'); + } + + buffer.writeln(''); + + // 错误响应格式 + buffer.writeln('### 错误响应格式'); + buffer.writeln(''); + buffer.writeln('```json'); + buffer.writeln('{'); + buffer.writeln(' "error": {'); + buffer.writeln(' "code": "ERROR_CODE",'); + buffer.writeln(' "message": "错误描述",'); + buffer.writeln(' "details": "详细信息"'); + buffer.writeln(' }'); + buffer.writeln('}'); + buffer.writeln('```'); + buffer.writeln(''); + } + + /// 生成示例代码 + void _generateExamplesDocumentation(StringBuffer buffer) { + buffer.writeln('## 💡 示例代码'); + buffer.writeln(''); + + // Dart HTTP客户端示例 + buffer.writeln('### Dart HTTP客户端'); + buffer.writeln(''); + buffer.writeln('```dart'); + buffer.writeln('import \'dart:convert\';'); + buffer.writeln('import \'package:http/http.dart\' as http;'); + buffer.writeln(''); + buffer.writeln('class ApiClient {'); + buffer.writeln(' static const String baseUrl = \'${_getBaseUrl()}\';'); + buffer.writeln(' String? _token;'); + buffer.writeln(''); + buffer.writeln(' void setToken(String token) {'); + buffer.writeln(' _token = token;'); + buffer.writeln(' }'); + buffer.writeln(''); + buffer.writeln(' Map get _headers => {'); + buffer.writeln(' \'Content-Type\': \'application/json\','); + buffer.writeln( + ' if (_token != null) \'Authorization\': \'Bearer \$_token\','); + buffer.writeln(' };'); + buffer.writeln(''); + buffer + .writeln(' Future> get(String endpoint) async {'); + buffer.writeln(' final response = await http.get('); + buffer.writeln(' Uri.parse(\'\$baseUrl\$endpoint\'),'); + buffer.writeln(' headers: _headers,'); + buffer.writeln(' );'); + buffer.writeln(''); + buffer.writeln(' if (response.statusCode == 200) {'); + buffer.writeln(' return jsonDecode(response.body);'); + buffer.writeln(' } else {'); + buffer.writeln( + ' throw Exception(\'Failed to load data: \${response.statusCode}\');'); + buffer.writeln(' }'); + buffer.writeln(' }'); + buffer.writeln(''); + buffer.writeln( + ' Future> post(String endpoint, Map data) async {'); + buffer.writeln(' final response = await http.post('); + buffer.writeln(' Uri.parse(\'\$baseUrl\$endpoint\'),'); + buffer.writeln(' headers: _headers,'); + buffer.writeln(' body: jsonEncode(data),'); + buffer.writeln(' );'); + buffer.writeln(''); + buffer.writeln( + ' if (response.statusCode == 200 || response.statusCode == 201) {'); + buffer.writeln(' return jsonDecode(response.body);'); + buffer.writeln(' } else {'); + buffer.writeln( + ' throw Exception(\'Failed to post data: \${response.statusCode}\');'); + buffer.writeln(' }'); + buffer.writeln(' }'); + buffer.writeln('}'); + buffer.writeln('```'); + buffer.writeln(''); + } + + /// 生成更新日志 + void _generateChangeLog(StringBuffer buffer) { + buffer.writeln('## 📝 更新日志'); + buffer.writeln(''); + + buffer.writeln( + '### ${document.version} - ${DateTime.now().toIso8601String().split('T')[0]}'); + buffer.writeln(''); + buffer.writeln('- 🎉 初始版本发布'); + buffer.writeln('- 📡 ${document.paths.length} 个API端点'); + buffer.writeln('- 📋 ${document.models.length} 个数据模型'); + buffer.writeln('- 🔧 完整的API文档'); + buffer.writeln(''); + + buffer.writeln('---'); + buffer.writeln(''); + buffer.writeln('*文档由 Swagger CLI By Max 自动生成*'); + buffer.writeln(''); + } + + /// 按控制器分组路径 + Map> _groupPathsByController() { + final groups = >{}; + + for (final path in document.paths.values) { + final controllerName = StringUtils.extractControllerName(path); + groups.putIfAbsent(controllerName, () => []).add(path); + } + + return groups; + } + + // 已移动到 StringUtils.extractControllerName + + /// 获取基础URL + String _getBaseUrl() { + return document.schemes.isNotEmpty + ? '${document.schemes.first}://${document.host}${document.basePath}' + : 'https://${document.host}${document.basePath}'; + } + + /// 获取参数位置名称 + String _getLocationName(ParameterLocation location) { + switch (location) { + case ParameterLocation.query: + return '查询'; + case ParameterLocation.path: + return '路径'; + case ParameterLocation.header: + return '请求头'; + case ParameterLocation.body: + return '请求体'; + case ParameterLocation.form: + return '表单'; + case ParameterLocation.cookie: + return 'Cookie'; + } + } + + /// 获取属性类型描述 + String _getPropertyTypeDescription(ApiProperty prop) { + String baseType = prop.type.value; + + if (prop.format != null) { + baseType += ' (${prop.format})'; + } + + if (prop.nullable) { + baseType += '?'; + } + + return baseType; + } + + /// 生成模型示例 + String _generateModelExample(ApiModel model) { + if (model.isEnum) { + return '"${model.enumValues.first}"'; + } + + final buffer = StringBuffer(); + buffer.writeln('{'); + + final properties = model.properties.entries.toList(); + for (int i = 0; i < properties.length; i++) { + final entry = properties[i]; + final propName = entry.key; + final prop = entry.value; + + final exampleValue = _generatePropertyExample(prop); + buffer.write(' "$propName": $exampleValue'); + + if (i < properties.length - 1) { + buffer.write(','); + } + + buffer.writeln(); + } + + buffer.write('}'); + return buffer.toString(); + } + + /// 生成属性示例 + String _generatePropertyExample(ApiProperty prop) { + switch (prop.type) { + case PropertyType.string: + return '"string"'; + case PropertyType.integer: + return '0'; + case PropertyType.number: + return '0.0'; + case PropertyType.boolean: + return 'true'; + case PropertyType.array: + return '[]'; + case PropertyType.object: + return '{}'; + case PropertyType.reference: + return '{}'; + default: + return 'null'; + } + } + + /// 生成统计信息 + Map _generateStats() { + final stats = {}; + + stats['totalEndpoints'] = document.paths.length; + stats['controllersCount'] = _groupPathsByController().length; + stats['modelsCount'] = document.models.length; + + // HTTP方法统计 + final methodStats = {}; + for (final path in document.paths.values) { + final method = path.method.value; + methodStats[method] = (methodStats[method] ?? 0) + 1; + } + stats['methodStats'] = methodStats; + + return stats; + } +} diff --git a/lib/generators/endpoint_code_generator.dart b/lib/generators/endpoint_code_generator.dart new file mode 100644 index 0000000..44e6c0d --- /dev/null +++ b/lib/generators/endpoint_code_generator.dart @@ -0,0 +1,257 @@ +import '../core/models.dart'; +import '../utils/string_utils.dart'; +import 'base_generator.dart'; + +/// 端点代码生成器 +/// 负责生成API端点常量代码 +class EndpointCodeGenerator extends BaseGenerator { + final SwaggerDocument document; + final bool includeBaseUrl; + final String? customBaseUrl; + + EndpointCodeGenerator( + this.document, { + this.includeBaseUrl = true, + this.customBaseUrl, + }); + + @override + String get generatorType => 'EndpointCodeGenerator'; + + @override + String generate() { + final buffer = StringBuffer(); + + // 生成文件头 + buffer.writeln(generateFileHeader('API 端点常量定义')); + buffer.writeln(''); + + // 生成端点类 + buffer.writeln('/// API路径常量定义'); + buffer.writeln('/// 统一管理所有API端点路径,便于维护和修改'); + buffer.writeln('class ApiPaths {'); + buffer.writeln(' ApiPaths._(); // 私有构造函数,防止实例化'); + buffer.writeln(''); + + // 生成基础URL常量 + if (includeBaseUrl) { + final baseUrl = customBaseUrl ?? + (document.schemes.isNotEmpty + ? '${document.schemes.first}://${document.host}${document.basePath}' + : 'https://${document.host}${document.basePath}'); + + buffer.writeln(' /// 基础URL'); + buffer.writeln(' static const String baseUrl = \'$baseUrl\';'); + buffer.writeln(''); + } + + // 按控制器分组生成端点 + final controllerGroups = _groupPathsByController(); + + for (final entry in controllerGroups.entries) { + final controllerName = entry.key; + final paths = entry.value; + + buffer.writeln(' // ${controllerName}相关端点'); + + for (final path in paths) { + final constantName = _generateConstantName(path); + final cleanPath = StringUtils.cleanPath(path.path); + + // 生成注释 + if (path.summary.isNotEmpty) { + buffer.writeln(' /// ${path.summary}'); + } + if (path.description.isNotEmpty && path.description != path.summary) { + buffer.writeln(' /// ${path.description}'); + } + + buffer.writeln(' static const String $constantName = \'$cleanPath\';'); + buffer.writeln(''); + } + + buffer.writeln(''); + } + + // 生成所有端点的列表 + buffer.writeln(' /// 所有端点列表'); + buffer.writeln(' static const List allEndpoints = ['); + + for (final entry in controllerGroups.entries) { + final paths = entry.value; + for (final path in paths) { + final constantName = _generateConstantName(path); + buffer.writeln(' $constantName,'); + } + } + + buffer.writeln(' ];'); + buffer.writeln(''); + + // 生成HTTP方法常量 + buffer.writeln(' /// HTTP方法常量'); + buffer.writeln(' static const Map httpMethods = {'); + + for (final entry in controllerGroups.entries) { + final paths = entry.value; + for (final path in paths) { + final constantName = _generateConstantName(path); + buffer.writeln(' \'$constantName\': \'${path.method.value}\','); + } + } + + buffer.writeln(' };'); + buffer.writeln(''); + + // 生成完整URL构建方法 + if (includeBaseUrl) { + buffer.writeln(' /// 构建完整URL'); + buffer.writeln( + ' static String buildUrl(String endpoint, {Map? params}) {'); + buffer.writeln(' String url = baseUrl + endpoint;'); + buffer.writeln(' '); + buffer.writeln(' if (params != null && params.isNotEmpty) {'); + buffer.writeln(' final queryParams = [];'); + buffer.writeln(' params.forEach((key, value) {'); + buffer.writeln(' if (value != null) {'); + buffer.writeln( + ' queryParams.add(\'\\\${Uri.encodeComponent(key)}=\\\${Uri.encodeComponent(value.toString())}\');'); + buffer.writeln(' }'); + buffer.writeln(' });'); + buffer.writeln(' '); + buffer.writeln(' if (queryParams.isNotEmpty) {'); + buffer.writeln(' url += \'?\' + queryParams.join(\'&\');'); + buffer.writeln(' }'); + buffer.writeln(' }'); + buffer.writeln(' '); + buffer.writeln(' return url;'); + buffer.writeln(' }'); + buffer.writeln(''); + + // 生成路径参数替换方法 + buffer.writeln(' /// 替换路径参数'); + buffer.writeln( + ' static String replacePathParams(String endpoint, Map params) {'); + buffer.writeln(' String result = endpoint;'); + buffer.writeln(' params.forEach((key, value) {'); + buffer.writeln( + ' result = result.replaceAll(\'{\\\$key}\', value.toString());'); + buffer.writeln(' });'); + buffer.writeln(' return result;'); + buffer.writeln(' }'); + buffer.writeln(''); + } + + // 生成端点验证方法 + buffer.writeln(' /// 验证端点是否存在'); + buffer.writeln(' static bool isValidEndpoint(String endpoint) {'); + buffer.writeln(' return allEndpoints.contains(endpoint);'); + buffer.writeln(' }'); + buffer.writeln(''); + + // 生成获取HTTP方法的方法 + buffer.writeln(' /// 获取端点的HTTP方法'); + buffer.writeln(' static String? getHttpMethod(String endpoint) {'); + buffer.writeln(' return httpMethods[endpoint];'); + buffer.writeln(' }'); + buffer.writeln(''); + + buffer.writeln('}'); + + // 生成枚举类型的端点定义(可选) + buffer.writeln(''); + buffer.writeln('/// API端点枚举'); + buffer.writeln('/// 提供类型安全的端点访问'); + buffer.writeln('enum ApiEndpoint {'); + + for (final entry in controllerGroups.entries) { + final paths = entry.value; + for (final path in paths) { + final enumName = _generateEnumName(path); + final constantName = _generateConstantName(path); + + if (path.summary.isNotEmpty) { + buffer.writeln(' /// ${path.summary}'); + } + buffer.writeln( + ' $enumName(ApiPaths.$constantName, \'${path.method.value}\'),'); + } + } + + buffer.writeln(';'); + buffer.writeln(''); + + // 生成枚举的构造函数和方法 + buffer.writeln(' const ApiEndpoint(this.path, this.method);'); + buffer.writeln(''); + buffer.writeln(' /// 端点路径'); + buffer.writeln(' final String path;'); + buffer.writeln(''); + buffer.writeln(' /// HTTP方法'); + buffer.writeln(' final String method;'); + buffer.writeln(''); + + if (includeBaseUrl) { + buffer.writeln(' /// 获取完整URL'); + buffer.writeln(' String get fullUrl => ApiPaths.baseUrl + path;'); + buffer.writeln(''); + } + + buffer.writeln(' /// 根据路径查找端点'); + buffer.writeln(' static ApiEndpoint? findByPath(String path) {'); + buffer.writeln(' for (final endpoint in values) {'); + buffer.writeln(' if (endpoint.path == path) {'); + buffer.writeln(' return endpoint;'); + buffer.writeln(' }'); + buffer.writeln(' }'); + buffer.writeln(' return null;'); + buffer.writeln(' }'); + buffer.writeln(''); + + buffer.writeln(' /// 根据HTTP方法过滤端点'); + buffer + .writeln(' static List filterByMethod(String method) {'); + buffer.writeln( + ' return values.where((endpoint) => endpoint.method == method).toList();'); + buffer.writeln(' }'); + buffer.writeln(''); + + buffer.writeln('}'); + + return generateTypeCheckedCode(buffer.toString()); + } + + /// 按控制器分组路径 + Map> _groupPathsByController() { + final groups = >{}; + + for (final path in document.paths.values) { + final controllerName = StringUtils.extractControllerName(path); + groups.putIfAbsent(controllerName, () => []).add(path); + } + + return groups; + } + + // 已移动到 StringUtils.extractControllerName + + /// 生成常量名称 + String _generateConstantName(ApiPath path) { + final baseName = + StringUtils.generateEndpointName(path.path, path.operationId); + final methodPrefix = path.method.value.toLowerCase(); + + return StringUtils.toCamelCase('${methodPrefix}_$baseName'); + } + + /// 生成枚举名称 + String _generateEnumName(ApiPath path) { + final baseName = + StringUtils.generateEndpointName(path.path, path.operationId); + final methodPrefix = path.method.value.toLowerCase(); + + return StringUtils.toCamelCase('${methodPrefix}_$baseName'); + } + + // 已移动到 StringUtils.cleanPath +} diff --git a/lib/generators/model_code_generator.dart b/lib/generators/model_code_generator.dart new file mode 100644 index 0000000..5a24a32 --- /dev/null +++ b/lib/generators/model_code_generator.dart @@ -0,0 +1,682 @@ +import '../core/models.dart'; +import '../utils/string_utils.dart'; +import 'base_generator.dart'; + +/// 模型代码生成器 +/// 负责生成Dart模型类代码 +class ModelCodeGenerator extends ModelGenerator { + ModelCodeGenerator(super.document, {super.useSimpleModels}); + + @override + String get generatorType => 'ModelCodeGenerator'; + + @override + String generate() { + final buffer = StringBuffer(); + + // 生成文件头 + buffer.writeln(generateFileHeader('API 数据模型定义')); + buffer.writeln(''); + + if (!useSimpleModels) { + buffer.writeln( + 'import \'package:json_annotation/json_annotation.dart\';', + ); + buffer.writeln(''); + } + + // 生成所有模型 + final models = document.models.values.toList(); + for (int i = 0; i < models.length; i++) { + final model = models[i]; + buffer.writeln(generateModelCode(model)); + + // 添加模型间的分隔符 + if (i < models.length - 1) { + buffer.writeln(''); + buffer.writeln('// ${'=' * 60}'); + buffer.writeln(''); + } + } + + return generateTypeCheckedCode(buffer.toString()); + } + + @override + String generateModelCode(ApiModel model) { + if (model.isEnum) { + return generateEnumCode(model); + } + + return useSimpleModels + ? generateSimpleModelCode(model) + : generateAnnotatedModelCode(model); + } + + /// 生成简洁版模型代码 + String generateSimpleModelCode(ApiModel model) { + final className = StringUtils.generateClassName(model.name); + final buffer = StringBuffer(); + + // 生成导入依赖 + final importedTypes = getImportedTypes(model); + for (final importType in importedTypes) { + final importFileName = StringUtils.generateFileName(importType); + buffer.writeln('import \'$importFileName\';'); + } + + if (importedTypes.isNotEmpty) { + buffer.writeln(''); + } + + // 生成类注释 + if (model.description.isNotEmpty) { + buffer.writeln(StringUtils.generateComment(model.description)); + } + + buffer.writeln('class $className {'); + + // 生成属性 + model.properties.forEach((propName, property) { + final dartType = getDartPropertyType(property); + final nullable = property.nullable ? '?' : ''; + final dartPropName = StringUtils.toDartPropertyName(propName); + + if (property.description.isNotEmpty) { + buffer.writeln( + ' ${StringUtils.generateComment(property.description)}', + ); + } + + buffer.writeln(' final $dartType$nullable $dartPropName;'); + buffer.writeln(''); + }); + + // 生成构造函数 + if (model.properties.isEmpty) { + buffer.writeln(' const $className();'); + } else { + buffer.writeln(' const $className({'); + model.properties.forEach((propName, property) { + final dartPropName = StringUtils.toDartPropertyName(propName); + final required = property.required ? 'required ' : ''; + buffer.writeln(' ${required}this.$dartPropName,'); + }); + buffer.writeln(' });'); + } + buffer.writeln(''); + + // 生成 fromJson 方法 + buffer.writeln( + ' factory $className.fromJson(Map json) {', + ); + if (model.properties.isEmpty) { + buffer.writeln(' return const $className();'); + } else { + buffer.writeln(' return $className('); + model.properties.forEach((propName, property) { + final dartPropName = StringUtils.toDartPropertyName(propName); + final dartType = getDartPropertyType(property); + + buffer.write(' $dartPropName: '); + + // 生成类型转换逻辑 + if (property.type == PropertyType.reference && + property.reference != null) { + final refType = StringUtils.generateClassName(property.reference!); + if (property.nullable) { + buffer.write( + 'json[\'$propName\'] != null ? $refType.fromJson(json[\'$propName\']) : null', + ); + } else { + buffer.write('$refType.fromJson(json[\'$propName\'])'); + } + } else if (property.type == PropertyType.array) { + // 简化的数组处理 + buffer.write( + 'json[\'$propName\'] != null ? List.from(json[\'$propName\']) : null', + ); + } else { + // 基本类型 + if (property.nullable) { + buffer.write('json[\'$propName\'] as $dartType?'); + } else { + buffer.write('json[\'$propName\'] as $dartType'); + } + } + + buffer.writeln(','); + }); + buffer.writeln(' );'); + } + buffer.writeln(' }'); + buffer.writeln(''); + + // 生成 toJson 方法 + buffer.writeln(' Map toJson() {'); + buffer.writeln(' return {'); + model.properties.forEach((propName, property) { + final dartPropName = StringUtils.toDartPropertyName(propName); + + if (property.type == PropertyType.reference && + property.reference != null) { + buffer.write(' \'$propName\': $dartPropName?.toJson()'); + } else if (property.type == PropertyType.array) { + buffer.write(' \'$propName\': $dartPropName'); + } else { + buffer.write(' \'$propName\': $dartPropName'); + } + + buffer.writeln(','); + }); + buffer.writeln(' };'); + buffer.writeln(' }'); + buffer.writeln(''); + + // 生成 copyWith 方法 + if (model.properties.isNotEmpty) { + buffer.writeln(' $className copyWith({'); + model.properties.forEach((propName, property) { + final dartType = getDartPropertyType(property); + final dartPropName = StringUtils.toDartPropertyName(propName); + buffer.writeln(' $dartType? $dartPropName,'); + }); + buffer.writeln(' }) {'); + buffer.writeln(' return $className('); + model.properties.forEach((propName, property) { + final dartPropName = StringUtils.toDartPropertyName(propName); + buffer.writeln( + ' $dartPropName: $dartPropName ?? this.$dartPropName,', + ); + }); + buffer.writeln(' );'); + buffer.writeln(' }'); + buffer.writeln(''); + } + + buffer.writeln('}'); + + return buffer.toString(); + } + + /// 生成带注解的模型代码 + String generateAnnotatedModelCode(ApiModel model) { + final className = StringUtils.generateClassName(model.name); + final buffer = StringBuffer(); + + // 生成导入依赖 + final importedTypes = getImportedTypes(model); + for (final importType in importedTypes) { + final importFileName = StringUtils.generateFileName(importType); + buffer.writeln('import \'$importFileName\';'); + } + + if (importedTypes.isNotEmpty) { + buffer.writeln(''); + } + + // 生成 part 声明 + final partFileName = StringUtils.generateFileName(model.name); + final generatedPart = partFileName.replaceAll('.dart', '.g.dart'); + buffer.writeln('part \'$generatedPart\';'); + buffer.writeln(''); + + // 生成类注释 + if (model.description.isNotEmpty) { + buffer.writeln(StringUtils.generateComment(model.description)); + } + + buffer.writeln('@JsonSerializable()'); + buffer.writeln('class $className {'); + + // 生成属性 + model.properties.forEach((propName, property) { + final dartType = getDartPropertyType(property); + final nullable = property.nullable ? '?' : ''; + final dartPropName = StringUtils.toDartPropertyName(propName); + + if (property.description.isNotEmpty) { + buffer.writeln( + ' ${StringUtils.generateComment(property.description)}', + ); + } + + // 添加JsonKey注解 + final needsJsonKey = + _needsJsonKeyAnnotation(dartPropName, propName, property); + if (needsJsonKey.isNotEmpty) { + buffer.writeln(' @JsonKey($needsJsonKey)'); + } + + buffer.writeln(' final $dartType$nullable $dartPropName;'); + buffer.writeln(''); + }); + + // 生成构造函数 + if (model.properties.isEmpty) { + buffer.writeln(' const $className();'); + } else { + buffer.writeln(' const $className({'); + model.properties.forEach((propName, property) { + final dartPropName = StringUtils.toDartPropertyName(propName); + final required = property.required ? 'required ' : ''; + buffer.writeln(' ${required}this.$dartPropName,'); + }); + buffer.writeln(' });'); + } + buffer.writeln(''); + + // 生成 fromJson 工厂方法 + buffer.writeln( + ' factory $className.fromJson(Map json) => _\$${className}FromJson(json);', + ); + buffer.writeln(''); + + // 生成 toJson 方法 + buffer.writeln( + ' Map toJson() => _\$${className}ToJson(this);', + ); + buffer.writeln(''); + + buffer.writeln('}'); + + return buffer.toString(); + } + + /// 生成单独的模型文件 + Map generateSeparateModelFiles() { + final files = {}; + + // 生成所有模型文件 + for (final model in document.models.values) { + final fileName = StringUtils.generateFileName(model.name); + final content = generateSingleModelFile(model); + files[fileName] = content; + } + + // 生成 index.dart 文件 + final modelFileNames = document.models.keys + .map((name) => StringUtils.generateFileName(name)) + .toList(); + final indexContent = generateIndexFile(modelFileNames); + files['index.dart'] = indexContent; + + return files; + } + + /// 生成单个模型文件 + String generateSingleModelFile(ApiModel model) { + final buffer = StringBuffer(); + + // 生成文件头 + buffer.writeln(generateFileHeader('${model.name} 模型定义')); + buffer.writeln(''); + + // 枚举类需要导入 json_annotation 以使用 @JsonEnum 注解 + if (!useSimpleModels && model.isEnum) { + buffer.writeln( + 'import \'package:json_annotation/json_annotation.dart\';', + ); + buffer.writeln(''); + } + // 普通类且非简洁模式时导入 json_annotation + else if (!useSimpleModels && !model.isEnum) { + buffer.writeln( + 'import \'package:json_annotation/json_annotation.dart\';', + ); + buffer.writeln(''); + } + + // 生成导入依赖 - 统一使用 index.dart + final importedTypes = getImportedTypes(model); + if (importedTypes.isNotEmpty) { + buffer.writeln('import \'index.dart\';'); + buffer.writeln(''); + } + + // 生成模型代码,但不包含导入语句和文件头(因为已经在上面生成了) + buffer.writeln(_generateModelCodeWithoutImports(model)); + + return generateTypeCheckedCode(buffer.toString()); + } + + /// 生成模型代码(不包含导入语句) + String _generateModelCodeWithoutImports(ApiModel model) { + if (model.isEnum) { + return _generateEnumCodeWithoutImports(model); + } + + return useSimpleModels + ? _generateSimpleModelCodeWithoutImports(model) + : _generateAnnotatedModelCodeWithoutImports(model); + } + + /// 生成枚举代码(不包含导入语句) + String _generateEnumCodeWithoutImports(ApiModel model) { + final className = StringUtils.generateClassName(model.name); + final enumType = model.enumType?.value ?? 'string'; + final buffer = StringBuffer(); + + // 生成枚举类注释 + if (model.description.isNotEmpty) { + buffer.writeln(StringUtils.generateComment(model.description)); + } + + // 添加 @JsonEnum 注解 + buffer.writeln('@JsonEnum()'); + buffer.writeln('enum $className {'); + + // 生成枚举值 + for (int i = 0; i < model.enumValues.length; i++) { + final value = model.enumValues[i]; + final enumName = StringUtils.generateEnumValueName(value, i); + + if (enumType == 'integer' || enumType == 'number') { + buffer.writeln(' $enumName($value),'); + } else { + buffer.writeln(' $enumName(\'$value\'),'); + } + } + + // 移除最后一个逗号 + final content = buffer.toString().trimRight(); + buffer.clear(); + buffer.writeln(content.substring(0, content.lastIndexOf(','))); + buffer.writeln(';'); + buffer.writeln(''); + + // 生成构造函数和方法 + buffer.writeln(' const $className(this.value);'); + buffer.writeln( + ' final ${enumType == 'integer' || enumType == 'number' ? 'int' : 'String'} value;', + ); + buffer.writeln(''); + + // 生成 fromValue 方法 + buffer.writeln(' static $className fromValue(dynamic value) {'); + buffer.writeln(' for (final enumValue in $className.values) {'); + buffer.writeln(' if (enumValue.value == value) {'); + buffer.writeln(' return enumValue;'); + buffer.writeln(' }'); + buffer.writeln(' }'); + buffer.writeln(' throw ArgumentError(\'Unknown enum value: \$value\');'); + buffer.writeln(' }'); + buffer.writeln(''); + + // 生成 fromJson 方法 + buffer.writeln(' factory $className.fromJson(dynamic json) {'); + buffer.writeln(' return fromValue(json);'); + buffer.writeln(' }'); + buffer.writeln(''); + + // 生成 toJson 方法 + buffer.writeln(' dynamic toJson() => value;'); + buffer.writeln(''); + + buffer.writeln('}'); + + return buffer.toString(); + } + + // 已移动到 StringUtils.generateEnumValueName + + /// 生成简洁版模型代码(不包含导入语句) + String _generateSimpleModelCodeWithoutImports(ApiModel model) { + final className = StringUtils.generateClassName(model.name); + final buffer = StringBuffer(); + + // 生成类注释 + if (model.description.isNotEmpty) { + buffer.writeln(StringUtils.generateComment(model.description)); + } + + buffer.writeln('class $className {'); + + // 生成属性 + model.properties.forEach((propName, property) { + final dartType = getDartPropertyType(property); + final nullable = property.nullable ? '?' : ''; + final dartPropName = StringUtils.toDartPropertyName(propName); + + if (property.description.isNotEmpty) { + buffer.writeln( + ' ${StringUtils.generateComment(property.description)}', + ); + } + + buffer.writeln(' final $dartType$nullable $dartPropName;'); + buffer.writeln(''); + }); + + // 生成构造函数 + if (model.properties.isEmpty) { + buffer.writeln(' const $className();'); + } else { + buffer.writeln(' const $className({'); + model.properties.forEach((propName, property) { + final dartPropName = StringUtils.toDartPropertyName(propName); + final required = property.required ? 'required ' : ''; + buffer.writeln(' ${required}this.$dartPropName,'); + }); + buffer.writeln(' });'); + } + buffer.writeln(''); + + // 生成 fromJson 方法 + buffer.writeln( + ' factory $className.fromJson(Map json) {', + ); + if (model.properties.isEmpty) { + buffer.writeln(' return const $className();'); + } else { + buffer.writeln(' return $className('); + model.properties.forEach((propName, property) { + final dartPropName = StringUtils.toDartPropertyName(propName); + final dartType = getDartPropertyType(property); + + buffer.write(' $dartPropName: '); + + // 生成类型转换逻辑 + if (property.type == PropertyType.reference && + property.reference != null) { + final refType = StringUtils.generateClassName(property.reference!); + if (property.nullable) { + buffer.write( + 'json[\'$propName\'] != null ? $refType.fromJson(json[\'$propName\']) : null', + ); + } else { + buffer.write('$refType.fromJson(json[\'$propName\'])'); + } + } else if (property.type == PropertyType.array) { + // 简化的数组处理 + buffer.write( + 'json[\'$propName\'] != null ? List.from(json[\'$propName\']) : null', + ); + } else { + // 基本类型 + if (property.nullable) { + buffer.write('json[\'$propName\'] as $dartType?'); + } else { + buffer.write('json[\'$propName\'] as $dartType'); + } + } + + buffer.writeln(','); + }); + buffer.writeln(' );'); + } + buffer.writeln(' }'); + buffer.writeln(''); + + // 生成 toJson 方法 + buffer.writeln(' Map toJson() {'); + buffer.writeln(' return {'); + model.properties.forEach((propName, property) { + final dartPropName = StringUtils.toDartPropertyName(propName); + + if (property.type == PropertyType.reference && + property.reference != null) { + buffer.write(' \'$propName\': $dartPropName?.toJson()'); + } else if (property.type == PropertyType.array) { + buffer.write(' \'$propName\': $dartPropName'); + } else { + buffer.write(' \'$propName\': $dartPropName'); + } + + buffer.writeln(','); + }); + buffer.writeln(' };'); + buffer.writeln(' }'); + buffer.writeln(''); + + // 生成 copyWith 方法 + if (model.properties.isNotEmpty) { + buffer.writeln(' $className copyWith({'); + model.properties.forEach((propName, property) { + final dartType = getDartPropertyType(property); + final dartPropName = StringUtils.toDartPropertyName(propName); + buffer.writeln(' $dartType? $dartPropName,'); + }); + buffer.writeln(' }) {'); + buffer.writeln(' return $className('); + model.properties.forEach((propName, property) { + final dartPropName = StringUtils.toDartPropertyName(propName); + buffer.writeln( + ' $dartPropName: $dartPropName ?? this.$dartPropName,', + ); + }); + buffer.writeln(' );'); + buffer.writeln(' }'); + buffer.writeln(''); + } + + buffer.writeln('}'); + + return buffer.toString(); + } + + /// 生成带注解的模型代码(不包含导入语句) + String _generateAnnotatedModelCodeWithoutImports(ApiModel model) { + final className = StringUtils.generateClassName(model.name); + final buffer = StringBuffer(); + + // 生成 part 声明 + final partFileName = StringUtils.generateFileName(model.name); + final generatedPart = partFileName.replaceAll('.dart', '.g.dart'); + buffer.writeln('part \'$generatedPart\';'); + buffer.writeln(''); + + // 生成类注释 + if (model.description.isNotEmpty) { + buffer.writeln(StringUtils.generateComment(model.description)); + } + + buffer.writeln('@JsonSerializable()'); + buffer.writeln('class $className {'); + + // 生成属性 + model.properties.forEach((propName, property) { + final dartType = getDartPropertyType(property); + final nullable = property.nullable ? '?' : ''; + final dartPropName = StringUtils.toDartPropertyName(propName); + + if (property.description.isNotEmpty) { + buffer.writeln( + ' ${StringUtils.generateComment(property.description)}', + ); + } + + // 添加JsonKey注解 + final needsJsonKey = + _needsJsonKeyAnnotation(dartPropName, propName, property); + if (needsJsonKey.isNotEmpty) { + buffer.writeln(' @JsonKey($needsJsonKey)'); + } + + buffer.writeln(' final $dartType$nullable $dartPropName;'); + buffer.writeln(''); + }); + + // 生成构造函数 + if (model.properties.isEmpty) { + buffer.writeln(' const $className();'); + } else { + buffer.writeln(' const $className({'); + model.properties.forEach((propName, property) { + final dartPropName = StringUtils.toDartPropertyName(propName); + final required = property.required ? 'required ' : ''; + buffer.writeln(' ${required}this.$dartPropName,'); + }); + buffer.writeln(' });'); + } + buffer.writeln(''); + + // 生成 fromJson 工厂方法 + buffer.writeln( + ' factory $className.fromJson(Map json) => _\$${className}FromJson(json);', + ); + buffer.writeln(''); + + // 生成 toJson 方法 + buffer.writeln( + ' Map toJson() => _\$${className}ToJson(this);', + ); + buffer.writeln(''); + + buffer.writeln('}'); + + return buffer.toString(); + } + + /// 生成模型索引文件 + String generateIndexFile(List modelFileNames) { + final buffer = StringBuffer(); + + buffer.writeln(generateFileHeader('API 模型导出文件')); + buffer.writeln(''); + + // 添加 library 声明 + buffer.writeln('library;'); + buffer.writeln(''); + + // 按文件名排序并导出所有模型 + final sortedFiles = List.from(modelFileNames)..sort(); + + for (final fileName in sortedFiles) { + buffer.writeln('export \'$fileName\';'); + } + + return generateTypeCheckedCode(buffer.toString()); + } + + /// 判断是否需要JsonKey注解以及注解的内容 + String _needsJsonKeyAnnotation( + String dartPropName, String propName, ApiProperty property) { + final annotations = []; + + // 属性名与JSON字段名不同时需要name参数 + if (dartPropName != propName) { + annotations.add('name: \'$propName\''); + } + + // DateTime类型需要特殊处理 + if (property.type == PropertyType.string && + (property.format == 'date-time' || property.format == 'date')) { + // 对于DateTime类型,通常json_annotation会自动处理,但可以显式指定 + // annotations.add('fromJson: DateTime.parse, toJson: _dateTimeToString'); + } + + // 枚举类型的处理 + if (property.type == PropertyType.reference) { + // 检查是否是枚举类型(这里需要更复杂的逻辑来判断) + // 暂时不添加特殊处理 + } + + // 如果需要忽略某些属性 + // if (shouldIgnore) { + // annotations.add('ignore: true'); + // } + + return annotations.join(', '); + } +} diff --git a/lib/generators/retrofit_api_generator.dart b/lib/generators/retrofit_api_generator.dart new file mode 100644 index 0000000..cd1d7d4 --- /dev/null +++ b/lib/generators/retrofit_api_generator.dart @@ -0,0 +1,1563 @@ +import '../core/models.dart'; +import '../utils/string_utils.dart'; +import 'base_generator.dart'; + +/// Retrofit 风格的 API 生成器 +/// 负责生成带有注解的 API 接口类 +class RetrofitApiGenerator extends BaseGenerator { + final SwaggerDocument document; + final String className; + final bool useRetrofit; + final bool useDio; + final bool splitByTags; + + RetrofitApiGenerator( + this.document, { + this.className = 'ApiClient', + this.useRetrofit = true, + this.useDio = true, + this.splitByTags = false, + }); + + @override + String get generatorType => 'RetrofitApiGenerator'; + + @override + String generate() { + if (splitByTags) { + // 按 tags 分组生成多个文件时,返回主文件内容 + return generateMainApiFile(); + } else { + // 生成单个文件 + return generateSingleApiFile(); + } + } + + /// 生成单个 API 文件 + String generateSingleApiFile() { + final buffer = StringBuffer(); + + // 生成文件头 + buffer.writeln(generateFileHeader('Retrofit 风格 API 接口定义')); + buffer.writeln(''); + + // 生成导入语句 + _generateImports(buffer); + + // 生成 API 接口类 + _generateApiInterface(buffer); + + return generateTypeCheckedCode(buffer.toString()); + } + + /// 生成主 API 文件(当按 tags 分组时) + String generateMainApiFile() { + final buffer = StringBuffer(); + + // 生成文件头 + buffer.writeln(generateFileHeader('主 API 接口定义 - 集合所有 Tag 的 API')); + buffer.writeln(''); + + // 生成导入语句 + _generateMainImports(buffer); + + // 生成主 API 接口类 + _generateMainApiInterface(buffer); + + return generateTypeCheckedCode(buffer.toString()); + } + + /// 按 tags 分组生成多个 API 文件 + Map generateApiFilesByTags() { + final tagGroups = _groupPathsByTags(); + final apiFiles = {}; + + for (final entry in tagGroups.entries) { + final tagName = entry.key; + final paths = entry.value; + + final buffer = StringBuffer(); + + // 生成文件头 + buffer.writeln(generateFileHeader('$tagName API 接口定义')); + buffer.writeln(''); + + // 生成导入语句 + _generateTagImports(buffer, paths); + + // 生成 API 接口类 + _generateTagApiInterface(buffer, tagName, paths); + + final fileName = _generateTagFileName(tagName); + apiFiles[fileName] = generateTypeCheckedCode(buffer.toString()); + } + + return apiFiles; + } + + /// 生成导入语句 + void _generateImports(StringBuffer buffer) { + if (useDio) { + buffer.writeln('import \'package:dio/dio.dart\';'); + } + + if (useRetrofit) { + buffer.writeln('import \'package:retrofit/retrofit.dart\';'); + } + + buffer.writeln(''); + + // 导入基础响应类型(从用户项目中导入) + buffer.writeln( + 'import \'package:learning_officer_oa/common/models/common/base_result.dart\';'); + buffer.writeln( + 'import \'package:learning_officer_oa/common/models/common/base_page_result.dart\';'); + buffer.writeln(''); + + // 导入生成的模型类 + final modelImports = _getRequiredModelImports(); + for (final modelImport in modelImports) { + buffer.writeln( + 'import \'../api_models/${StringUtils.generateFileName(modelImport)}\';'); + } + + if (modelImports.isNotEmpty) { + buffer.writeln(''); + } + + // 生成 part 声明 + buffer.writeln('part \'api_client.g.dart\';'); + buffer.writeln(''); + } + + /// 生成 API 接口类 + void _generateApiInterface(StringBuffer buffer) { + buffer.writeln('/// $className API 接口'); + buffer.writeln('/// 使用 Retrofit 风格的注解定义'); + + if (useRetrofit) { + buffer.writeln('@RestApi(parser: Parser.JsonSerializable)'); + } + + buffer.writeln('abstract class $className {'); + + if (useRetrofit) { + buffer.writeln( + ' factory $className(Dio dio, {String? baseUrl}) = _$className;'); + } + + buffer.writeln(''); + + // 按控制器分组生成接口方法 + final controllerGroups = _groupPathsByController(); + + for (final entry in controllerGroups.entries) { + final controllerName = entry.key; + final paths = entry.value; + + buffer.writeln(' // ========== $controllerName 相关接口 =========='); + buffer.writeln(''); + + for (final path in paths) { + _generateApiMethod(buffer, path); + } + + buffer.writeln(''); + } + + buffer.writeln('}'); + + // 生成扩展方法(如果不使用 Retrofit) + if (!useRetrofit) { + _generateManualImplementation(buffer); + } + } + + /// 生成单个 API 方法 + void _generateApiMethod(StringBuffer buffer, ApiPath path) { + final methodName = _generateSimpleMethodName(path); + final httpMethod = path.method.value.toUpperCase(); + final cleanPath = StringUtils.cleanPath(path.path); + + // 生成方法注释 + if (path.summary.isNotEmpty) { + buffer.writeln(' /// ${path.summary}'); + } + if (path.description.isNotEmpty && path.description != path.summary) { + buffer.writeln(' /// ${path.description}'); + } + + // 生成 HTTP 方法注解 + if (useRetrofit) { + buffer.writeln(' @$httpMethod(\'$cleanPath\')'); + } + + // 生成方法签名 + final returnType = _generateReturnType(path); + final parameters = _generateParameters(path); + + buffer.writeln(' Future<$returnType> $methodName('); + + if (parameters.isNotEmpty) { + for (int i = 0; i < parameters.length; i++) { + final param = parameters[i]; + final isLast = i == parameters.length - 1; + if (param.annotation.isNotEmpty) { + buffer.writeln( + ' ${param.annotation} ${param.type} ${param.name}${isLast ? '' : ','}'); + } else { + buffer.writeln(' ${param.type} ${param.name}${isLast ? '' : ','}'); + } + } + } + + buffer.writeln(' );'); + buffer.writeln(''); + } + + /// 生成简化的方法名称 + String _generateSimpleMethodName(ApiPath path) { + final method = path.method.value.toLowerCase(); + + // 优先使用 operationId(如果存在且有意义) + if (path.operationId.isNotEmpty) { + final operationId = path.operationId; + // 如果 operationId 已经包含了 HTTP 方法前缀,直接使用 + if (operationId.toLowerCase().startsWith(method)) { + return StringUtils.toCamelCase(operationId); + } + // 否则添加 HTTP 方法前缀 + return '$method${StringUtils.toPascalCase(operationId)}'; + } + + // 清理路径,移除 /api/v1 前缀 + String cleanedPath = path.path.replaceFirst(RegExp(r'^/api/v\d+'), ''); + if (cleanedPath.isEmpty) { + cleanedPath = path.path; + } + + // 从路径中提取核心部分 + final pathParts = cleanedPath + .split('/') + .where((part) => part.isNotEmpty && !part.startsWith('{')) + .toList(); + + if (pathParts.length >= 2) { + // 只使用方法名部分,避免重复控制器名 + // 如 /TaskSummarize/GetSummarizeTaskByDate -> getSummarizeTaskByDate + final action = StringUtils.toPascalCase(pathParts[1]); + + // 如果方法名已经以HTTP方法开头,移除重复前缀 + final actionLower = action.toLowerCase(); + if (actionLower.startsWith(method)) { + // 移除重复的HTTP方法前缀 + final cleanAction = action.substring(method.length); + return '$method${StringUtils.toPascalCase(cleanAction)}'; + } + + return '$method$action'; + } else if (pathParts.length == 1) { + // 只有一个部分:如 /HealthCheck -> getHealthCheck + final action = StringUtils.toPascalCase(pathParts[0]); + + // 如果方法名已经以HTTP方法开头,移除重复前缀 + final actionLower = action.toLowerCase(); + if (actionLower.startsWith(method)) { + // 移除重复的HTTP方法前缀 + final cleanAction = action.substring(method.length); + return '$method${StringUtils.toPascalCase(cleanAction)}'; + } + + return '$method$action'; + } + + // 最后的备用方案:使用完整路径 + final sanitizedPath = + pathParts.map((part) => StringUtils.toPascalCase(part)).join(''); + return '$method$sanitizedPath'; + } + + /// 生成返回类型 + String _generateReturnType(ApiPath path) { + // 优先从实际的 schema 中解析类型 + final schemaType = _extractResponseTypeFromPath(path); + if (schemaType != null) { + return _wrapWithBaseResult(schemaType, path); + } + + // 如果无法从 schema 解析,使用智能推断 + final inferredType = _inferReturnTypeFromPath(path); + if (inferredType != null) { + return _wrapWithBaseResult(inferredType, path); + } + + // 默认返回类型 + return 'BaseResult>'; + } + + /// 包装返回类型为BaseResult或BasePageResult + String _wrapWithBaseResult(String originalType, ApiPath? path) { + // 检查是否是列表类型且可能需要分页 + if (originalType.startsWith('List<') && + _isPageableType(originalType, path)) { + // 提取List中的类型 + final innerType = originalType.substring(5, originalType.length - 1); + return 'BaseResult>'; + } + + // 对于其他类型,使用BaseResult包装 + return 'BaseResult<$originalType>'; + } + + /// 智能判断是否是可分页的类型 + bool _isPageableType(String type, ApiPath? path) { + if (path == null) { + return false; + } + + final pathLower = path.path.toLowerCase(); + final summaryLower = path.summary.toLowerCase(); + final operationId = path.operationId.toLowerCase(); + final tags = path.tags.map((tag) => tag.toLowerCase()).toList(); + + double score = 0.0; + + // 1. 基于查询参数判断(权重最高,因为这是最直接的证据) + if (_hasPaginationParameters(path)) { + score += 5; + } + + // 2. 基于路径关键词判断 + if (_hasPaginationKeywords(pathLower, summaryLower, operationId, tags)) { + score += 3; + } + + // 3. 基于API路径模式判断 + if (_hasPaginationPathPattern(pathLower)) { + score += 3; + } + + // 4. 基于返回类型名称判断(权重最低,因为可能误判) + if (_hasPaginationTypeName(type)) { + score += 0.5; + } + + // 需要达到阈值才认为是分页 + return score >= 4; + } + + /// 检查是否包含分页相关的关键词 + bool _hasPaginationKeywords(String pathLower, String summaryLower, + String operationId, List tags) { + final paginationKeywords = [ + 'page', + 'pagination', + '分页', + '列表', + 'list', + 'getlist', + 'get_list', + 'search', + '查询', + 'filter', + '筛选', + 'find', + '查找' + ]; + + // 检查路径 + if (paginationKeywords.any((keyword) => pathLower.contains(keyword))) { + return true; + } + + // 检查摘要 + if (paginationKeywords.any((keyword) => summaryLower.contains(keyword))) { + return true; + } + + // 检查操作ID + if (paginationKeywords.any((keyword) => operationId.contains(keyword))) { + return true; + } + + // 检查标签 + if (tags.any( + (tag) => paginationKeywords.any((keyword) => tag.contains(keyword)))) { + return true; + } + + return false; + } + + /// 检查是否包含分页相关的查询参数 + bool _hasPaginationParameters(ApiPath path) { + final paginationParams = [ + 'page', + 'size', + 'limit', + 'offset', + 'skip', + 'take', + 'pagesize', + 'pagenumber', + 'pageindex', + 'pagenum', + 'currentpage', + 'page_size', + 'page_number', + 'page_index' + ]; + + final timeRangeParams = [ + 'begintime', + 'endtime', + 'begindate', + 'enddate', + 'starttime', + 'endtime', + 'startdate', + 'enddate' + ]; + + final queryParams = path.parameters + .where((p) => p.location == ParameterLocation.query) + .map((p) => p.name.toLowerCase()) + .toList(); + + // 检查是否有分页参数 + final hasPaginationParams = queryParams.any((param) => paginationParams + .any((paginationParam) => param.contains(paginationParam))); + + // 检查是否只有时间范围参数(这种情况通常不是分页) + final hasOnlyTimeRangeParams = queryParams.isNotEmpty && + queryParams.every((param) => + timeRangeParams.any((timeParam) => param.contains(timeParam)) || + param.contains('username') || + param.contains('userid') || + param.contains('date') || + param.contains('year') || + param.contains('month')); + + // 如果有分页参数,返回true + if (hasPaginationParams) { + return true; + } + + // 如果只有时间范围参数,返回false(不是分页) + if (hasOnlyTimeRangeParams) { + return false; + } + + return false; + } + + /// 检查返回类型名称是否暗示分页 + bool _hasPaginationTypeName(String type) { + final paginationTypePatterns = [ + RegExp(r'List<.*Result>'), + RegExp(r'List<.*List.*>'), + RegExp(r'List<.*Page.*>'), + RegExp(r'List<.*Search.*>'), + RegExp(r'List<.*Filter.*>'), + RegExp(r'List<.*Task.*>'), + RegExp(r'List<.*User.*>'), + RegExp(r'List<.*School.*>'), + RegExp(r'List<.*Class.*>'), + ]; + + return paginationTypePatterns.any((pattern) => pattern.hasMatch(type)); + } + + /// 检查API路径模式是否暗示分页 + bool _hasPaginationPathPattern(String pathLower) { + final paginationPathPatterns = [ + RegExp(r'/get.*list'), + RegExp(r'/search.*'), + RegExp(r'/find.*'), + RegExp(r'/query.*'), + RegExp(r'/filter.*'), + RegExp(r'/page.*'), + ]; + + return paginationPathPatterns.any((pattern) => pattern.hasMatch(pathLower)); + } + + /// 从响应中提取返回类型 + String? _extractResponseType(ApiResponse response) { + // 优先检查 content.application/json.schema (Swagger 3.0) + if (response.content != null) { + final applicationJson = + response.content!['application/json'] as Map?; + if (applicationJson != null) { + final schema = applicationJson['schema'] as Map?; + final type = _extractTypeFromSchema(schema); + if (type != null) { + return type; + } + } + } + + // 检查 schema 字段 (Swagger 2.0) + final type = _extractTypeFromSchema(response.schema); + if (type != null) { + return type; + } + + return null; + } + + /// 从 schema 中提取类型 + String? _extractTypeFromSchema(Map? schema) { + if (schema == null) return null; + + // 处理 $ref 引用 + if (schema['\$ref'] != null) { + final ref = schema['\$ref'] as String; + final parts = ref.split('/'); + if (parts.isNotEmpty) { + final refName = parts.last; + // 检查是否是已知的模型类型 + if (document.models.containsKey(refName)) { + return StringUtils.generateClassName(refName); + } + // 尝试生成类名 + return StringUtils.generateClassName(refName); + } + } + + // 处理数组类型 + if (schema['type'] == 'array' && schema['items'] != null) { + final items = schema['items'] as Map; + final itemType = _extractTypeFromSchema(items); + if (itemType != null) { + return 'List<$itemType>'; + } + } + + // 处理对象类型 + if (schema['type'] == 'object') { + // 检查是否有 properties 定义 + if (schema['properties'] != null) { + // 这是一个复杂对象,返回 Map + return 'Map'; + } + // 检查是否有 additionalProperties + if (schema['additionalProperties'] != null) { + return 'Map'; + } + // 检查是否有 allOf, anyOf, oneOf + if (schema['allOf'] != null || + schema['anyOf'] != null || + schema['oneOf'] != null) { + return 'Map'; + } + } + + // 处理基本类型 + if (schema['type'] != null) { + final type = schema['type'] as String; + switch (type) { + case 'string': + // 检查是否有 format + final format = schema['format'] as String?; + if (format == 'date-time' || format == 'date') { + return 'DateTime'; + } + if (format == 'uuid') { + return 'String'; + } + return 'String'; + case 'integer': + return 'int'; + case 'number': + return 'double'; + case 'boolean': + return 'bool'; + case 'null': + return 'dynamic'; + default: + return 'dynamic'; + } + } + + // 处理枚举类型 + if (schema['enum'] != null) { + return 'String'; + } + + return null; + } + + /// 从路径的响应中提取返回类型 + String? _extractResponseTypeFromPath(ApiPath path) { + // 查找成功响应 (200, 201, 202) + final successResponses = ['200', '201', '202']; + + for (final statusCode in successResponses) { + final response = path.responses[statusCode]; + if (response != null) { + final type = _extractResponseType(response); + if (type != null) { + return type; + } + } + } + + // 如果没有找到明确的成功响应,尝试查找第一个有 schema 的响应 + for (final response in path.responses.values) { + final type = _extractResponseType(response); + if (type != null) { + return type; + } + } + + return null; + } + + /// 根据路径推断返回类型 + String? _inferReturnTypeFromPath(ApiPath path) { + final pathLower = path.path.toLowerCase(); + final summaryLower = path.summary.toLowerCase(); + final operationId = path.operationId.toLowerCase(); + final tags = path.tags.map((tag) => tag.toLowerCase()).toList(); + + // 基于 operationId 推断类型 + if (operationId.isNotEmpty) { + final inferredType = _inferTypeFromOperationId(operationId); + if (inferredType != null) { + return inferredType; + } + } + + // 基于路径关键词推断类型 + final inferredType = + _inferTypeFromPathKeywords(pathLower, summaryLower, tags); + if (inferredType != null) { + return inferredType; + } + + return null; + } + + /// 生成参数列表 + List _generateParameters(ApiPath path) { + final parameters = []; + + // 处理路径参数 + final pathParams = path.parameters + .where((p) => p.location == ParameterLocation.path) + .toList(); + for (final param in pathParams) { + parameters.add(ApiMethodParameter( + name: StringUtils.toCamelCase(param.name), + type: _getDartType(param.type), + annotation: useRetrofit ? '@Path(\'${param.name}\')' : '', + required: param.required, + )); + } + + // 处理查询参数 + final queryParams = path.parameters + .where((p) => p.location == ParameterLocation.query) + .toList(); + + // 当 GET 请求的查询参数超过4个时,生成参数实体类 + if (path.method == HttpMethod.get && queryParams.length > 4) { + final parameterEntityClassName = _generateParameterEntityClassName(path); + + // 生成参数实体类 + _generateParameterEntity(path, parameterEntityClassName, queryParams); + + // 添加参数实体类作为单个参数 + parameters.add(ApiMethodParameter( + name: 'parameters', + type: '$parameterEntityClassName?', + annotation: useRetrofit ? '@Queries()' : '', + required: false, + )); + } else { + // 原有逻辑:单独处理每个查询参数 + for (final param in queryParams) { + final nullable = param.required ? '' : '?'; + parameters.add(ApiMethodParameter( + name: StringUtils.toCamelCase(param.name), + type: '${_getDartType(param.type)}$nullable', + annotation: useRetrofit ? '@Query(\'${param.name}\')' : '', + required: param.required, + )); + } + } + + // 处理请求体参数 - 使用具体的模型类型 + final bodyParams = path.parameters + .where((p) => p.location == ParameterLocation.body) + .toList(); + for (final param in bodyParams) { + final bodyType = _inferRequestBodyType(path); + parameters.add(ApiMethodParameter( + name: StringUtils.toCamelCase( + param.name.isNotEmpty ? param.name : 'request'), + type: bodyType, + annotation: useRetrofit ? '@Body()' : '', + required: true, + )); + } + + // 如果是 POST/PUT/PATCH 但没有明确的 body 参数,添加一个通用的 body 参数 + if ((path.method == HttpMethod.post || + path.method == HttpMethod.put || + path.method == HttpMethod.patch) && + bodyParams.isEmpty) { + final bodyType = _inferRequestBodyType(path); + parameters.add(ApiMethodParameter( + name: 'request', + type: bodyType, + annotation: useRetrofit ? '@Body()' : '', + required: true, + )); + } + + return parameters; + } + + /// 推断请求体类型 + String _inferRequestBodyType(ApiPath path) { + // 优先从实际的 requestBody schema 中解析类型 + if (path.requestBody != null) { + final schemaType = _extractRequestBodyType(path.requestBody!); + if (schemaType != null) { + return schemaType; + } + } + + // 如果无法从 schema 解析,使用路径推断 + final pathLower = path.path.toLowerCase(); + + // 登录请求 + if (pathLower.contains('/login/userlogin') && !pathLower.contains('code')) { + return 'UserLoginRequest'; + } + + if (pathLower.contains('/login/usercodelogin') || + pathLower.contains('/getuserlogincode')) { + return 'LoginCodeRequest'; + } + + // 注册请求 + if (pathLower.contains('/register')) { + return 'RegisterRequest'; + } + + // 刷新token + if (pathLower.contains('/refreshtoken')) { + return 'RefreshTokenRequest'; + } + + // 修改密码 + if (pathLower.contains('/updatemypasswod')) { + return 'MyInfoResetPwdRequest'; + } + + // 换绑手机 + if (pathLower.contains('/updatemyphone')) { + return 'MyPhoneBindRequest'; + } + + // 班级相关请求 + if (pathLower.contains('/addclasses')) { + return 'ClassTeacherRequest'; + } + + // 默认使用通用类型 + return 'Map'; + } + + /// 从请求体中提取请求类型 + String? _extractRequestBodyType(ApiRequestBody requestBody) { + // 检查 content.application/json.schema + if (requestBody.content != null) { + final applicationJson = + requestBody.content!['application/json'] as Map?; + if (applicationJson != null) { + final schema = applicationJson['schema'] as Map?; + final type = _extractTypeFromSchema(schema); + if (type != null) { + return type; + } + } + } + + return null; + } + + /// 获取需要导入的模型类型 + Set _getRequiredModelImports() { + return _getRequiredModelImportsForPaths(document.paths.values.toList()); + } + + /// 生成手动实现(当不使用 Retrofit 时) + void _generateManualImplementation(StringBuffer buffer) { + buffer.writeln(''); + buffer.writeln('/// ${className} 的手动实现'); + buffer.writeln('/// 使用 Dio 进行网络请求'); + buffer.writeln('class ${className}Impl implements $className {'); + buffer.writeln(' final Dio _dio;'); + buffer.writeln(''); + buffer.writeln(' ${className}Impl(this._dio);'); + buffer.writeln(''); + + // 生成方法实现 + final controllerGroups = _groupPathsByController(); + + for (final entry in controllerGroups.entries) { + final controllerName = entry.key; + final paths = entry.value; + + buffer.writeln(' // ========== $controllerName 相关接口实现 =========='); + buffer.writeln(''); + + for (final path in paths) { + _generateManualMethodImplementation(buffer, path); + } + } + + buffer.writeln('}'); + } + + /// 生成手动方法实现 + void _generateManualMethodImplementation(StringBuffer buffer, ApiPath path) { + final methodName = _generateSimpleMethodName(path); + final httpMethod = path.method.value.toLowerCase(); + final cleanPath = StringUtils.cleanPath(path.path); + final returnType = _generateReturnType(path); + final parameters = _generateParameters(path); + + buffer.writeln(' @override'); + buffer.writeln(' Future<$returnType> $methodName('); + + if (parameters.isNotEmpty) { + for (int i = 0; i < parameters.length; i++) { + final param = parameters[i]; + final isLast = i == parameters.length - 1; + buffer.writeln(' ${param.type} ${param.name}${isLast ? '' : ','}'); + } + } + + buffer.writeln(' ) async {'); + + // 构建请求路径 + var requestPath = cleanPath; + final pathParams = + parameters.where((p) => p.annotation.contains('@Path')).toList(); + + if (pathParams.isNotEmpty) { + buffer.writeln(' String path = \'$requestPath\';'); + for (final param in pathParams) { + final paramName = param.name; + final pathParamName = + param.annotation.replaceAll('@Path(\'', '').replaceAll('\')', ''); + buffer.writeln( + ' path = path.replaceAll(\'{$pathParamName}\', $paramName.toString());'); + } + } else { + buffer.writeln(' const String path = \'$requestPath\';'); + } + + // 构建查询参数 + final queryParams = + parameters.where((p) => p.annotation.contains('@Query')).toList(); + if (queryParams.isNotEmpty) { + buffer.writeln(''); + buffer.writeln(' final Map queryParams = {};'); + for (final param in queryParams) { + final paramName = param.name; + final queryParamName = + param.annotation.replaceAll('@Query(\'', '').replaceAll('\')', ''); + buffer.writeln( + ' if ($paramName != null) queryParams[\'$queryParamName\'] = $paramName;'); + } + } + + // 构建请求体 + final bodyParams = + parameters.where((p) => p.annotation.contains('@Body')).toList(); + String? bodyParam; + if (bodyParams.isNotEmpty) { + bodyParam = bodyParams.first.name; + } + + buffer.writeln(''); + buffer.writeln(' final response = await _dio.$httpMethod('); + buffer.writeln(' path,'); + + if (queryParams.isNotEmpty) { + buffer.writeln(' queryParameters: queryParams,'); + } + + if (bodyParam != null) { + // 如果是具体的模型类型,调用toJson方法 + final bodyType = bodyParams.first.type; + if (bodyType != 'Map' && !bodyType.contains('?')) { + buffer.writeln(' data: $bodyParam.toJson(),'); + } else { + buffer.writeln(' data: $bodyParam,'); + } + } + + buffer.writeln(' );'); + buffer.writeln(''); + + // 处理响应 + if (returnType.startsWith('List<')) { + // 列表类型的处理 + final itemType = returnType.substring(5, returnType.length - 1); + buffer.writeln(' final data = response.data as List;'); + buffer.writeln( + ' return data.map((item) => $itemType.fromJson(item)).toList();'); + } else if (returnType != 'Map') { + // 具体模型类型的处理 + buffer.writeln(' return $returnType.fromJson(response.data);'); + } else { + // 通用类型的处理 + buffer.writeln(' return response.data;'); + } + + buffer.writeln(' }'); + buffer.writeln(''); + } + + /// 按控制器分组路径 + Map> _groupPathsByController() { + final groups = >{}; + + for (final path in document.paths.values) { + final controllerName = StringUtils.extractControllerName(path); + groups.putIfAbsent(controllerName, () => []).add(path); + } + + return groups; + } + + // 已移动到 StringUtils.extractControllerName + + /// 获取 Dart 类型 + String _getDartType(PropertyType type) { + switch (type) { + case PropertyType.string: + return 'String'; + case PropertyType.integer: + return 'int'; + case PropertyType.number: + return 'double'; + case PropertyType.boolean: + return 'bool'; + case PropertyType.array: + return 'List'; + case PropertyType.object: + return 'Map'; + case PropertyType.reference: + return 'dynamic'; + default: + return 'dynamic'; + } + } + + // 已移动到 StringUtils.cleanPath + + /// 获取基础URL + String _getBaseUrl() { + if (document.schemes.isNotEmpty) { + return '${document.schemes.first}://${document.host}${document.basePath}'; + } + return 'https://${document.host}${document.basePath}'; + } + + /// 按 tags 分组路径 + Map> _groupPathsByTags() { + final groups = >{}; + + for (final path in document.paths.values) { + if (path.tags.isNotEmpty) { + for (final tag in path.tags) { + groups.putIfAbsent(tag, () => []).add(path); + } + } else { + // 如果没有 tags,放入 General 分组 + groups.putIfAbsent('General', () => []).add(path); + } + } + + return groups; + } + + /// 生成 tag 文件名 + String _generateTagFileName(String tagName) { + return '${StringUtils.toSnakeCase(tagName)}_api.dart'; + } + + /// 生成主文件的导入语句 + void _generateMainImports(StringBuffer buffer) { + if (useDio) { + buffer.writeln('import \'package:dio/dio.dart\';'); + } + + buffer.writeln(''); + + // 导入基础响应类型(从用户项目中导入) + buffer.writeln( + 'import \'package:learning_officer_oa/common/models/common/base_result.dart\';'); + buffer.writeln( + 'import \'package:learning_officer_oa/common/models/common/base_page_result.dart\';'); + buffer.writeln(''); + + // 导入错误处理相关类 + buffer.writeln('import \'api_error.dart\';'); + buffer.writeln('import \'api_error_handler.dart\';'); + buffer.writeln(''); + + // 导入所有 tag 的 API 文件 + final tagGroups = _groupPathsByTags(); + for (final tagName in tagGroups.keys) { + final fileName = _generateTagFileName(tagName); + buffer.writeln('import \'$fileName\';'); + } + + buffer.writeln(''); + } + + /// 生成主 API 接口类 + void _generateMainApiInterface(StringBuffer buffer) { + buffer.writeln('/// 统一API客户端类'); + buffer.writeln('/// 聚合所有分模块的API接口,提供统一的访问入口'); + buffer.writeln('class $className {'); + buffer.writeln(' final Dio _dio;'); + + // 生成各个子API的私有字段 + final tagGroups = _groupPathsByTags(); + for (final tagName in tagGroups.keys) { + final className = _generateTagClassName(tagName); + buffer.writeln( + ' late final $className _${StringUtils.toCamelCase(tagName)}Api;'); + } + + buffer.writeln(''); + + // 生成构造函数 + buffer.writeln(' $className(this._dio, {String? baseUrl}) {'); + for (final tagName in tagGroups.keys) { + final className = _generateTagClassName(tagName); + final fieldName = '_${StringUtils.toCamelCase(tagName)}Api'; + buffer.writeln(' $fieldName = $className(_dio, baseUrl: baseUrl);'); + } + buffer.writeln(' }'); + + buffer.writeln(''); + + // 为每个 tag 生成一个获取器 + for (final tagName in tagGroups.keys) { + final className = _generateTagClassName(tagName); + final fieldName = '_${StringUtils.toCamelCase(tagName)}Api'; + buffer.writeln(' /// ${tagName}相关API'); + buffer.writeln( + ' $className get ${StringUtils.toCamelCase(tagName)} => $fieldName;'); + buffer.writeln(''); + } + + // 生成通用工具方法 + buffer.writeln(' /// 获取Dio实例'); + buffer.writeln(' Dio get dio => _dio;'); + buffer.writeln(''); + + buffer.writeln(' /// 设置认证token'); + buffer.writeln(' void setAuthToken(String token) {'); + buffer.writeln( + ' _dio.options.headers[\'Authorization\'] = \'Bearer \$token\';'); + buffer.writeln(' }'); + buffer.writeln(''); + + buffer.writeln(' /// 清除认证token'); + buffer.writeln(' void clearAuthToken() {'); + buffer.writeln(' _dio.options.headers.remove(\'Authorization\');'); + buffer.writeln(' }'); + buffer.writeln(''); + + buffer.writeln(' /// 设置基础URL'); + buffer.writeln(' void setBaseUrl(String baseUrl) {'); + buffer.writeln(' _dio.options.baseUrl = baseUrl;'); + buffer.writeln(' }'); + buffer.writeln(''); + + buffer.writeln(' /// 添加请求拦截器'); + buffer.writeln(' void addRequestInterceptor(Interceptor interceptor) {'); + buffer.writeln(' _dio.interceptors.add(interceptor);'); + buffer.writeln(' }'); + buffer.writeln(''); + + buffer.writeln(' /// 添加响应拦截器'); + buffer.writeln(' void addResponseInterceptor(Interceptor interceptor) {'); + buffer.writeln(' _dio.interceptors.add(interceptor);'); + buffer.writeln(' }'); + buffer.writeln(''); + + buffer.writeln(' /// 添加错误拦截器'); + buffer.writeln(' void addErrorInterceptor(Interceptor interceptor) {'); + buffer.writeln(' _dio.interceptors.add(interceptor);'); + buffer.writeln(' }'); + buffer.writeln(''); + + buffer.writeln(' /// 创建带错误处理的API调用'); + buffer.writeln( + ' Future callWithErrorHandling(Future Function() apiCall) async {'); + buffer.writeln(' try {'); + buffer.writeln(' return await apiCall();'); + buffer.writeln(' } on DioException catch (e) {'); + buffer.writeln(' final error = ApiErrorHandler.handleDioError(e);'); + buffer.writeln(' throw error;'); + buffer.writeln(' } catch (e) {'); + buffer.writeln(' final error = ApiError('); + buffer.writeln(' type: ApiErrorType.unknown,'); + buffer.writeln(' message: \'未知错误: \$e\','); + buffer.writeln(' code: -1,'); + buffer.writeln(' originalError: e,'); + buffer.writeln(' );'); + buffer.writeln(' throw error;'); + buffer.writeln(' }'); + buffer.writeln(' }'); + + buffer.writeln('}'); + } + + /// 生成特定 tag 的导入语句 + void _generateTagImports(StringBuffer buffer, List paths) { + if (useDio) { + buffer.writeln('import \'package:dio/dio.dart\';'); + } + + if (useRetrofit) { + buffer.writeln('import \'package:retrofit/retrofit.dart\';'); + } + + buffer.writeln(''); + + // 导入基础响应类型(从用户项目中导入) + buffer.writeln( + 'import \'package:learning_officer_oa/common/models/common/base_result.dart\';'); + buffer.writeln( + 'import \'package:learning_officer_oa/common/models/common/base_page_result.dart\';'); + buffer.writeln(''); + + // 导入生成的模型类 + final modelImports = _getRequiredModelImportsForPaths(paths); + for (final modelImport in modelImports) { + buffer.writeln( + 'import \'../api_models/${StringUtils.generateFileName(modelImport)}\';'); + } + + if (modelImports.isNotEmpty) { + buffer.writeln(''); + } + + // 生成 part 声明 + final tagName = paths.first.tags.first; + final fileName = _generateTagFileName(tagName); + final partFileName = fileName.replaceAll('.dart', '.g.dart'); + buffer.writeln('part \'$partFileName\';'); + buffer.writeln(''); + } + + /// 生成特定 tag 的 API 接口类 + void _generateTagApiInterface( + StringBuffer buffer, String tagName, List paths) { + final className = _generateTagClassName(tagName); + + buffer.writeln('/// $tagName API 接口'); + buffer.writeln('/// 负责处理 $tagName 相关的接口'); + + if (useRetrofit) { + buffer.writeln('@RestApi(parser: Parser.JsonSerializable)'); + } + + buffer.writeln('abstract class $className {'); + + if (useRetrofit) { + buffer.writeln( + ' factory $className(Dio dio, {String? baseUrl}) = _$className;'); + } + + buffer.writeln(''); + + buffer.writeln(' // ========== $tagName 相关接口 =========='); + buffer.writeln(''); + + for (final path in paths) { + _generateApiMethod(buffer, path); + } + + buffer.writeln('}'); + + // 生成扩展方法(如果不使用 Retrofit) + if (!useRetrofit) { + _generateTagManualImplementation(buffer, tagName, className, paths); + } + } + + /// 生成 tag 类名 + String _generateTagClassName(String tagName) { + return '${StringUtils.toPascalCase(tagName)}Api'; + } + + /// 获取指定路径列表所需的模型导入 + Set _getRequiredModelImportsForPaths(List paths) { + final imports = {}; + + for (final path in paths) { + // 从返回值类型中提取导入 + final returnType = _generateReturnType(path); + _extractImportsFromType(returnType, imports); + + // 从请求体类型中提取导入 + final requestBodyType = _inferRequestBodyType(path); + _extractImportsFromType(requestBodyType, imports); + + // 从参数类型中提取导入 + final parameters = _generateParameters(path); + for (final param in parameters) { + _extractImportsFromType(param.type, imports); + } + + // 添加参数实体类导入 + final queryParams = path.parameters + .where((p) => p.location == ParameterLocation.query) + .toList(); + if (path.method == HttpMethod.get && queryParams.length > 4) { + final parameterEntityClassName = + _generateParameterEntityClassName(path); + imports.add(parameterEntityClassName); + } + } + + return imports; + } + + /// 从类型字符串中提取需要导入的模型类 + void _extractImportsFromType(String type, Set imports) { + // 移除 Future 包装 + String cleanType = type.replaceAll(RegExp(r'Future<(.+)>'), r'$1'); + + // 处理可空类型(移除?) + if (cleanType.endsWith('?')) { + cleanType = cleanType.substring(0, cleanType.length - 1); + } + + // 处理 BaseResult 类型 + if (cleanType.startsWith('BaseResult<') && cleanType.endsWith('>')) { + final innerType = cleanType.substring(11, cleanType.length - 1); + _extractImportsFromType(innerType, imports); + return; + } + + // 处理 BasePageResult 类型 + if (cleanType.startsWith('BasePageResult<') && cleanType.endsWith('>')) { + final innerType = cleanType.substring(15, cleanType.length - 1); + _extractImportsFromType(innerType, imports); + return; + } + + // 处理 List 类型 + if (cleanType.startsWith('List<') && cleanType.endsWith('>')) { + final itemType = cleanType.substring(5, cleanType.length - 1); + _extractImportsFromType(itemType, imports); + return; + } + + // 排除基本类型和通用类型 + if (_isBasicType(cleanType)) { + return; + } + + // 添加模型类型到导入列表(使用转换后的类名) + imports.add(StringUtils.generateClassName(cleanType)); + } + + /// 判断是否为基本类型 + bool _isBasicType(String type) { + const basicTypes = { + 'String', + 'int', + 'double', + 'bool', + 'dynamic', + 'Map', + 'Object', + 'void', + 'BaseResult', + 'BasePageResult', + }; + + // 检查基本类型 + if (basicTypes.contains(type)) { + return true; + } + + // 检查可空的基本类型 + if (type.endsWith('?')) { + final baseType = type.substring(0, type.length - 1); + return basicTypes.contains(baseType); + } + + return false; + } + + /// 生成 tag 的手动实现 + void _generateTagManualImplementation(StringBuffer buffer, String tagName, + String className, List paths) { + buffer.writeln(''); + buffer.writeln('/// ${className} 的手动实现'); + buffer.writeln('/// 使用 Dio 进行网络请求'); + buffer.writeln('class ${className}Impl implements $className {'); + buffer.writeln(' final Dio _dio;'); + buffer.writeln(''); + buffer.writeln(' ${className}Impl(this._dio);'); + buffer.writeln(''); + + buffer.writeln(' // ========== $tagName 相关接口实现 =========='); + buffer.writeln(''); + + for (final path in paths) { + _generateManualMethodImplementation(buffer, path); + } + + buffer.writeln('}'); + } + + /// 生成参数实体类的类名 + String _generateParameterEntityClassName(ApiPath path) { + final methodName = _generateSimpleMethodName(path); + return '${StringUtils.toPascalCase(methodName)}Parameters'; + } + + /// 生成参数实体类 + void _generateParameterEntity( + ApiPath path, String className, List queryParams) { + if (!_generatedParameterEntities.containsKey(className)) { + final buffer = StringBuffer(); + + // 生成文件头注释 + buffer.writeln('/// 参数实体类 - $className'); + buffer.writeln( + '/// 用于 ${path.method.value.toUpperCase()} ${path.path} 的查询参数'); + buffer.writeln(''); + + // 导入语句 + buffer + .writeln('import \'package:json_annotation/json_annotation.dart\';'); + buffer.writeln(''); + buffer.writeln('part \'${StringUtils.toSnakeCase(className)}.g.dart\';'); + buffer.writeln(''); + + // 生成参数实体类 + buffer.writeln('@JsonSerializable()'); + buffer.writeln('class $className {'); + + // 生成属性 + for (final param in queryParams) { + final dartName = StringUtils.toCamelCase(param.name); + final dartType = _getDartType(param.type); + final nullable = param.required ? '' : '?'; + + // 处理描述中的换行符,确保注释格式正确 + final cleanDescription = param.description + .replaceAll('\r\n', ' ') + .replaceAll('\n', ' ') + .replaceAll('\r', ' ') + .trim(); + buffer.writeln( + ' /// ${cleanDescription.isNotEmpty ? cleanDescription : param.name}'); + buffer.writeln(' @JsonKey(name: \'${param.name}\')'); + buffer.writeln(' final $dartType$nullable $dartName;'); + buffer.writeln(''); + } + + // 生成构造函数 + buffer.writeln(' const $className({'); + for (final param in queryParams) { + final dartName = StringUtils.toCamelCase(param.name); + final required = param.required ? 'required ' : ''; + buffer.writeln(' ${required}this.$dartName,'); + } + buffer.writeln(' });'); + buffer.writeln(''); + + // 生成 fromJson 方法 + buffer.writeln( + ' factory $className.fromJson(Map json) =>'); + buffer.writeln(' _\$${className}FromJson(json);'); + buffer.writeln(''); + + // 生成 toJson 方法 + buffer.writeln( + ' Map toJson() => _\$${className}ToJson(this);'); + buffer.writeln(''); + + // 生成 toQueryMap 方法(用于 Dio 查询参数) + buffer.writeln(' /// 转换为查询参数 Map'); + buffer.writeln(' Map toQueryMap() {'); + buffer.writeln(' final map = {};'); + for (final param in queryParams) { + final dartName = StringUtils.toCamelCase(param.name); + buffer.writeln( + ' if ($dartName != null) map[\'${param.name}\'] = $dartName;'); + } + buffer.writeln(' return map;'); + buffer.writeln(' }'); + + buffer.writeln('}'); + + _generatedParameterEntities[className] = buffer.toString(); + } + } + + /// 存储已生成的参数实体类 + final Map _generatedParameterEntities = {}; + + /// 获取生成的参数实体类 + Map get generatedParameterEntities => + _generatedParameterEntities; + + /// 生成参数实体类文件 + Map generateParameterEntityFiles() { + final files = {}; + + for (final entry in _generatedParameterEntities.entries) { + final className = entry.key; + final content = entry.value; + final fileName = StringUtils.generateFileName(className); + files[fileName] = content; + } + + return files; + } + + /// 确保参数实体类已生成(在调用 generate 之前调用) + void ensureParameterEntitiesGenerated() { + // 遍历所有路径,确保参数实体类已生成 + for (final path in document.paths.values) { + final queryParams = path.parameters + .where((p) => p.location == ParameterLocation.query) + .toList(); + + if (path.method == HttpMethod.get && queryParams.length > 4) { + final parameterEntityClassName = + _generateParameterEntityClassName(path); + _generateParameterEntity(path, parameterEntityClassName, queryParams); + } + } + } + + /// 基于 operationId 推断类型 + String? _inferTypeFromOperationId(String operationId) { + // 基于 operationId 的模式匹配推断类型 + if (operationId.contains('login')) { + return 'UserLoginResult'; + } + if (operationId.contains('register')) { + return 'UserLoginResult'; + } + if (operationId.contains('get') && operationId.contains('list')) { + // 根据 operationId 推断列表类型 + if (operationId.contains('class')) { + return 'List'; + } + if (operationId.contains('task')) { + return 'List'; + } + if (operationId.contains('school')) { + return 'List'; + } + if (operationId.contains('user')) { + return 'List'; + } + } + if (operationId.contains('upload')) { + return 'SysFileViewDto'; + } + if (operationId.contains('config')) { + return 'OsConfigResult'; + } + if (operationId.contains('sign')) { + return 'OssSignResult'; + } + if (operationId.contains('version')) { + return 'UpdateappResult'; + } + + return null; + } + + /// 基于路径关键词推断类型 + String? _inferTypeFromPathKeywords( + String pathLower, String summaryLower, List tags) { + // 基于路径关键词的智能推断 + if (pathLower.contains('/login/')) { + return 'UserLoginResult'; + } + + if (pathLower.contains('/get') && pathLower.contains('list')) { + // 列表类型的推断 + if (pathLower.contains('class') && !pathLower.contains('task')) { + return 'List'; + } + if (pathLower.contains('task')) { + return 'List'; + } + if (pathLower.contains('school')) { + return 'List'; + } + if (pathLower.contains('user')) { + return 'List'; + } + } + + if (pathLower.contains('/upload') || pathLower.contains('/file')) { + return 'SysFileViewDto'; + } + + if (pathLower.contains('/config')) { + return 'OsConfigResult'; + } + + if (pathLower.contains('/sign')) { + return 'OssSignResult'; + } + + if (pathLower.contains('/version')) { + return 'UpdateappResult'; + } + + // 基于 summary 和 tags 的推断 + if (summaryLower.contains('列表') || summaryLower.contains('分页')) { + return 'List'; + } + + if (tags.any((tag) => tag.contains('task'))) { + return 'TaskInfoResult'; + } + + return null; + } +} + +/// API 方法参数 +class ApiMethodParameter { + final String name; + final String type; + final String annotation; + final bool required; + + ApiMethodParameter({ + required this.name, + required this.type, + required this.annotation, + required this.required, + }); +} diff --git a/lib/parsers/swagger_data_parser.dart b/lib/parsers/swagger_data_parser.dart new file mode 100644 index 0000000..d6fe40b --- /dev/null +++ b/lib/parsers/swagger_data_parser.dart @@ -0,0 +1,412 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:http/http.dart' as http; + +import '../core/config.dart'; +import '../core/exceptions.dart'; +import '../core/models.dart'; +import '../utils/cache_manager.dart'; +import '../utils/performance_monitor.dart'; +import '../utils/string_utils.dart'; +import '../utils/type_validator.dart'; + +/// Swagger数据解析器 +/// 负责解析Swagger JSON文档并提取相关信息 +class SwaggerDataParser { + final CacheManager _cacheManager; + final PerformanceMonitor _performanceMonitor; + final TypeValidator _typeValidator; + + // 缓存解析结果 + SwaggerDocument? _cachedDocument; + + SwaggerDataParser() + : _cacheManager = CacheManager(), + _performanceMonitor = PerformanceMonitor(), + _typeValidator = TypeValidator(); + + /// 获取并解析Swagger JSON文档 + Future fetchAndParseSwaggerDocument() async { + // 如果有缓存且未过期,直接返回缓存结果 + if (_cachedDocument != null) { + return _cachedDocument!; + } + + return _performanceMonitor.measure( + 'fetchAndParseSwaggerDocument', + () async { + try { + print('🔄 正在获取Swagger JSON文档...'); + + Map jsonData; + + if (SwaggerConfig.swaggerJsonUrl.startsWith('file://')) { + // 处理本地文件 + final filePath = + SwaggerConfig.swaggerJsonUrl.replaceFirst('file://', ''); + final file = File(filePath); + if (await file.exists()) { + final content = await file.readAsString(); + jsonData = json.decode(content) as Map; + } else { + throw SwaggerParseException( + '本地文件不存在', + url: SwaggerConfig.swaggerJsonUrl, + details: '文件路径: $filePath', + ); + } + } else { + // 处理远程URL + final response = await http.get( + Uri.parse(SwaggerConfig.swaggerJsonUrl), + headers: SwaggerConfig.httpHeaders, + ); + + if (response.statusCode == 200) { + jsonData = json.decode(response.body) as Map; + } else { + throw SwaggerParseException( + 'HTTP请求失败', + url: SwaggerConfig.swaggerJsonUrl, + statusCode: response.statusCode, + details: 'HTTP响应状态码: ${response.statusCode}', + ); + } + } + + _cachedDocument = await parseSwaggerDocument(jsonData); + print('✅ Swagger文档解析完成'); + return _cachedDocument!; + } catch (e) { + if (e is SwaggerParseException) { + rethrow; + } + throw SwaggerParseException( + '获取Swagger文档失败', + url: SwaggerConfig.swaggerJsonUrl, + details: e.toString(), + ); + } + }, + ); + } + + /// 解析Swagger JSON文档 + Future parseSwaggerDocument( + Map jsonData, + ) async { + return _performanceMonitor.measure('parseSwaggerDocument', () async { + // 尝试从缓存获取 + final cacheKey = 'swagger_doc_${jsonData.hashCode}'; + final cachedResult = _cacheManager.get(cacheKey); + if (cachedResult != null) { + _cachedDocument = cachedResult; + return cachedResult; + } + + // 解析文档基本信息 + final info = jsonData['info'] as Map? ?? {}; + final title = info['title'] as String? ?? 'API Documentation'; + final version = info['version'] as String? ?? '1.0.0'; + final description = info['description'] as String? ?? ''; + + // 解析其他基本信息 + final host = jsonData['host'] as String? ?? ''; + final basePath = jsonData['basePath'] as String? ?? '/'; + final schemes = List.from(jsonData['schemes'] ?? ['https']); + final consumes = List.from(jsonData['consumes'] ?? []); + final produces = List.from(jsonData['produces'] ?? []); + + // 解析tags信息 (用于获取控制器描述) + final tagsInfo = _parseTagsInfo(jsonData); + + // 解析API路径 + final paths = _parseApiPaths(jsonData); + + // 解析API模型 + final models = _parseApiModels(jsonData); + + // 解析API控制器 (传入tags信息) + final controllers = _parseApiControllers(paths, tagsInfo); + + final document = SwaggerDocument( + title: title, + version: version, + description: description, + host: host, + basePath: basePath, + schemes: schemes, + consumes: consumes, + produces: produces, + paths: paths, + models: models, + controllers: controllers, + ); + + // 缓存结果 + _cacheManager.put(cacheKey, document); + _cachedDocument = document; + + return document; + }); + } + + /// 解析tags信息 + Map _parseTagsInfo(Map jsonData) { + final tagsInfo = {}; + + try { + final tags = jsonData['tags'] as List?; + if (tags != null) { + for (final tag in tags) { + if (tag is Map) { + final name = tag['name'] as String?; + final description = tag['description'] as String?; + if (name != null && description != null) { + tagsInfo[name] = description; + } + } + } + } + } catch (e) { + print('⚠️ 解析tags信息时发生错误: $e'); + } + + return tagsInfo; + } + + /// 解析API路径 + Map _parseApiPaths(Map jsonData) { + final paths = {}; + final pathsData = jsonData['paths'] as Map?; + + if (pathsData == null) { + throw SwaggerParseException('未发现API路径定义'); + } + + try { + pathsData.forEach((pathKey, pathValue) { + if (pathValue is Map) { + pathValue.forEach((methodKey, methodValue) { + if (methodValue is Map) { + final method = HttpMethod.fromString(methodKey); + final apiPath = ApiPath.fromJson( + pathKey, + methodKey, // 传入字符串而不是HttpMethod对象 + methodValue, + ); + final key = + '${method.value.toUpperCase()}_${pathKey.replaceAll('/', '_')}'; + paths[key] = apiPath; + } + }); + } + }); + } catch (e) { + throw SwaggerParseException('解析API路径失败', details: e.toString()); + } + + return paths; + } + + /// 解析API模型 + Map _parseApiModels(Map jsonData) { + final models = {}; + + // 优先解析 components/schemas (Swagger 3.0) + final schemas = jsonData['components']?['schemas'] as Map?; + // 如果没有 components/schemas,尝试解析 definitions (Swagger 2.0) + final definitions = jsonData['definitions'] as Map?; + + final modelDefinitions = schemas ?? definitions; + + if (modelDefinitions == null) { + print('ℹ️ 未发现模型定义 (components/schemas 或 definitions)'); + return models; + } + + print( + '🔍 发现模型定义位置: ${schemas != null ? 'components/schemas' : 'definitions'}', + ); + + try { + modelDefinitions.forEach((name, definition) { + final model = ApiModel.fromJson( + name, + definition as Map, + ); + models[name] = model; + }); + } catch (e) { + throw SwaggerParseException('解析API模型失败', details: e.toString()); + } + + return models; + } + + /// 解析API控制器 + Map _parseApiControllers( + Map paths, + Map tagsInfo, + ) { + final controllers = >{}; + + try { + // 根据tags分组API路径 + for (final apiPath in paths.values) { + for (final tag in apiPath.tags) { + controllers.putIfAbsent(tag, () => []).add(apiPath); + } + } + + // 创建控制器对象 + final result = {}; + 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 propData) { + try { + // 直接类型 + if (propData['type'] != null) { + return propData['type'] as String; + } + + // 引用类型 ($ref) + if (propData['\$ref'] != null) { + final ref = propData['\$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; + 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'; + case 'object': + return 'Map'; + 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() { + _cachedDocument = null; + _cacheManager.clear(); + } + + /// 获取文档统计信息 + Map getDocumentStats() { + if (_cachedDocument == null) { + return {}; + } + + final doc = _cachedDocument!; + + // 统计HTTP方法 + final methodStats = {}; + for (final path in doc.paths.values) { + final method = path.method.value; + methodStats[method] = (methodStats[method] ?? 0) + 1; + } + + // 统计模型类型 + final modelStats = {}; + 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, + }; + } +} diff --git a/lib/swagger_cli_new.dart b/lib/swagger_cli_new.dart new file mode 100644 index 0000000..a0f1dc9 --- /dev/null +++ b/lib/swagger_cli_new.dart @@ -0,0 +1,193 @@ +import 'dart:io'; + +import 'commands/base_command.dart'; +import 'commands/generate_command.dart'; +import 'core/config.dart'; +import 'utils/performance_monitor.dart'; +import 'utils/string_utils.dart'; + +/// Swagger CLI 应用程序 +/// 使用命令模式架构的新版本CLI工具 +class SwaggerCLI { + final Map _commands = {}; + final PerformanceMonitor _monitor = PerformanceMonitor(); + + SwaggerCLI() { + _registerCommands(); + } + + /// 注册所有命令 + void _registerCommands() { + _registerCommand(GenerateCommand()); + // 未来可以添加更多命令: + // _registerCommand(ParseCommand()); + // _registerCommand(ValidateCommand()); + // _registerCommand(InfoCommand()); + // _registerCommand(TestCommand()); + // _registerCommand(CleanCommand()); + } + + /// 注册单个命令 + void _registerCommand(BaseCommand command) { + _commands[command.name] = command; + } + + /// 运行CLI应用程序 + Future run(List arguments) async { + try { + _showBanner(); + + if (arguments.isEmpty || + arguments.first == 'help' || + arguments.first == '--help') { + _showHelp(); + return 0; + } + + final commandName = arguments.first; + final commandArgs = + arguments.length > 1 ? arguments.sublist(1) : []; + + // 检查特殊命令 + if (commandName == 'version' || commandName == '--version') { + _showVersion(); + return 0; + } + + // 查找并执行命令 + final command = _commands[commandName]; + if (command == null) { + print('❌ 未知命令: $commandName'); + print(''); + _showAvailableCommands(); + return 1; + } + + // 检查命令帮助 + if (commandArgs.contains('--help') || commandArgs.contains('-h')) { + command.showHelp(); + return 0; + } + + // 执行命令 + final stopwatch = Stopwatch()..start(); + final exitCode = await command.execute(commandArgs); + stopwatch.stop(); + + // 显示执行时间 + if (exitCode == 0) { + print(''); + print('⏱️ 执行时间: ${StringUtils.formatDuration(stopwatch.elapsed)}'); + } + + return exitCode; + } catch (error, stackTrace) { + print('❌ 应用程序错误: $error'); + print('堆栈跟踪: $stackTrace'); + return 1; + } + } + + /// 显示应用程序横幅 + void _showBanner() { + print(''); + print('🚀 Swagger API 代码生成器 v2.0'); + print('====================================='); + print('强大的 Swagger API 代码生成工具'); + print(''); + } + + /// 显示帮助信息 + void _showHelp() { + print('用法: dart swagger_cli_new.dart <命令> [选项]'); + print(''); + print('全新的命令式架构,提供更好的可扩展性和用户体验。'); + print(''); + _showAvailableCommands(); + _showGlobalOptions(); + _showExamples(); + _showContact(); + } + + /// 显示可用命令 + void _showAvailableCommands() { + print('📋 可用命令:'); + print(''); + + for (final command in _commands.values) { + print(' ${command.name.padRight(12)} ${command.description}'); + } + + print(' help 显示帮助信息'); + print(' version 显示版本信息'); + print(''); + } + + /// 显示全局选项 + void _showGlobalOptions() { + print('🔧 全局选项:'); + print(' -h, --help 显示帮助信息'); + print(' --version 显示版本信息'); + print(''); + } + + /// 显示使用示例 + void _showExamples() { + print('💡 使用示例:'); + print(''); + print(' # 生成所有文件'); + print(' dart swagger_cli_new.dart generate --all'); + print(''); + print(' # 只生成模型文件(简洁版本)'); + print(' dart swagger_cli_new.dart generate --models --simple'); + print(''); + print(' # 生成到指定目录并启用性能监控'); + print( + ' dart swagger_cli_new.dart generate --all --output-dir lib/generated --performance', + ); + print(''); + print(' # 查看具体命令的帮助'); + print(' dart swagger_cli_new.dart generate --help'); + print(''); + } + + /// 显示联系信息 + void _showContact() { + print('🌐 更多信息:'); + print(' API文档: ${SwaggerConfig.swaggerJsonUrl}'); + print(' 基础URL: ${SwaggerConfig.baseUrl}'); + print(''); + } + + /// 显示版本信息 + void _showVersion() { + print('Swagger CLI v2.0.0'); + print('构建于: ${DateTime.now().toIso8601String()}'); + print('Dart SDK: ${Platform.version}'); + print(''); + print('功能特性:'); + print('- 🏗️ 模块化命令架构'); + print('- 🚀 性能监控和优化'); + print('- 🔍 智能类型验证'); + print('- 📋 详细的错误报告'); + print('- 💾 智能缓存机制'); + print('- 📚 丰富的文档生成'); + print(''); + } + + /// 格式化持续时间 + // 已移动到 StringUtils.formatDuration + + /// 获取可用命令列表 + List get availableCommands => _commands.keys.toList(); + + /// 获取特定命令 + BaseCommand? getCommand(String name) => _commands[name]; +} + +/// CLI应用程序入口点 +Future main(List arguments) async { + final cli = SwaggerCLI(); + final exitCode = await cli.run(arguments); + exit(exitCode); +} diff --git a/lib/utils/cache_manager.dart b/lib/utils/cache_manager.dart new file mode 100644 index 0000000..df6b593 --- /dev/null +++ b/lib/utils/cache_manager.dart @@ -0,0 +1,125 @@ +import 'dart:collection'; + +import 'string_utils.dart'; + +/// 缓存管理器 +/// 提供简单的内存缓存功能 +class CacheManager { + static const int _maxMemoryItems = 100; + + // 内存缓存 + final Map _memoryCache = {}; + final Queue _accessOrder = Queue(); + + CacheManager(); + + /// 从缓存获取数据 + T? get(String key) { + final entry = _memoryCache[key]; + if (entry != null && !entry.isExpired) { + _updateAccessOrder(key); + return entry.value as T?; + } + return null; + } + + /// 存储数据到缓存 + void put(String key, T value, [Duration? ttl]) { + ttl ??= const Duration(hours: 1); + + // 检查内存缓存大小限制 + if (_memoryCache.length >= _maxMemoryItems) { + _evictOldestItem(); + } + + final entry = _CacheEntry( + value: value, + expiresAt: DateTime.now().add(ttl), + ); + + _memoryCache[key] = entry; + _updateAccessOrder(key); + } + + /// 检查是否存在缓存 + bool has(String key) { + final entry = _memoryCache[key]; + return entry != null && !entry.isExpired; + } + + /// 删除缓存项 + void remove(String key) { + _memoryCache.remove(key); + _accessOrder.removeWhere((k) => k == key); + } + + /// 清空缓存 + void clear() { + _memoryCache.clear(); + _accessOrder.clear(); + } + + /// 获取缓存统计信息 + CacheStats getStats() { + return CacheStats( + memoryItems: _memoryCache.length, + diskItems: 0, + hitRate: 0.0, + totalSize: 0, + ); + } + + /// 更新访问顺序 + void _updateAccessOrder(String key) { + _accessOrder.removeWhere((k) => k == key); + _accessOrder.addLast(key); + } + + /// 驱逐最旧的项 + void _evictOldestItem() { + if (_accessOrder.isNotEmpty) { + final oldestKey = _accessOrder.removeFirst(); + _memoryCache.remove(oldestKey); + } + } +} + +/// 缓存条目 +class _CacheEntry { + final dynamic value; + final DateTime expiresAt; + + _CacheEntry({ + required this.value, + required this.expiresAt, + }); + + bool get isExpired => DateTime.now().isAfter(expiresAt); +} + +/// 缓存统计信息 +class CacheStats { + final int memoryItems; + final int diskItems; + final double hitRate; + final int totalSize; + + const CacheStats({ + required this.memoryItems, + required this.diskItems, + required this.hitRate, + required this.totalSize, + }); + + @override + String toString() { + return 'CacheStats{' + 'memoryItems: $memoryItems, ' + 'diskItems: $diskItems, ' + 'hitRate: ${(hitRate * 100).toStringAsFixed(1)}%, ' + 'totalSize: ${StringUtils.formatBytes(totalSize)}' + '}'; + } + + // 已移动到 StringUtils.formatBytes +} diff --git a/lib/utils/file_utils.dart b/lib/utils/file_utils.dart new file mode 100644 index 0000000..1ebe935 --- /dev/null +++ b/lib/utils/file_utils.dart @@ -0,0 +1,457 @@ +import 'dart:io'; + +import 'package:path/path.dart' as path; + +/// 文件工具类 +/// 提供文件操作、目录管理和代码格式化功能 +class FileUtils { + /// 确保目录存在 + static Future ensureDirectoryExists(String dirPath) async { + final directory = Directory(dirPath); + if (!await directory.exists()) { + await directory.create(recursive: true); + } + return directory; + } + + /// 安全写入文件 + static Future safeWriteFile(String filePath, String content) async { + try { + final file = File(filePath); + final directory = file.parent; + + // 确保目录存在 + if (!await directory.exists()) { + await directory.create(recursive: true); + } + + // 写入文件 + await file.writeAsString(content); + } catch (e) { + throw FileSystemException('写入文件失败: $filePath', filePath); + } + } + + /// 安全读取文件 + static Future safeReadFile(String filePath) async { + try { + final file = File(filePath); + if (!await file.exists()) { + throw FileSystemException('文件不存在: $filePath', filePath); + } + + return await file.readAsString(); + } catch (e) { + throw FileSystemException('读取文件失败: $filePath', filePath); + } + } + + /// 检查文件是否存在 + static Future fileExists(String filePath) async { + return await File(filePath).exists(); + } + + /// 检查目录是否存在 + static Future directoryExists(String dirPath) async { + return await Directory(dirPath).exists(); + } + + /// 删除文件(如果存在) + static Future deleteFileIfExists(String filePath) async { + final file = File(filePath); + if (await file.exists()) { + await file.delete(); + } + } + + /// 删除目录(如果存在) + static Future deleteDirectoryIfExists(String dirPath) async { + final directory = Directory(dirPath); + if (await directory.exists()) { + await directory.delete(recursive: true); + } + } + + /// 复制文件 + static Future copyFile( + String sourcePath, String destinationPath) async { + try { + final sourceFile = File(sourcePath); + final destinationFile = File(destinationPath); + + if (!await sourceFile.exists()) { + throw FileSystemException('源文件不存在: $sourcePath', sourcePath); + } + + // 确保目标目录存在 + final destinationDir = destinationFile.parent; + if (!await destinationDir.exists()) { + await destinationDir.create(recursive: true); + } + + await sourceFile.copy(destinationPath); + } catch (e) { + throw FileSystemException('复制文件失败: $sourcePath -> $destinationPath', + sourcePath, e is OSError ? e : null); + } + } + + /// 移动文件 + static Future moveFile( + String sourcePath, String destinationPath) async { + try { + final sourceFile = File(sourcePath); + final destinationFile = File(destinationPath); + + if (!await sourceFile.exists()) { + throw FileSystemException('源文件不存在: $sourcePath', sourcePath); + } + + // 确保目标目录存在 + final destinationDir = destinationFile.parent; + if (!await destinationDir.exists()) { + await destinationDir.create(recursive: true); + } + + await sourceFile.rename(destinationPath); + } catch (e) { + throw FileSystemException('移动文件失败: $sourcePath -> $destinationPath', + sourcePath, e is OSError ? e : null); + } + } + + /// 获取文件大小 + static Future getFileSize(String filePath) async { + try { + final file = File(filePath); + if (!await file.exists()) { + return 0; + } + return await file.length(); + } catch (e) { + return 0; + } + } + + /// 获取目录大小 + static Future getDirectorySize(String dirPath) async { + try { + final directory = Directory(dirPath); + if (!await directory.exists()) { + return 0; + } + + int totalSize = 0; + await for (final entity in directory.list(recursive: true)) { + if (entity is File) { + totalSize += await entity.length(); + } + } + return totalSize; + } catch (e) { + return 0; + } + } + + /// 列出目录中的文件 + static Future> listFiles(String dirPath, + {String? extension}) async { + try { + final directory = Directory(dirPath); + if (!await directory.exists()) { + return []; + } + + final files = []; + await for (final entity in directory.list()) { + if (entity is File) { + if (extension == null || entity.path.endsWith(extension)) { + files.add(entity.path); + } + } + } + return files; + } catch (e) { + return []; + } + } + + /// 列出目录中的子目录 + static Future> listDirectories(String dirPath) async { + try { + final directory = Directory(dirPath); + if (!await directory.exists()) { + return []; + } + + final directories = []; + await for (final entity in directory.list()) { + if (entity is Directory) { + directories.add(entity.path); + } + } + return directories; + } catch (e) { + return []; + } + } + + /// 创建备份文件 + static Future createBackup(String filePath) async { + try { + final file = File(filePath); + if (!await file.exists()) { + throw FileSystemException('文件不存在: $filePath', filePath); + } + + final timestamp = DateTime.now().toIso8601String().replaceAll(':', '-'); + final backupPath = '${filePath}.backup.$timestamp'; + + await file.copy(backupPath); + return backupPath; + } catch (e) { + throw FileSystemException( + '创建备份失败: $filePath', filePath, e is OSError ? e : null); + } + } + + /// 恢复备份文件 + static Future restoreBackup( + String backupPath, String originalPath) async { + try { + final backupFile = File(backupPath); + if (!await backupFile.exists()) { + throw FileSystemException('备份文件不存在: $backupPath', backupPath); + } + + await backupFile.copy(originalPath); + } catch (e) { + throw FileSystemException('恢复备份失败: $backupPath -> $originalPath', + backupPath, e is OSError ? e : null); + } + } + + /// 格式化文件路径 + static String formatPath(String filePath) { + return path.normalize(filePath); + } + + /// 获取文件名(不包括路径) + static String getFileName(String filePath) { + return path.basename(filePath); + } + + /// 获取文件名(不包括扩展名) + static String getFileNameWithoutExtension(String filePath) { + return path.basenameWithoutExtension(filePath); + } + + /// 获取文件扩展名 + static String getFileExtension(String filePath) { + return path.extension(filePath); + } + + /// 获取文件所在目录 + static String getDirectoryPath(String filePath) { + return path.dirname(filePath); + } + + /// 连接路径 + static String joinPath(List parts) { + return path.joinAll(parts); + } + + /// 获取相对路径 + static String getRelativePath(String filePath, String basePath) { + return path.relative(filePath, from: basePath); + } + + /// 获取绝对路径 + static String getAbsolutePath(String filePath) { + return path.absolute(filePath); + } + + /// 检查路径是否为绝对路径 + static bool isAbsolute(String filePath) { + return path.isAbsolute(filePath); + } + + /// 清理文件名(移除不合法字符) + static String sanitizeFileName(String fileName) { + // 移除或替换不合法的文件名字符 + return fileName + .replaceAll(RegExp(r'[<>:"/\\|?*]'), '_') + .replaceAll(RegExp(r'\s+'), '_') + .replaceAll(RegExp(r'_{2,}'), '_') + .replaceAll(RegExp(r'^_|_$'), ''); + } + + /// 生成唯一文件名 + static Future generateUniqueFileName( + String basePath, String fileName) async { + final directory = Directory(basePath); + final extension = getFileExtension(fileName); + final nameWithoutExt = getFileNameWithoutExtension(fileName); + + String uniqueName = fileName; + int counter = 1; + + while (await File(path.join(basePath, uniqueName)).exists()) { + uniqueName = '${nameWithoutExt}_$counter$extension'; + counter++; + } + + return uniqueName; + } + + /// 批量操作文件 + static Future batchOperation( + List filePaths, + Future Function(String filePath) operation, + ) async { + for (final filePath in filePaths) { + try { + await operation(filePath); + } catch (e) { + print('批量操作失败: $filePath - $e'); + } + } + } + + /// 查找文件 + static Future> findFiles( + String searchPath, + String pattern, { + bool recursive = false, + }) async { + try { + final directory = Directory(searchPath); + if (!await directory.exists()) { + return []; + } + + final regex = RegExp(pattern); + final foundFiles = []; + + await for (final entity in directory.list(recursive: recursive)) { + if (entity is File) { + final fileName = getFileName(entity.path); + if (regex.hasMatch(fileName)) { + foundFiles.add(entity.path); + } + } + } + + return foundFiles; + } catch (e) { + return []; + } + } + + /// 获取文件修改时间 + static Future getFileModifiedTime(String filePath) async { + try { + final file = File(filePath); + if (!await file.exists()) { + return null; + } + + final stat = await file.stat(); + return stat.modified; + } catch (e) { + return null; + } + } + + /// 比较文件修改时间 + static Future isFileNewer(String filePath1, String filePath2) async { + final time1 = await getFileModifiedTime(filePath1); + final time2 = await getFileModifiedTime(filePath2); + + if (time1 == null || time2 == null) { + return false; + } + + return time1.isAfter(time2); + } + + /// 计算文件哈希 + static Future calculateFileHash(String filePath) async { + try { + final file = File(filePath); + if (!await file.exists()) { + return null; + } + + final bytes = await file.readAsBytes(); + return bytes.hashCode.toString(); + } catch (e) { + return null; + } + } + + /// 格式化文件大小 + static String formatFileSize(int bytes) { + if (bytes < 1024) { + return '${bytes}B'; + } else if (bytes < 1024 * 1024) { + return '${(bytes / 1024).toStringAsFixed(1)}KB'; + } else if (bytes < 1024 * 1024 * 1024) { + return '${(bytes / (1024 * 1024)).toStringAsFixed(1)}MB'; + } else { + return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)}GB'; + } + } + + /// 创建临时文件 + static Future createTempFile(String prefix, {String? suffix}) async { + final tempDir = Directory.systemTemp; + final timestamp = DateTime.now().millisecondsSinceEpoch; + final fileName = '$prefix$timestamp${suffix ?? ''}'; + + return File(path.join(tempDir.path, fileName)); + } + + /// 清理临时文件 + static Future cleanupTempFiles(String pattern) async { + try { + final tempDir = Directory.systemTemp; + final regex = RegExp(pattern); + + await for (final entity in tempDir.list()) { + if (entity is File) { + final fileName = getFileName(entity.path); + if (regex.hasMatch(fileName)) { + await entity.delete(); + } + } + } + } catch (e) { + print('清理临时文件失败: $e'); + } + } + + /// 获取项目根目录下的generator目录路径(兼容旧版本) + static String getProjectRootGeneratorDir() { + final currentDir = Directory.current.path; + return joinPath([currentDir, 'generator']); + } + + /// 安全地写入文件(兼容旧版本) + static Future writeFile(String path, String content) async { + await safeWriteFile(path, content); + } + + /// 获取目录中的文件列表(兼容旧版本) + static Future> listDirectory(String path) async { + try { + final directory = Directory(path); + if (!await directory.exists()) { + return []; + } + return await directory.list().toList(); + } catch (e) { + return []; + } + } +} diff --git a/lib/utils/performance_monitor.dart b/lib/utils/performance_monitor.dart new file mode 100644 index 0000000..449a00d --- /dev/null +++ b/lib/utils/performance_monitor.dart @@ -0,0 +1,335 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:logging/logging.dart'; + +/// 性能监控器 +/// 用于监控和记录应用程序性能指标 +class PerformanceMonitor { + static final Logger _logger = Logger('PerformanceMonitor'); + + final Map _metrics = {}; + final Map> _measurements = {}; + final bool _enabled; + + PerformanceMonitor({bool enabled = true}) : _enabled = enabled { + if (_enabled) { + _logger.info('性能监控器已启用'); + } + } + + /// 测量异步操作的执行时间 + Future measure( + String operationName, Future Function() operation) async { + if (!_enabled) { + return await operation(); + } + + final stopwatch = Stopwatch()..start(); + + try { + final result = await operation(); + stopwatch.stop(); + + _recordMeasurement(operationName, stopwatch.elapsed); + + return result; + } catch (e) { + stopwatch.stop(); + _recordMeasurement(operationName, stopwatch.elapsed, error: e); + rethrow; + } + } + + /// 测量同步操作的执行时间 + T measureSync(String operationName, T Function() operation) { + if (!_enabled) { + return operation(); + } + + final stopwatch = Stopwatch()..start(); + + try { + final result = operation(); + stopwatch.stop(); + + _recordMeasurement(operationName, stopwatch.elapsed); + + return result; + } catch (e) { + stopwatch.stop(); + _recordMeasurement(operationName, stopwatch.elapsed, error: e); + rethrow; + } + } + + /// 开始计时 + PerformanceTimer startTimer(String operationName) { + return PerformanceTimer(operationName, this); + } + + /// 记录测量结果 + void _recordMeasurement(String operationName, Duration duration, + {dynamic error}) { + if (!_enabled) return; + + // 记录到测量历史中 + _measurements.putIfAbsent(operationName, () => []); + _measurements[operationName]!.add(duration); + + // 更新或创建性能指标 + if (_metrics.containsKey(operationName)) { + _metrics[operationName]!.addMeasurement(duration, error: error); + } else { + _metrics[operationName] = PerformanceMetric(operationName); + _metrics[operationName]!.addMeasurement(duration, error: error); + } + + // 记录日志 + final status = error != null ? 'ERROR' : 'SUCCESS'; + _logger.info('$operationName: ${duration.inMilliseconds}ms [$status]'); + } + + /// 获取所有性能指标 + Map getMetrics() { + return Map.unmodifiable(_metrics); + } + + /// 获取特定操作的性能指标 + PerformanceMetric? getMetric(String operationName) { + return _metrics[operationName]; + } + + /// 获取性能报告 + PerformanceReport generateReport() { + final totalOperations = + _metrics.values.fold(0, (sum, metric) => sum + metric.count); + final totalTime = _metrics.values.fold( + Duration.zero, + (sum, metric) => sum + metric.totalTime, + ); + + return PerformanceReport( + totalOperations: totalOperations, + totalTime: totalTime, + metrics: Map.from(_metrics), + generatedAt: DateTime.now(), + ); + } + + /// 清空所有指标 + void clear() { + _metrics.clear(); + _measurements.clear(); + } + + /// 获取慢操作(执行时间超过阈值) + List getSlowOperations( + [Duration threshold = const Duration(milliseconds: 500)]) { + final slowOps = []; + + for (final metric in _metrics.values) { + if (metric.maxTime >= threshold) { + slowOps.add(SlowOperation( + name: metric.operationName, + maxTime: metric.maxTime, + avgTime: metric.averageTime, + count: metric.count, + )); + } + } + + // 按最大执行时间排序 + slowOps.sort((a, b) => b.maxTime.compareTo(a.maxTime)); + + return slowOps; + } + + /// 导出性能数据到文件 + Future exportToFile(String filePath) async { + if (!_enabled) return; + + try { + final report = generateReport(); + final data = { + 'generatedAt': report.generatedAt.toIso8601String(), + 'summary': { + 'totalOperations': report.totalOperations, + 'totalTime': report.totalTime.inMilliseconds, + }, + 'metrics': report.metrics.map((key, value) => MapEntry(key, { + 'operationName': value.operationName, + 'count': value.count, + 'totalTime': value.totalTime.inMilliseconds, + 'averageTime': value.averageTime.inMilliseconds, + 'minTime': value.minTime.inMilliseconds, + 'maxTime': value.maxTime.inMilliseconds, + 'errorCount': value.errorCount, + 'lastExecuted': value.lastExecuted?.toIso8601String(), + })), + }; + + final file = File(filePath); + await file.writeAsString(json.encode(data)); + + _logger.info('性能数据已导出到: $filePath'); + } catch (e) { + _logger.severe('导出性能数据失败: $e'); + } + } + + /// 打印性能摘要 + void printSummary() { + if (!_enabled) { + print('性能监控器已禁用'); + return; + } + + if (_metrics.isEmpty) { + print('没有性能数据'); + return; + } + + print('\n🔍 性能监控摘要:'); + print('=' * 50); + + final sortedMetrics = _metrics.values.toList() + ..sort((a, b) => b.totalTime.compareTo(a.totalTime)); + + for (final metric in sortedMetrics) { + print('${metric.operationName}:'); + print(' 执行次数: ${metric.count}'); + print(' 总时间: ${metric.totalTime.inMilliseconds}ms'); + print(' 平均时间: ${metric.averageTime.inMilliseconds}ms'); + print(' 最小时间: ${metric.minTime.inMilliseconds}ms'); + print(' 最大时间: ${metric.maxTime.inMilliseconds}ms'); + if (metric.errorCount > 0) { + print(' 错误次数: ${metric.errorCount}'); + } + print(''); + } + + final slowOps = getSlowOperations(); + if (slowOps.isNotEmpty) { + print('🐌 慢操作 (>500ms):'); + for (final op in slowOps.take(5)) { + print(' ${op.name}: ${op.maxTime.inMilliseconds}ms (最大)'); + } + } + } +} + +/// 性能计时器 +class PerformanceTimer { + final String operationName; + final PerformanceMonitor monitor; + final Stopwatch _stopwatch; + + PerformanceTimer(this.operationName, this.monitor) + : _stopwatch = Stopwatch()..start(); + + /// 停止计时并记录结果 + void stop({dynamic error}) { + if (!_stopwatch.isRunning) return; + + _stopwatch.stop(); + monitor._recordMeasurement(operationName, _stopwatch.elapsed, error: error); + } + + /// 获取当前经过的时间 + Duration get elapsed => _stopwatch.elapsed; +} + +/// 性能指标 +class PerformanceMetric { + final String operationName; + int count = 0; + Duration totalTime = Duration.zero; + Duration minTime = Duration.zero; + Duration maxTime = Duration.zero; + int errorCount = 0; + DateTime? lastExecuted; + + PerformanceMetric(this.operationName); + + /// 添加测量结果 + void addMeasurement(Duration duration, {dynamic error}) { + count++; + totalTime += duration; + lastExecuted = DateTime.now(); + + if (error != null) { + errorCount++; + } + + if (count == 1) { + minTime = duration; + maxTime = duration; + } else { + if (duration < minTime) minTime = duration; + if (duration > maxTime) maxTime = duration; + } + } + + /// 平均执行时间 + Duration get averageTime { + if (count == 0) return Duration.zero; + return Duration(microseconds: totalTime.inMicroseconds ~/ count); + } + + /// 成功率 + double get successRate { + if (count == 0) return 0.0; + return (count - errorCount) / count; + } +} + +/// 性能报告 +class PerformanceReport { + final int totalOperations; + final Duration totalTime; + final Map metrics; + final DateTime generatedAt; + + const PerformanceReport({ + required this.totalOperations, + required this.totalTime, + required this.metrics, + required this.generatedAt, + }); + + /// 获取最慢的操作 + PerformanceMetric? get slowestOperation { + if (metrics.isEmpty) return null; + + return metrics.values.reduce((a, b) => a.maxTime > b.maxTime ? a : b); + } + + /// 获取最频繁的操作 + PerformanceMetric? get mostFrequentOperation { + if (metrics.isEmpty) return null; + + return metrics.values.reduce((a, b) => a.count > b.count ? a : b); + } + + /// 获取平均执行时间 + Duration get averageExecutionTime { + if (totalOperations == 0) return Duration.zero; + return Duration(microseconds: totalTime.inMicroseconds ~/ totalOperations); + } +} + +/// 慢操作信息 +class SlowOperation { + final String name; + final Duration maxTime; + final Duration avgTime; + final int count; + + const SlowOperation({ + required this.name, + required this.maxTime, + required this.avgTime, + required this.count, + }); +} diff --git a/lib/utils/string_utils.dart b/lib/utils/string_utils.dart new file mode 100644 index 0000000..e7fc8c1 --- /dev/null +++ b/lib/utils/string_utils.dart @@ -0,0 +1,343 @@ +/// 字符串工具类 +/// +/// 提供常用的字符串处理功能,包括命名风格转换、注释生成等。 +/// +/// # 典型用法示例 +/// ```dart +/// // snake_case 转 camelCase +/// StringUtils.toDartPropertyName('user_id'); // userId +/// // 含特殊字符 +/// StringUtils.toDartPropertyName('user-id'); // userId +/// // 数字开头 +/// StringUtils.toDartPropertyName('1st_field'); // n1stField +/// // 空字符串 +/// StringUtils.toDartPropertyName(''); // property +/// ``` +/// +/// This utility provides string conversion helpers for code generation, such as +/// converting snake_case to camelCase, generating Dart class names, and cleaning descriptions. +/// +library; + +import '../core/models.dart'; + +class StringUtils { + /// 转换为camelCase + static String toCamelCase(String input) { + if (input.isEmpty) return input; + + final parts = input.split('_').where((p) => p.isNotEmpty).toList(); + if (parts.isEmpty) return input; + + String result = parts.first.toLowerCase(); + for (int i = 1; i < parts.length; i++) { + final part = parts[i]; + if (part.isNotEmpty) { + result += part[0].toUpperCase() + part.substring(1).toLowerCase(); + } + } + + return result.isEmpty ? input : result; + } + + /// 转换为PascalCase + static String toPascalCase(String input) { + if (input.isEmpty) return input; + + // 如果输入包含下划线,先按下划线分割并转换每个部分 + if (input.contains('_')) { + final parts = input.split('_'); + String result = ''; + for (final part in parts) { + if (part.isNotEmpty) { + // 保持每个部分的原始大小写,只确保首字母大写 + if (part[0].toUpperCase() == part[0]) { + // 如果首字母已经是大写,保持整个部分不变 + result += part; + } else { + // 如果首字母是小写,只转换首字母为大写 + result += part[0].toUpperCase() + part.substring(1); + } + } + } + return result.isEmpty ? input : result; + } + + // 如果输入已经是PascalCase格式(没有下划线且首字母大写),直接返回 + if (input[0].toUpperCase() == input[0]) { + return input; + } + + // 如果输入是camelCase格式(没有下划线),转换首字母为大写 + if (RegExp(r'^[a-zA-Z][a-zA-Z0-9]*$').hasMatch(input)) { + return input[0].toUpperCase() + input.substring(1); + } + + return input; + } + + /// 转换为snake_case + static String toSnakeCase(String input) { + if (input.isEmpty) return input; + + // 处理PascalCase和camelCase + final result = + input.replaceAllMapped(RegExp(r'([A-Z]+)([A-Z][a-z])'), (match) { + return '${match[1]!.substring(0, match[1]!.length - 1)}_${match[2]}'; + }).replaceAllMapped(RegExp(r'([a-z\d])([A-Z])'), (match) { + return '${match[1]}_${match[2]}'; + }).toLowerCase(); + + return result; + } + + /// 转换为符合Dart命名规范的属性名 + /// + /// - 支持 snake_case、kebab-case、空格、特殊字符自动转为 camelCase + /// - 已经是驼峰命名的字符串保持不变 + /// - 数字开头自动加前缀 n + /// - 空字符串返回 'property' + /// + /// # 示例 + /// ```dart + /// StringUtils.toDartPropertyName('user_id'); // userId + /// StringUtils.toDartPropertyName('user-id'); // userId + /// StringUtils.toDartPropertyName('1st_field'); // n1stField + /// StringUtils.toDartPropertyName('classCadreId'); // classCadreId + /// StringUtils.toDartPropertyName(''); // property + /// ``` + static String toDartPropertyName(String propName) { + // 如果已经是驼峰命名(没有下划线且首字母小写),直接返回 + if (RegExp(r'^[a-z][a-zA-Z0-9]*$').hasMatch(propName)) { + return propName; + } + + // 处理特殊字符和数字开头的情况 + String result = propName; + + // 如果以数字开头,添加前缀 + if (RegExp(r'^[0-9]').hasMatch(result)) { + result = 'n$result'; + } + + // 替换特殊字符为下划线 + result = result.replaceAll(RegExp(r'[^a-zA-Z0-9_]'), '_'); + + // 转换为camelCase + result = toCamelCase(result); + + // 确保不为空 + if (result.isEmpty) { + result = 'property'; + } + + return result; + } + + /// 清理描述文本,移除特殊字符和格式化多行注释 + static String cleanDescription(String description) { + if (description.isEmpty) return description; + + // 移除多余的空白字符和换行符 + String cleaned = description + .replaceAll(RegExp(r'\s+'), ' ') + .replaceAll(RegExp(r'[\r\n]+'), ' ') + .trim(); + + // 移除可能引起语法错误的特殊字符 + cleaned = cleaned + .replaceAll(RegExp(r'[^\w\s\u4e00-\u9fa5()(),,.。::;;!!??_/\\-]'), '') + .replaceAll(RegExp(r'\s+'), ' ') + .trim(); + + // 如果描述过长,截取前200个字符 + if (cleaned.length > 200) { + cleaned = '${cleaned.substring(0, 200)}...'; + } + + return cleaned; + } + + /// 生成端点名称 + static String generateEndpointName(String path, String? operationId) { + // 如果有operationId,优先使用 + if (operationId != null && operationId.isNotEmpty) { + return toCamelCase(operationId); + } + + // 移除路径中的版本前缀 + String cleanPath = path.replaceFirst('/api/v1', ''); + + // 移除开头的斜杠 + if (cleanPath.startsWith('/')) { + cleanPath = cleanPath.substring(1); + } + + // 将路径转换为camelCase + final parts = cleanPath.split('/'); + if (parts.length >= 2) { + final controller = parts[0]; + final action = parts[1]; + + // 特殊情况处理 + if (controller.toLowerCase() == 'login' && + action.toLowerCase() == 'userlogin') { + return 'login'; + } + + // 转换为camelCase + return toCamelCase('${controller}_$action'); + } + + return toCamelCase(cleanPath.replaceAll('/', '_')); + } + + /// 生成Dart类名 + static String generateClassName(String name) { + // 确保类名以大写字母开头 + final cleanName = name.replaceAll(RegExp(r'[^a-zA-Z0-9_]'), '_'); + return toPascalCase(cleanName); + } + + /// 生成文件名 + static String generateFileName(String name) { + // 转换为snake_case并添加.dart扩展名 + return '${toSnakeCase(name)}.dart'; + } + + /// 验证是否为有效的Dart标识符 + static bool isValidDartIdentifier(String identifier) { + if (identifier.isEmpty) return false; + + // Dart标识符规则:以字母或下划线开头,后面可以是字母、数字或下划线 + final regex = RegExp(r'^[a-zA-Z_][a-zA-Z0-9_]*$'); + return regex.hasMatch(identifier); + } + + /// 生成枚举值名称 + static String generateEnumValueName(dynamic value, int index) { + if (value is String) { + // 尝试从字符串生成合法的枚举名 + final cleanValue = value.replaceAll(RegExp(r'[^a-zA-Z0-9_]'), ''); + if (cleanValue.isNotEmpty && isValidDartIdentifier(cleanValue)) { + return toCamelCase(cleanValue); + } + } + + // 默认使用 value + 索引 + return 'value${index + 1}'; + } + + /// 提取控制器名称 + static String extractControllerName(ApiPath path) { + // 从tags中提取控制器名称 + if (path.tags.isNotEmpty) { + return path.tags.first; + } + + // 从路径中提取控制器名称 + final pathParts = path.path.split('/'); + if (pathParts.length > 1) { + return toPascalCase(pathParts[1]); + } + + return 'General'; + } + + /// 清理路径,移除版本前缀 + static String cleanPath(String path) { + // 移除版本前缀 + return path.replaceFirst(RegExp(r'^/api/v\d+'), ''); + } + + /// 格式化字节大小 + static String formatBytes(int bytes) { + if (bytes < 1024) return '${bytes}B'; + if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)}KB'; + if (bytes < 1024 * 1024 * 1024) + return '${(bytes / (1024 * 1024)).toStringAsFixed(1)}MB'; + return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)}GB'; + } + + /// 格式化持续时间 + static String formatDuration(Duration duration) { + if (duration.inMilliseconds >= 1000) { + return '${(duration.inMilliseconds / 1000).toStringAsFixed(2)}秒'; + } else { + return '${duration.inMilliseconds}毫秒'; + } + } + + /// 转义字符串中的特殊字符 + String escapeString(String input) { + return input + .replaceAll('\\', '\\\\') + .replaceAll('"', '\\"') + .replaceAll('\n', '\\n') + .replaceAll('\r', '\\r') + .replaceAll('\t', '\\t'); + } + + /// 缩进文本 + String indent(String text, int spaces) { + final indentation = ' ' * spaces; + return text.split('\n').map((line) => '$indentation$line').join('\n'); + } + + /// 生成注释块 + static String generateComment(String text, {bool isBlock = false}) { + if (text.isEmpty) return ''; + + final cleanText = cleanDescription(text); + + if (isBlock) { + return '/**\n * $cleanText\n */'; + } else { + return '/// $cleanText'; + } + } + + /// pluralize 单词 + String pluralize(String word) { + if (word.isEmpty) return word; + + final lowerWord = word.toLowerCase(); + + // 特殊复数形式 + const irregularPlurals = { + 'child': 'children', + 'person': 'people', + 'man': 'men', + 'woman': 'women', + 'mouse': 'mice', + 'goose': 'geese', + }; + + if (irregularPlurals.containsKey(lowerWord)) { + return irregularPlurals[lowerWord]!; + } + + // 规则复数形式 + if (lowerWord.endsWith('y')) { + return '${word.substring(0, word.length - 1)}ies'; + } else if (lowerWord.endsWith('s') || + lowerWord.endsWith('sh') || + lowerWord.endsWith('ch') || + lowerWord.endsWith('x') || + lowerWord.endsWith('z')) { + return '${word}es'; + } else { + return '${word}s'; + } + } + + /// 生成文件头注释 + static String generateFileHeader(String description, String source) { + final timestamp = DateTime.now().toIso8601String(); + return '''// $description +// 基于 Swagger API 文档: +// 自动生成于: $timestamp by Max +// Copyright (C) 2025 YuanXuan. All rights reserved. +'''; + } +} diff --git a/lib/utils/type_validator.dart b/lib/utils/type_validator.dart new file mode 100644 index 0000000..ad3b0b7 --- /dev/null +++ b/lib/utils/type_validator.dart @@ -0,0 +1,632 @@ +import '../core/models.dart'; + +/// 类型验证器 +/// 提供严格的类型检查和验证功能 +class TypeValidator { + /// 验证API模型 + static ValidationResult validateApiModel(ApiModel model) { + final errors = []; + final warnings = []; + + // 验证模型名称 + if (!_isValidDartIdentifier(model.name)) { + errors.add( + ValidationError( + field: 'name', + message: '模型名称不符合Dart命名规范: ${model.name}', + severity: ErrorSeverity.error, + ), + ); + } + + // 验证枚举类型 + if (model.isEnum) { + final enumValidation = _validateEnum(model); + errors.addAll(enumValidation.errors); + warnings.addAll(enumValidation.warnings); + } else { + // 验证类属性 + final propertiesValidation = _validateProperties(model.properties); + errors.addAll(propertiesValidation.errors); + warnings.addAll(propertiesValidation.warnings); + } + + return ValidationResult( + isValid: errors.isEmpty, + errors: errors, + warnings: warnings, + ); + } + + /// 验证API属性 + static ValidationResult validateApiProperty( + String propertyName, + ApiProperty property, + ) { + final errors = []; + final warnings = []; + + // 验证属性名称 + if (!_isValidDartIdentifier(propertyName)) { + errors.add( + ValidationError( + field: 'name', + message: '属性名称不符合Dart命名规范: $propertyName', + severity: ErrorSeverity.error, + ), + ); + } + + // 验证类型 + if (!_isValidPropertyType(property.type)) { + errors.add( + ValidationError( + field: 'type', + message: '不支持的属性类型: ${property.type}', + severity: ErrorSeverity.error, + ), + ); + } + + // 验证引用类型 + if (property.type == PropertyType.reference) { + if (property.reference == null || property.reference!.isEmpty) { + errors.add( + ValidationError( + field: 'reference', + message: '引用类型缺少引用目标', + severity: ErrorSeverity.error, + ), + ); + } else if (!_isValidDartIdentifier(property.reference!)) { + errors.add( + ValidationError( + field: 'reference', + message: '引用目标不符合Dart命名规范: ${property.reference}', + severity: ErrorSeverity.error, + ), + ); + } + } + + // 验证可空性和必填性的逻辑 + if (property.required && property.nullable) { + warnings.add('属性 $propertyName 同时标记为必填和可空,这可能导致逻辑冲突'); + } + + // 验证日期格式 + if (property.type == PropertyType.string && + (property.format == 'date' || property.format == 'date-time')) { + // 日期类型的特殊验证 + if (property.example != null && + !_isValidDateFormat(property.example.toString())) { + warnings.add('属性 $propertyName 的示例值不符合日期格式'); + } + } + + return ValidationResult( + isValid: errors.isEmpty, + errors: errors, + warnings: warnings, + ); + } + + /// 验证API路径 + static ValidationResult validateApiPath(ApiPath path) { + final errors = []; + final warnings = []; + + // 验证路径格式 + if (!path.path.startsWith('/')) { + errors.add( + ValidationError( + field: 'path', + message: 'API路径必须以/开头: ${path.path}', + severity: ErrorSeverity.error, + ), + ); + } + + // 验证操作ID + if (path.operationId.isNotEmpty && + !_isValidDartIdentifier(path.operationId)) { + warnings.add('操作ID不符合Dart命名规范: ${path.operationId}'); + } + + // 验证参数 + for (final param in path.parameters) { + final paramValidation = _validateParameter(param); + errors.addAll(paramValidation.errors); + warnings.addAll(paramValidation.warnings); + } + + // 验证路径参数一致性 + final pathParams = _extractPathParameters(path.path); + final definedPathParams = path.parameters + .where((p) => p.location == ParameterLocation.path) + .map((p) => p.name) + .toSet(); + + for (final pathParam in pathParams) { + if (!definedPathParams.contains(pathParam)) { + errors.add( + ValidationError( + field: 'parameters', + message: '路径参数 $pathParam 在路径中定义但未在参数列表中声明', + severity: ErrorSeverity.error, + ), + ); + } + } + + return ValidationResult( + isValid: errors.isEmpty, + errors: errors, + warnings: warnings, + ); + } + + /// 验证Swagger文档 + static ValidationResult validateSwaggerDocument(SwaggerDocument document) { + final errors = []; + final warnings = []; + + // 验证基本信息 + if (document.title.isEmpty) { + warnings.add('文档标题为空'); + } + + if (document.version.isEmpty) { + warnings.add('文档版本为空'); + } + + // 验证模型 + for (final model in document.models.values) { + final modelValidation = validateApiModel(model); + errors.addAll(modelValidation.errors); + warnings.addAll(modelValidation.warnings); + } + + // 验证路径 + for (final path in document.paths.values) { + final pathValidation = validateApiPath(path); + errors.addAll(pathValidation.errors); + warnings.addAll(pathValidation.warnings); + } + + // 验证引用完整性 + final referenceValidation = _validateReferences(document); + errors.addAll(referenceValidation.errors); + warnings.addAll(referenceValidation.warnings); + + return ValidationResult( + isValid: errors.isEmpty, + errors: errors, + warnings: warnings, + ); + } + + /// 验证生成的代码 + static ValidationResult validateGeneratedCode( + String code, + CodeType codeType, + ) { + final errors = []; + final warnings = []; + + // 基础语法检查 + if (code.trim().isEmpty) { + errors.add( + ValidationError( + field: 'code', + message: '生成的代码为空', + severity: ErrorSeverity.error, + ), + ); + } + + // 检查括号匹配 + if (!_isBalancedBrackets(code)) { + errors.add( + ValidationError( + field: 'syntax', + message: '代码中存在不匹配的括号', + severity: ErrorSeverity.error, + ), + ); + } + + // 检查基本Dart语法要素 + switch (codeType) { + case CodeType.model: + if (!code.contains('class ') && !code.contains('enum ')) { + errors.add( + ValidationError( + field: 'content', + message: '模型代码必须包含class或enum声明', + severity: ErrorSeverity.error, + ), + ); + } + break; + case CodeType.endpoints: + if (!code.contains('class ')) { + errors.add( + ValidationError( + field: 'content', + message: '端点代码必须包含class声明', + severity: ErrorSeverity.error, + ), + ); + } + break; + case CodeType.documentation: + if (!code.contains('#')) { + warnings.add('文档代码似乎不包含Markdown标题'); + } + break; + } + + // 检查潜在的命名冲突 + final identifiers = _extractIdentifiers(code); + final duplicates = _findDuplicates(identifiers); + if (duplicates.isNotEmpty) { + warnings.add('检测到可能的命名冲突: ${duplicates.join(', ')}'); + } + + return ValidationResult( + isValid: errors.isEmpty, + errors: errors, + warnings: warnings, + ); + } + + /// 验证枚举 + static ValidationResult _validateEnum(ApiModel model) { + final errors = []; + final warnings = []; + + if (model.enumValues.isEmpty) { + errors.add( + ValidationError( + field: 'enumValues', + message: '枚举类型必须包含至少一个值', + severity: ErrorSeverity.error, + ), + ); + } + + // 验证枚举值的类型一致性 + if (model.enumValues.length > 1) { + final firstType = model.enumValues.first.runtimeType; + for (final value in model.enumValues) { + if (value.runtimeType != firstType) { + warnings.add('枚举值类型不一致'); + break; + } + } + } + + // 验证枚举值的唯一性 + final uniqueValues = model.enumValues.toSet(); + if (uniqueValues.length != model.enumValues.length) { + errors.add( + ValidationError( + field: 'enumValues', + message: '枚举值存在重复', + severity: ErrorSeverity.error, + ), + ); + } + + return ValidationResult( + isValid: errors.isEmpty, + errors: errors, + warnings: warnings, + ); + } + + /// 验证属性集合 + static ValidationResult _validateProperties( + Map properties, + ) { + final errors = []; + final warnings = []; + + for (final entry in properties.entries) { + final propertyValidation = validateApiProperty(entry.key, entry.value); + errors.addAll(propertyValidation.errors); + warnings.addAll(propertyValidation.warnings); + } + + // 检查是否存在保留字冲突 + final reservedWords = _getDartReservedWords(); + for (final propertyName in properties.keys) { + if (reservedWords.contains(propertyName.toLowerCase())) { + warnings.add('属性名 $propertyName 是Dart保留字,可能导致编译错误'); + } + } + + return ValidationResult( + isValid: errors.isEmpty, + errors: errors, + warnings: warnings, + ); + } + + /// 验证参数 + static ValidationResult _validateParameter(ApiParameter parameter) { + final errors = []; + final warnings = []; + + // 验证参数名称 + if (!_isValidDartIdentifier(parameter.name)) { + warnings.add('参数名称不符合Dart命名规范: ${parameter.name}'); + } + + // 验证路径参数必须为required + if (parameter.location == ParameterLocation.path && !parameter.required) { + errors.add( + ValidationError( + field: 'required', + message: '路径参数 ${parameter.name} 必须标记为required', + severity: ErrorSeverity.error, + ), + ); + } + + return ValidationResult( + isValid: errors.isEmpty, + errors: errors, + warnings: warnings, + ); + } + + /// 验证引用完整性 + static ValidationResult _validateReferences(SwaggerDocument document) { + final errors = []; + final warnings = []; + + final modelNames = document.models.keys.toSet(); + + // 检查模型中的引用 + for (final model in document.models.values) { + for (final property in model.properties.values) { + if (property.type == PropertyType.reference && + property.reference != null) { + if (!modelNames.contains(property.reference)) { + errors.add( + ValidationError( + field: 'reference', + message: + '模型 ${model.name} 中的属性 ${property.name} 引用了不存在的类型: ${property.reference}', + severity: ErrorSeverity.error, + ), + ); + } + } + } + } + + return ValidationResult( + isValid: errors.isEmpty, + errors: errors, + warnings: warnings, + ); + } + + /// 检查是否为有效的Dart标识符 + static bool _isValidDartIdentifier(String identifier) { + if (identifier.isEmpty) return false; + + // Dart标识符规则:以字母或下划线开头,后面可以是字母、数字或下划线 + final regex = RegExp(r'^[a-zA-Z_][a-zA-Z0-9_]*$'); + return regex.hasMatch(identifier); + } + + /// 检查是否为有效的属性类型 + static bool _isValidPropertyType(PropertyType type) { + // 所有枚举值都是有效的 + return true; + } + + /// 检查是否为有效的日期格式 + static bool _isValidDateFormat(String dateString) { + try { + DateTime.parse(dateString); + return true; + } catch (e) { + return false; + } + } + + /// 从路径中提取路径参数 + static Set _extractPathParameters(String path) { + final regex = RegExp(r'\{(\w+)\}'); + final matches = regex.allMatches(path); + return matches.map((match) => match.group(1)!).toSet(); + } + + /// 检查括号是否平衡 + static bool _isBalancedBrackets(String code) { + final brackets = {'{': '}', '(': ')', '[': ']'}; + final stack = []; + + for (final char in code.split('')) { + if (brackets.containsKey(char)) { + stack.add(char); + } else if (brackets.containsValue(char)) { + if (stack.isEmpty) return false; + final last = stack.removeLast(); + if (brackets[last] != char) return false; + } + } + + return stack.isEmpty; + } + + /// 提取代码中的标识符 + static List _extractIdentifiers(String code) { + final regex = RegExp(r'\b[a-zA-Z_][a-zA-Z0-9_]*\b'); + final matches = regex.allMatches(code); + return matches.map((match) => match.group(0)!).toList(); + } + + /// 查找重复项 + static List _findDuplicates(List items) { + final seen = {}; + final duplicates = {}; + + for (final item in items) { + if (seen.contains(item)) { + duplicates.add(item); + } else { + seen.add(item); + } + } + + return duplicates.toList(); + } + + /// 获取Dart保留字列表 + static Set _getDartReservedWords() { + return { + 'abstract', + 'as', + 'assert', + 'async', + 'await', + 'break', + 'case', + 'catch', + 'class', + 'const', + 'continue', + 'default', + 'deferred', + 'do', + 'dynamic', + 'else', + 'enum', + 'export', + 'extends', + 'external', + 'factory', + 'false', + 'final', + 'finally', + 'for', + 'function', + 'get', + 'hide', + 'if', + 'implements', + 'import', + 'in', + 'interface', + 'is', + 'library', + 'mixin', + 'new', + 'null', + 'on', + 'operator', + 'part', + 'required', + 'rethrow', + 'return', + 'set', + 'show', + 'static', + 'super', + 'switch', + 'sync', + 'this', + 'throw', + 'true', + 'try', + 'typedef', + 'var', + 'void', + 'while', + 'with', + 'yield', + }; + } +} + +/// 验证结果 +class ValidationResult { + final bool isValid; + final List errors; + final List warnings; + + const ValidationResult({ + required this.isValid, + required this.errors, + required this.warnings, + }); + + /// 是否有错误 + bool get hasErrors => errors.isNotEmpty; + + /// 是否有警告 + bool get hasWarnings => warnings.isNotEmpty; + + /// 获取关键错误 + List get criticalErrors => + errors.where((e) => e.severity == ErrorSeverity.error).toList(); + + /// 生成验证报告 + String generateReport() { + final buffer = StringBuffer(); + + if (isValid) { + buffer.writeln('✅ 验证通过'); + } else { + buffer.writeln('❌ 验证失败'); + } + + if (errors.isNotEmpty) { + buffer.writeln('\n🚨 错误:'); + for (final error in errors) { + buffer.writeln( + '- [${error.severity.name.toUpperCase()}] ${error.field}: ${error.message}', + ); + } + } + + if (warnings.isNotEmpty) { + buffer.writeln('\n⚠️ 警告:'); + for (final warning in warnings) { + buffer.writeln('- $warning'); + } + } + + return buffer.toString(); + } +} + +/// 验证错误 +class ValidationError { + final String field; + final String message; + final ErrorSeverity severity; + + const ValidationError({ + required this.field, + required this.message, + required this.severity, + }); + + @override + String toString() { + return 'ValidationError(field: $field, message: $message, severity: $severity)'; + } +} + +/// 错误严重程度 +enum ErrorSeverity { warning, error, critical } + +/// 代码类型 +enum CodeType { model, endpoints, documentation } diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..3709437 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,637 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f + url: "https://pub.flutter-io.cn" + source: hosted + version: "85.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: de617bfdc64f3d8b00835ec2957441ceca0a29cdf7881f7ab231bc14f71159c0 + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.5.6" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.4" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.2" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.4" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.4" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.4" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792" + url: "https://pub.flutter-io.cn" + source: hosted + version: "9.1.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27" + url: "https://pub.flutter-io.cn" + source: hosted + version: "8.10.1" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.4" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.10.1" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.15.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.6" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.0" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.3" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.2" + http: + dependency: "direct main" + description: + name: http + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.1.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.2" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.9.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.9.5" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + url: "https://pub.flutter-io.cn" + source: hosted + version: "10.0.9" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.9" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.1" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "4f81479fe5194a622cdd1713fe1ecb683a6e6c85cd8cec8e2e35ee5ab3fdf2a1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.6" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.10.13" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.2" + test: + dependency: "direct dev" + description: + name: test + sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.25.15" + test_api: + dependency: transitive + description: + name: test_api + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.4" + test_core: + dependency: transitive + description: + name: test_core + sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.8" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + url: "https://pub.flutter-io.cn" + source: hosted + version: "15.0.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.8.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..a3f5060 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,27 @@ +name: swagger_generator_flutter +description: A Flutter project using generated API models + +version: 1.0.0+1 + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + + # 推荐版本必需的依赖 + json_annotation: ^4.8.1 + http: ^1.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + + # 代码生成必需的依赖 + build_runner: ^2.4.7 + json_serializable: ^6.7.1 + test: ^1.24.0 + +flutter: + uses-material-design: true \ No newline at end of file diff --git a/run_swagger.sh b/run_swagger.sh new file mode 100755 index 0000000..53f0f49 --- /dev/null +++ b/run_swagger.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +# 简化版 Swagger CLI 运行脚本 +# 提供便捷的命令行界面 + +# 颜色定义 +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# 脚本路径 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CLI_DART_FILE="$SCRIPT_DIR/bin/main.dart" + +# 显示帮助 +show_help() { + echo -e "${CYAN}🚀 Swagger CLI 工具${NC}" + echo "" + echo -e "${YELLOW}用法: $0 [命令] [选项]${NC}" + echo "" + echo -e "${GREEN}快速命令:${NC}" + echo -e " $0 all # 生成所有文件" + echo -e " $0 models # 生成数据模型" + echo -e " $0 endpoints # 生成API端点" + echo -e " $0 docs # 生成API文档" + echo -e " $0 api # 生成Retrofit API" + echo "" + echo -e "${GREEN}直接使用:${NC}" + echo -e " dart run bin/main.dart generate --help" + echo "" +} + +# 主函数 +main() { + if [ $# -eq 0 ] || [ "$1" = "help" ] || [ "$1" = "--help" ]; then + show_help + exit 0 + fi + + case "$1" in + all) + dart run "$CLI_DART_FILE" generate --models --api --split-by-tags + ;; + models) + dart run "$CLI_DART_FILE" generate --models + ;; + endpoints) + dart run "$CLI_DART_FILE" generate --endpoints + ;; + docs) + dart run "$CLI_DART_FILE" generate --docs + ;; + api) + dart run "$CLI_DART_FILE" generate --api + ;; + *) + echo -e "${YELLOW}未知命令: $1${NC}" + show_help + exit 1 + ;; + esac +} + +main "$@" \ No newline at end of file diff --git a/test_property_name.dart b/test_property_name.dart new file mode 100644 index 0000000..3af205a --- /dev/null +++ b/test_property_name.dart @@ -0,0 +1,20 @@ +import 'lib/utils/string_utils.dart'; + +void main() { + // 测试属性名转换 + print('Testing property name conversion:'); + print('classCadreId -> ${StringUtils.toDartPropertyName('classCadreId')}'); + print('meetingTitle -> ${StringUtils.toDartPropertyName('meetingTitle')}'); + print('taskInfo -> ${StringUtils.toDartPropertyName('taskInfo')}'); + print( + 'sunTaskUserResults -> ${StringUtils.toDartPropertyName('sunTaskUserResults')}'); + print( + 'sunTaskFileResults -> ${StringUtils.toDartPropertyName('sunTaskFileResults')}'); + + // 测试下划线格式 + print('\nTesting snake_case conversion:'); + print( + 'class_cadre_id -> ${StringUtils.toDartPropertyName('class_cadre_id')}'); + print('meeting_title -> ${StringUtils.toDartPropertyName('meeting_title')}'); + print('task_info -> ${StringUtils.toDartPropertyName('task_info')}'); +} diff --git a/tests/models_test.dart b/tests/models_test.dart new file mode 100644 index 0000000..edc9cfe --- /dev/null +++ b/tests/models_test.dart @@ -0,0 +1,923 @@ +import 'package:test/test.dart'; + +import '../lib/core/models.dart'; + +void main() { + group('ApiPath', () { + test('creates ApiPath with required fields', () { + final path = ApiPath( + path: '/api/users', + method: HttpMethod.get, + summary: 'Get users', + description: 'Retrieve all users', + operationId: 'getUsers', + tags: ['User'], + parameters: [], + responses: {}, + requestBody: null, + ); + + expect(path.path, '/api/users'); + expect(path.method, HttpMethod.get); + expect(path.summary, 'Get users'); + expect(path.description, 'Retrieve all users'); + expect(path.tags, ['User']); + expect(path.parameters, isEmpty); + expect(path.responses, isEmpty); + expect(path.requestBody, isNull); + }); + + test('creates ApiPath with all fields', () { + final parameters = [ + ApiParameter( + name: 'id', + location: ParameterLocation.path, + required: true, + type: PropertyType.integer, + description: 'User ID', + ), + ]; + + final responses = { + '200': ApiResponse( + code: '200', + description: 'Success', + schema: {'type': 'object'}, + content: null, + ), + }; + + final requestBody = ApiRequestBody( + description: 'User data', + required: true, + content: { + 'application/json': { + 'schema': {'type': 'object'} + } + }, + ); + + final path = ApiPath( + path: '/api/users/{id}', + method: HttpMethod.put, + summary: 'Update user', + description: 'Update user by ID', + operationId: 'updateUser', + tags: ['User'], + parameters: parameters, + responses: responses, + requestBody: requestBody, + ); + + expect(path.parameters, hasLength(1)); + expect(path.responses, hasLength(1)); + expect(path.requestBody, isNotNull); + }); + }); + + group('ApiParameter', () { + test('creates ApiParameter with required fields', () { + final param = ApiParameter( + name: 'id', + location: ParameterLocation.path, + required: true, + type: PropertyType.integer, + description: 'User ID', + ); + + expect(param.name, 'id'); + expect(param.location, ParameterLocation.path); + expect(param.required, true); + expect(param.type, PropertyType.integer); + expect(param.description, 'User ID'); + }); + + test('creates ApiParameter with optional fields', () { + final param = ApiParameter( + name: 'page', + location: ParameterLocation.query, + required: false, + type: PropertyType.integer, + description: 'Page number', + format: 'int32', + example: 1, + defaultValue: 1, + ); + + expect(param.format, 'int32'); + expect(param.example, 1); + expect(param.defaultValue, 1); + }); + + test('creates ApiParameter from JSON', () { + final json = { + 'name': 'id', + 'in': 'path', + 'required': true, + 'type': 'integer', + 'description': 'User ID', + 'format': 'int64', + 'example': 123, + 'default': 1, + }; + + final param = ApiParameter.fromJson(json); + + expect(param.name, 'id'); + expect(param.location, ParameterLocation.path); + expect(param.required, true); + expect(param.type, PropertyType.integer); + expect(param.description, 'User ID'); + expect(param.format, 'int64'); + expect(param.example, 123); + expect(param.defaultValue, 1); + }); + }); + + group('ApiResponse', () { + test('creates ApiResponse with required fields', () { + final response = ApiResponse( + code: '200', + description: 'Success', + schema: null, + content: null, + ); + + expect(response.code, '200'); + expect(response.description, 'Success'); + expect(response.schema, isNull); + expect(response.content, isNull); + }); + + test('creates ApiResponse with schema', () { + final schema = { + 'type': 'object', + 'properties': { + 'id': {'type': 'integer'}, + 'name': {'type': 'string'}, + }, + }; + + final response = ApiResponse( + code: '200', + description: 'Success', + schema: schema, + content: null, + ); + + expect(response.schema, equals(schema)); + }); + + test('creates ApiResponse from JSON', () { + final json = { + 'description': 'Success', + 'schema': {'type': 'object'}, + 'content': { + 'application/json': { + 'schema': {'type': 'object'}, + }, + }, + }; + + final response = ApiResponse.fromJson('200', json); + + expect(response.code, '200'); + expect(response.description, 'Success'); + expect(response.schema, isNotNull); + expect(response.content, isNotNull); + }); + }); + + group('ApiRequestBody', () { + test('creates ApiRequestBody with required fields', () { + final requestBody = ApiRequestBody( + description: 'User data', + required: true, + content: null, + ); + + expect(requestBody.description, 'User data'); + expect(requestBody.required, true); + expect(requestBody.content, isNull); + }); + + test('creates ApiRequestBody with content', () { + final content = { + 'application/json': { + 'schema': { + 'type': 'object', + 'properties': { + 'name': {'type': 'string'}, + 'email': {'type': 'string'}, + }, + }, + }, + }; + + final requestBody = ApiRequestBody( + description: 'User data', + required: true, + content: content, + ); + + expect(requestBody.content, equals(content)); + }); + + test('creates ApiRequestBody from JSON', () { + final json = { + 'description': 'User data', + 'required': true, + 'content': { + 'application/json': { + 'schema': {'type': 'object'}, + }, + }, + }; + + final requestBody = ApiRequestBody.fromJson(json); + + expect(requestBody.description, 'User data'); + expect(requestBody.required, true); + expect(requestBody.content, isNotNull); + }); + }); + + group('ApiModel', () { + test('creates ApiModel with required fields', () { + final model = ApiModel( + name: 'User', + description: 'User model', + properties: {}, + required: [], + ); + + expect(model.name, 'User'); + expect(model.description, 'User model'); + expect(model.properties, isEmpty); + expect(model.required, isEmpty); + expect(model.isEnum, false); + expect(model.enumValues, isEmpty); + expect(model.enumType, isNull); + }); + + test('creates ApiModel with properties', () { + final properties = { + 'id': ApiProperty( + name: 'id', + type: PropertyType.integer, + description: 'User ID', + required: true, + ), + 'name': ApiProperty( + name: 'name', + type: PropertyType.string, + description: 'User name', + required: true, + ), + }; + + final model = ApiModel( + name: 'User', + description: 'User model', + properties: properties, + required: ['id', 'name'], + ); + + expect(model.properties, hasLength(2)); + expect(model.required, ['id', 'name']); + }); + + test('creates enum ApiModel', () { + final model = ApiModel( + name: 'UserStatus', + description: 'User status enum', + properties: {}, + required: [], + isEnum: true, + enumValues: ['active', 'inactive', 'pending'], + enumType: PropertyType.string, + ); + + expect(model.isEnum, true); + expect(model.enumValues, ['active', 'inactive', 'pending']); + expect(model.enumType, PropertyType.string); + }); + + test('creates ApiModel from JSON', () { + final json = { + 'description': 'User model', + 'properties': { + 'id': { + 'type': 'integer', + 'description': 'User ID', + }, + 'name': { + 'type': 'string', + 'description': 'User name', + }, + }, + 'required': ['id', 'name'], + }; + + final model = ApiModel.fromJson('User', json); + + expect(model.name, 'User'); + expect(model.description, 'User model'); + expect(model.properties, hasLength(2)); + expect(model.required, ['id', 'name']); + }); + + test('creates enum ApiModel from JSON', () { + final json = { + 'description': 'User status enum', + 'enum': ['active', 'inactive', 'pending'], + 'type': 'string', + }; + + final model = ApiModel.fromJson('UserStatus', json); + + expect(model.isEnum, true); + expect(model.enumValues, ['active', 'inactive', 'pending']); + expect(model.enumType, PropertyType.string); + }); + }); + + group('ApiProperty', () { + test('creates ApiProperty with required fields', () { + final property = ApiProperty( + name: 'id', + type: PropertyType.integer, + description: 'User ID', + required: true, + ); + + expect(property.name, 'id'); + expect(property.type, PropertyType.integer); + expect(property.description, 'User ID'); + expect(property.required, true); + expect(property.nullable, false); + }); + + test('creates ApiProperty with optional fields', () { + final property = ApiProperty( + name: 'name', + type: PropertyType.string, + description: 'User name', + required: false, + nullable: true, + format: 'string', + example: 'John Doe', + defaultValue: 'Unknown', + reference: 'User', + ); + + expect(property.nullable, true); + expect(property.format, 'string'); + expect(property.example, 'John Doe'); + expect(property.defaultValue, 'Unknown'); + expect(property.reference, 'User'); + }); + + test('creates ApiProperty from JSON', () { + final json = { + 'type': 'string', + 'description': 'User name', + 'nullable': true, + 'format': 'string', + 'example': 'John Doe', + 'default': 'Unknown', + }; + + final property = ApiProperty.fromJson('name', json, []); + + expect(property.name, 'name'); + expect(property.type, PropertyType.string); + expect(property.description, 'User name'); + expect(property.nullable, true); + expect(property.format, 'string'); + expect(property.example, 'John Doe'); + expect(property.defaultValue, 'Unknown'); + }); + + test('creates ApiProperty with reference', () { + final json = { + 'type': 'object', + 'description': 'User object', + '\$ref': '#/components/schemas/User', + }; + + final property = ApiProperty.fromJson('user', json, []); + + expect(property.type, PropertyType.reference); + expect(property.reference, 'User'); + }); + + test('creates ApiProperty with array items', () { + final json = { + 'type': 'array', + 'description': 'User list', + 'items': { + 'type': 'object', + '\$ref': '#/components/schemas/User', + }, + }; + + final property = ApiProperty.fromJson('users', json, []); + + expect(property.type, PropertyType.array); + expect(property.items, isNotNull); + expect(property.items!.name, 'User'); + }); + }); + + group('PropertyType', () { + test('converts string to PropertyType', () { + expect(PropertyType.fromString('string'), PropertyType.string); + expect(PropertyType.fromString('integer'), PropertyType.integer); + expect(PropertyType.fromString('number'), PropertyType.number); + expect(PropertyType.fromString('boolean'), PropertyType.boolean); + expect(PropertyType.fromString('array'), PropertyType.array); + expect(PropertyType.fromString('object'), PropertyType.object); + expect(PropertyType.fromString('unknown'), PropertyType.string); + }); + + test('gets PropertyType value', () { + expect(PropertyType.string.value, 'string'); + expect(PropertyType.integer.value, 'integer'); + expect(PropertyType.number.value, 'number'); + expect(PropertyType.boolean.value, 'boolean'); + expect(PropertyType.array.value, 'array'); + expect(PropertyType.object.value, 'object'); + expect(PropertyType.reference.value, 'reference'); + }); + }); + + group('ParameterLocation', () { + test('converts string to ParameterLocation', () { + expect(ParameterLocation.fromString('query'), ParameterLocation.query); + expect(ParameterLocation.fromString('path'), ParameterLocation.path); + expect(ParameterLocation.fromString('header'), ParameterLocation.header); + expect(ParameterLocation.fromString('cookie'), ParameterLocation.cookie); + expect(ParameterLocation.fromString('unknown'), ParameterLocation.query); + }); + }); + + group('HttpMethod', () { + test('converts string to HttpMethod', () { + expect(HttpMethod.fromString('GET'), HttpMethod.get); + expect(HttpMethod.fromString('POST'), HttpMethod.post); + expect(HttpMethod.fromString('PUT'), HttpMethod.put); + expect(HttpMethod.fromString('DELETE'), HttpMethod.delete); + expect(HttpMethod.fromString('PATCH'), HttpMethod.patch); + expect(HttpMethod.fromString('HEAD'), HttpMethod.head); + expect(HttpMethod.fromString('OPTIONS'), HttpMethod.options); + }); + + test('handles case insensitive input', () { + expect(HttpMethod.fromString('get'), HttpMethod.get); + expect(HttpMethod.fromString('Post'), HttpMethod.post); + expect(HttpMethod.fromString('put'), HttpMethod.put); + }); + + test('returns default for unknown method', () { + expect(HttpMethod.fromString('UNKNOWN'), HttpMethod.get); + expect(HttpMethod.fromString(''), HttpMethod.get); + }); + + test('gets HttpMethod value', () { + expect(HttpMethod.get.value, 'GET'); + expect(HttpMethod.post.value, 'POST'); + expect(HttpMethod.put.value, 'PUT'); + expect(HttpMethod.delete.value, 'DELETE'); + expect(HttpMethod.patch.value, 'PATCH'); + expect(HttpMethod.head.value, 'HEAD'); + expect(HttpMethod.options.value, 'OPTIONS'); + }); + }); + + group('SwaggerDocument', () { + test('creates SwaggerDocument with required fields', () { + final document = SwaggerDocument( + title: 'Test API', + version: '1.0.0', + description: 'Test API description', + host: 'api.example.com', + basePath: '/api', + schemes: ['https'], + consumes: ['application/json'], + produces: ['application/json'], + paths: {}, + models: {}, + controllers: {}, + ); + + expect(document.title, 'Test API'); + expect(document.version, '1.0.0'); + expect(document.description, 'Test API description'); + expect(document.host, 'api.example.com'); + expect(document.basePath, '/api'); + expect(document.schemes, ['https']); + expect(document.consumes, ['application/json']); + expect(document.produces, ['application/json']); + expect(document.paths, isEmpty); + expect(document.models, isEmpty); + expect(document.controllers, isEmpty); + }); + + test('creates SwaggerDocument from JSON with all fields', () { + final json = { + 'info': { + 'title': 'Test API', + 'version': '1.0.0', + 'description': 'Test API description', + }, + 'host': 'api.example.com', + 'basePath': '/api', + 'schemes': ['https', 'http'], + 'consumes': ['application/json', 'application/xml'], + 'produces': ['application/json', 'text/plain'], + }; + + final document = SwaggerDocument.fromJson(json); + + expect(document.title, 'Test API'); + expect(document.version, '1.0.0'); + expect(document.description, 'Test API description'); + expect(document.host, 'api.example.com'); + expect(document.basePath, '/api'); + expect(document.schemes, ['https', 'http']); + expect(document.consumes, ['application/json', 'application/xml']); + expect(document.produces, ['application/json', 'text/plain']); + }); + + test('creates SwaggerDocument from JSON with minimal fields', () { + final json = {}; + + final document = SwaggerDocument.fromJson(json); + + expect(document.title, 'API'); + expect(document.version, '1.0.0'); + expect(document.description, ''); + expect(document.host, ''); + expect(document.basePath, ''); + expect(document.schemes, ['https']); + expect(document.consumes, ['application/json']); + expect(document.produces, ['application/json']); + }); + + test('creates SwaggerDocument from JSON with null info', () { + final json = {'info': null}; + + final document = SwaggerDocument.fromJson(json); + + expect(document.title, 'API'); + expect(document.version, '1.0.0'); + expect(document.description, ''); + }); + }); + + group('ApiController', () { + test('creates ApiController with required fields', () { + final controller = ApiController( + name: 'UserController', + description: 'User management controller', + paths: [], + ); + + expect(controller.name, 'UserController'); + expect(controller.description, 'User management controller'); + expect(controller.paths, isEmpty); + }); + + test('creates ApiController with paths', () { + final paths = [ + ApiPath( + path: '/api/users', + method: HttpMethod.get, + summary: 'Get users', + description: 'Retrieve all users', + operationId: 'getUsers', + tags: ['User'], + parameters: [], + responses: {}, + requestBody: null, + ), + ApiPath( + path: '/api/users/{id}', + method: HttpMethod.post, + summary: 'Create user', + description: 'Create a new user', + operationId: 'createUser', + tags: ['User'], + parameters: [], + responses: {}, + requestBody: null, + ), + ]; + + final controller = ApiController( + name: 'UserController', + description: 'User management controller', + paths: paths, + ); + + expect(controller.paths, hasLength(2)); + expect(controller.paths[0].path, '/api/users'); + expect(controller.paths[1].path, '/api/users/{id}'); + }); + + test('creates ApiController from paths', () { + final paths = [ + ApiPath( + path: '/api/users', + method: HttpMethod.get, + summary: 'Get users', + description: 'Retrieve all users', + operationId: 'getUsers', + tags: ['User'], + parameters: [], + responses: {}, + requestBody: null, + ), + ]; + + final controller = ApiController.fromPaths('UserController', paths); + + expect(controller.name, 'UserController'); + expect(controller.description, 'UserController'); + expect(controller.paths, hasLength(1)); + }); + }); + + group('ApiPath fromJson', () { + test('creates ApiPath from JSON with all fields', () { + final json = { + 'summary': 'Get users', + 'description': 'Retrieve all users', + 'operationId': 'getUsers', + 'tags': ['User'], + 'parameters': [ + { + 'name': 'id', + 'in': 'path', + 'required': true, + 'type': 'integer', + 'description': 'User ID', + } + ], + 'responses': { + '200': { + 'description': 'Success', + 'schema': {'type': 'object'}, + } + }, + 'requestBody': { + 'description': 'User data', + 'required': true, + 'content': { + 'application/json': { + 'schema': {'type': 'object'}, + }, + }, + }, + 'deprecated': true, + }; + + final path = ApiPath.fromJson('/api/users/{id}', 'PUT', json); + + expect(path.path, '/api/users/{id}'); + expect(path.method, HttpMethod.put); + expect(path.summary, 'Get users'); + expect(path.description, 'Retrieve all users'); + expect(path.operationId, 'getUsers'); + expect(path.tags, ['User']); + expect(path.parameters, hasLength(1)); + expect(path.responses, hasLength(1)); + expect(path.requestBody, isNotNull); + expect(path.deprecated, true); + }); + + test('creates ApiPath from JSON with minimal fields', () { + final json = {}; + + final path = ApiPath.fromJson('/api/users', 'GET', json); + + expect(path.path, '/api/users'); + expect(path.method, HttpMethod.get); + expect(path.summary, ''); + expect(path.description, ''); + expect(path.operationId, ''); + expect(path.tags, isEmpty); + expect(path.parameters, isEmpty); + expect(path.responses, isEmpty); + expect(path.requestBody, isNull); + expect(path.deprecated, false); + }); + }); + + group('ApiModel fromJson edge cases', () { + test('creates ApiModel with nullable properties', () { + final json = { + 'description': 'User model', + 'properties': { + 'id': { + 'type': 'integer', + 'description': 'User ID', + 'nullable': true, + }, + 'name': { + 'type': 'string', + 'description': 'User name', + 'nullable': false, + }, + }, + }; + + final model = ApiModel.fromJson('User', json); + + expect(model.properties['id']!.nullable, true); + expect(model.properties['id']!.required, false); + expect(model.properties['name']!.nullable, false); + expect(model.properties['name']!.required, true); + expect(model.required, ['name']); + }); + + test('creates ApiModel with explicit required fields', () { + final json = { + 'description': 'User model', + 'properties': { + 'id': { + 'type': 'integer', + 'description': 'User ID', + 'nullable': true, + }, + 'name': { + 'type': 'string', + 'description': 'User name', + 'nullable': false, + }, + }, + 'required': ['id', 'name'], + }; + + final model = ApiModel.fromJson('User', json); + + expect(model.properties['id']!.required, true); + expect(model.properties['name']!.required, true); + expect(model.required, ['id', 'name']); + }); + }); + + group('ApiProperty complex types', () { + test('creates ApiProperty with nested object', () { + final json = { + 'type': 'object', + 'description': 'User address', + 'properties': { + 'street': {'type': 'string'}, + 'city': {'type': 'string'}, + }, + }; + + final property = ApiProperty.fromJson('address', json, []); + + expect(property.type, PropertyType.object); + expect(property.description, 'User address'); + expect(property.required, false); + }); + + test('creates ApiProperty with array of primitives', () { + final json = { + 'type': 'array', + 'description': 'User tags', + 'items': { + 'type': 'string', + }, + }; + + final property = ApiProperty.fromJson('tags', json, []); + + expect(property.type, PropertyType.array); + expect(property.description, 'User tags'); + expect(property.items, isNotNull); + expect(property.items!.name, 'string'); + }); + + test('creates ApiProperty with array of objects', () { + final json = { + 'type': 'array', + 'description': 'User list', + 'items': { + 'type': 'object', + '\$ref': '#/components/schemas/User', + }, + }; + + final property = ApiProperty.fromJson('users', json, []); + + expect(property.type, PropertyType.array); + expect(property.description, 'User list'); + expect(property.items, isNotNull); + expect(property.items!.name, 'User'); + }); + + test('creates ApiProperty with file type', () { + final json = { + 'type': 'file', + 'description': 'User avatar', + 'format': 'binary', + }; + + final property = ApiProperty.fromJson('avatar', json, []); + + expect(property.type, PropertyType.file); + expect(property.description, 'User avatar'); + expect(property.format, 'binary'); + }); + + test('creates ApiProperty with date type', () { + final json = { + 'type': 'string', + 'format': 'date', + 'description': 'User birth date', + }; + + final property = ApiProperty.fromJson('birthDate', json, []); + + expect(property.type, PropertyType.string); + expect(property.format, 'date'); + expect(property.description, 'User birth date'); + }); + + test('creates ApiProperty with date-time type', () { + final json = { + 'type': 'string', + 'format': 'date-time', + 'description': 'User created at', + }; + + final property = ApiProperty.fromJson('createdAt', json, []); + + expect(property.type, PropertyType.string); + expect(property.format, 'date-time'); + expect(property.description, 'User created at'); + }); + }); + + group('Error handling and edge cases', () { + test('ApiParameter fromJson handles missing fields gracefully', () { + final json = {}; + + final param = ApiParameter.fromJson(json); + + expect(param.name, ''); + expect(param.location, ParameterLocation.query); + expect(param.required, false); + expect(param.type, PropertyType.string); + expect(param.description, ''); + expect(param.format, isNull); + expect(param.example, isNull); + expect(param.defaultValue, isNull); + }); + + test('ApiResponse fromJson handles missing fields gracefully', () { + final json = {}; + + final response = ApiResponse.fromJson('200', json); + + expect(response.code, '200'); + expect(response.description, ''); + expect(response.schema, isNull); + expect(response.content, isNull); + }); + + test('ApiRequestBody fromJson handles missing fields gracefully', () { + final json = {}; + + final requestBody = ApiRequestBody.fromJson(json); + + expect(requestBody.description, ''); + expect(requestBody.required, false); + expect(requestBody.content, isNull); + }); + + test('PropertyType fromString handles unknown types', () { + expect(PropertyType.fromString('unknown'), PropertyType.string); + expect(PropertyType.fromString(''), PropertyType.string); + expect(PropertyType.fromString('CUSTOM_TYPE'), PropertyType.string); + }); + + test('ParameterLocation fromString handles unknown locations', () { + expect(ParameterLocation.fromString('unknown'), ParameterLocation.query); + expect(ParameterLocation.fromString(''), ParameterLocation.query); + expect(ParameterLocation.fromString('CUSTOM_LOCATION'), + ParameterLocation.query); + }); + + test('HttpMethod fromString handles unknown methods', () { + expect(HttpMethod.fromString('unknown'), HttpMethod.get); + expect(HttpMethod.fromString(''), HttpMethod.get); + expect(HttpMethod.fromString('CUSTOM_METHOD'), HttpMethod.get); + }); + }); +} diff --git a/tests/string_utils_test.dart b/tests/string_utils_test.dart new file mode 100644 index 0000000..1cd5957 --- /dev/null +++ b/tests/string_utils_test.dart @@ -0,0 +1,204 @@ +import 'package:test/test.dart'; + +import '../lib/utils/string_utils.dart'; + +void main() { + group('StringUtils', () { + group('toDartPropertyName', () { + test('converts snake_case to camelCase', () { + expect(StringUtils.toDartPropertyName('user_id'), 'userId'); + expect(StringUtils.toDartPropertyName('user-id'), 'userId'); + expect(StringUtils.toDartPropertyName('1st_field'), 'n1stField'); + expect(StringUtils.toDartPropertyName(''), 'property'); + }); + + test('handles special characters', () { + expect(StringUtils.toDartPropertyName('field@name'), 'fieldName'); + expect(StringUtils.toDartPropertyName('with space'), 'withSpace'); + expect(StringUtils.toDartPropertyName('with.dot'), 'withDot'); + expect(StringUtils.toDartPropertyName('with/slash'), 'withSlash'); + }); + + test('handles numbers at start', () { + expect(StringUtils.toDartPropertyName('1field'), 'n1field'); + expect(StringUtils.toDartPropertyName('123test'), 'n123test'); + }); + }); + + group('toCamelCase', () { + test('converts snake_case to camelCase', () { + expect(StringUtils.toCamelCase('user_id'), 'userId'); + expect(StringUtils.toCamelCase('first_name'), 'firstName'); + expect(StringUtils.toCamelCase('api_version'), 'apiVersion'); + }); + + test('handles single word', () { + expect(StringUtils.toCamelCase('user'), 'user'); + expect(StringUtils.toCamelCase(''), ''); + }); + + test('handles multiple underscores', () { + expect(StringUtils.toCamelCase('user__id'), 'userId'); + expect(StringUtils.toCamelCase('_user_id_'), 'userId'); + }); + }); + + group('toPascalCase', () { + test('converts snake_case to PascalCase', () { + expect(StringUtils.toPascalCase('user_id'), 'UserId'); + expect(StringUtils.toPascalCase('first_name'), 'FirstName'); + expect(StringUtils.toPascalCase('api_version'), 'ApiVersion'); + }); + + test('handles already PascalCase', () { + expect(StringUtils.toPascalCase('User'), 'User'); + expect(StringUtils.toPascalCase('UserID'), 'UserID'); + }); + + test('handles camelCase input', () { + expect(StringUtils.toPascalCase('userId'), 'UserId'); + expect(StringUtils.toPascalCase('firstName'), 'FirstName'); + }); + + test('handles empty and single character', () { + expect(StringUtils.toPascalCase(''), ''); + expect(StringUtils.toPascalCase('a'), 'A'); + }); + }); + + group('toSnakeCase', () { + test('converts camelCase to snake_case', () { + expect(StringUtils.toSnakeCase('userId'), 'user_id'); + expect(StringUtils.toSnakeCase('firstName'), 'first_name'); + expect(StringUtils.toSnakeCase('apiVersion'), 'api_version'); + }); + + test('converts PascalCase to snake_case', () { + expect(StringUtils.toSnakeCase('UserID'), 'user_id'); + expect(StringUtils.toSnakeCase('FirstName'), 'first_name'); + }); + + test('handles acronyms', () { + expect(StringUtils.toSnakeCase('API'), 'api'); + expect(StringUtils.toSnakeCase('URL'), 'url'); + }); + + test('handles empty and single character', () { + expect(StringUtils.toSnakeCase(''), ''); + expect(StringUtils.toSnakeCase('a'), 'a'); + }); + }); + + group('generateClassName', () { + test('generates valid class names', () { + expect(StringUtils.generateClassName('user'), 'User'); + expect(StringUtils.generateClassName('user_id'), 'UserId'); + expect(StringUtils.generateClassName('api-version'), 'ApiVersion'); + }); + + test('handles special characters', () { + expect(StringUtils.generateClassName('user@name'), 'UserName'); + expect(StringUtils.generateClassName('user.name'), 'UserName'); + }); + }); + + group('generateFileName', () { + test('generates valid file names', () { + expect(StringUtils.generateFileName('User'), 'user.dart'); + expect(StringUtils.generateFileName('UserID'), 'user_id.dart'); + expect(StringUtils.generateFileName('ApiVersion'), 'api_version.dart'); + }); + }); + + group('isValidDartIdentifier', () { + test('valid identifiers', () { + expect(StringUtils.isValidDartIdentifier('user'), true); + expect(StringUtils.isValidDartIdentifier('user_id'), true); + expect(StringUtils.isValidDartIdentifier('_private'), true); + expect(StringUtils.isValidDartIdentifier('user123'), true); + }); + + test('invalid identifiers', () { + expect(StringUtils.isValidDartIdentifier(''), false); + expect(StringUtils.isValidDartIdentifier('123user'), false); + expect(StringUtils.isValidDartIdentifier('user-name'), false); + expect(StringUtils.isValidDartIdentifier('user@name'), false); + }); + }); + + group('generateEnumValueName', () { + test('generates valid enum names from strings', () { + expect(StringUtils.generateEnumValueName('active', 0), 'active'); + expect( + StringUtils.generateEnumValueName('user_status', 1), 'userStatus'); + }); + + test('handles invalid strings', () { + expect(StringUtils.generateEnumValueName('', 0), 'value1'); + expect(StringUtils.generateEnumValueName('123', 1), 'value2'); + }); + + test('handles non-string values', () { + expect(StringUtils.generateEnumValueName(123, 0), 'value1'); + expect(StringUtils.generateEnumValueName('', 1), 'value2'); + }); + }); + + group('cleanDescription', () { + test('cleans basic descriptions', () { + expect(StringUtils.cleanDescription(' test description '), + 'test description'); + expect(StringUtils.cleanDescription('line1\nline2'), 'line1 line2'); + }); + + test('removes special characters', () { + expect(StringUtils.cleanDescription('test@#\$%'), 'test'); + expect(StringUtils.cleanDescription('test[description]'), + 'testdescription'); + }); + + test('truncates long descriptions', () { + final longDesc = 'a' * 300; + final result = StringUtils.cleanDescription(longDesc); + expect(result.length, lessThanOrEqualTo(203)); // 200 + '...' + expect(result.endsWith('...'), true); + }); + + test('handles empty and null', () { + expect(StringUtils.cleanDescription(''), ''); + }); + }); + + group('generateComment', () { + test('generates single line comments', () { + expect(StringUtils.generateComment('test'), '/// test'); + expect(StringUtils.generateComment(''), ''); + }); + + test('generates block comments', () { + final result = StringUtils.generateComment('test', isBlock: true); + expect(result, contains('/**')); + expect(result, contains('test')); + expect(result, contains('*/')); + }); + }); + + group('formatBytes', () { + test('formats bytes correctly', () { + expect(StringUtils.formatBytes(1023), '1023B'); + expect(StringUtils.formatBytes(1024), '1.0KB'); + expect(StringUtils.formatBytes(1024 * 1024), '1.0MB'); + expect(StringUtils.formatBytes(1024 * 1024 * 1024), '1.0GB'); + }); + }); + + group('formatDuration', () { + test('formats duration correctly', () { + expect( + StringUtils.formatDuration(Duration(milliseconds: 500)), '500毫秒'); + expect(StringUtils.formatDuration(Duration(seconds: 1)), '1.00秒'); + expect(StringUtils.formatDuration(Duration(seconds: 2)), '2.00秒'); + }); + }); + }); +}