diff --git a/DEPENDENCY_UPDATE.md b/DEPENDENCY_UPDATE.md new file mode 100644 index 0000000..fad6bc3 --- /dev/null +++ b/DEPENDENCY_UPDATE.md @@ -0,0 +1,159 @@ +# 依赖包版本更新说明 + +## 更新日期 +2025-11-21 + +## 更新内容 + +### 主项目 (pubspec.yaml) + +#### 更新前 +```yaml +dependencies: + json_annotation: ^4.8.1 + freezed_annotation: ^2.4.1 + +dev_dependencies: + build_runner: ^2.4.7 + json_serializable: ^6.7.1 + retrofit_generator: ^8.0.0 + freezed: ^2.4.7 +``` + +#### 更新后 +```yaml +dependencies: + json_annotation: ^4.9.0 + freezed_annotation: ^3.1.0 + +dev_dependencies: + build_runner: ^2.10.4 + json_serializable: ^6.11.2 + retrofit_generator: ^10.2.0 + freezed: ^3.2.3 +``` + +### Example 项目 (example/pubspec.yaml) + +#### 更新前 +```yaml +dependencies: + json_annotation: ^4.9.0 + freezed_annotation: ^2.4.1 + +dev_dependencies: + build_runner: ^2.4.7 + retrofit_generator: ^9.0.0 + json_serializable: ^6.7.1 + freezed: ^2.4.7 +``` + +#### 更新后 +```yaml +dependencies: + json_annotation: ^4.9.0 + freezed_annotation: ^3.1.0 + +dev_dependencies: + build_runner: ^2.10.4 + retrofit_generator: ^10.2.0 + json_serializable: ^6.11.2 + freezed: ^3.2.3 +``` + +## 版本说明 + +### build_runner +- **旧版本**: 2.4.7 +- **新版本**: 2.10.4 +- **发布时间**: 2天前 (2025-11-19) +- **说明**: 最新稳定版本,修复了多个已知问题 + +### json_serializable +- **旧版本**: 6.7.1 +- **新版本**: 6.11.2 +- **说明**: 使用 6.11.2 而非 6.11.3,因为 6.11.3 与 freezed 3.2.3 存在 analyzer 版本冲突 + +### retrofit_generator +- **旧版本**: 8.0.0 / 9.0.0 +- **新版本**: 10.2.0 +- **发布时间**: 33小时前 (2025-11-20) +- **说明**: 最新版本,支持更多特性 + +### freezed +- **旧版本**: 2.4.7 +- **新版本**: 3.2.3 +- **发布时间**: 2个月前 (2025-09-10) +- **重要变更**: + - 需要 freezed_annotation 3.1.0+ + - 支持 Dart 3 的原生模式匹配 + - 建议使用 `switch` 表达式替代 `when`/`map` 方法 + +### freezed_annotation +- **旧版本**: 2.4.1 / 2.4.4 +- **新版本**: 3.1.0 +- **说明**: 必须升级到 3.x 以配合 freezed 3.x + +### json_annotation +- **旧版本**: 4.8.1 +- **新版本**: 4.9.0 +- **说明**: 保持 4.x 版本以确保兼容性 + +## 版本兼容性矩阵 + +| 包名 | 版本 | analyzer 依赖 | 兼容性 | +|------|------|--------------|--------| +| freezed 3.2.3 | ✅ | 7.5.9 - 9.0.0 | ✅ | +| json_serializable 6.11.2 | ✅ | 8.x | ✅ | +| json_serializable 6.11.3 | ❌ | 9.0.0+ | ❌ 与 freezed 冲突 | +| build_runner 2.10.4 | ✅ | - | ✅ | +| retrofit_generator 10.2.0 | ✅ | - | ✅ | + +## 解决的问题 + +1. **build_runner 缓存损坏**: 通过升级到最新版本解决 +2. **版本冲突**: + - freezed 3.x 需要 freezed_annotation 3.x + - freezed 3.2.3 与 json_serializable 6.11.3 存在 analyzer 版本冲突 + - 使用 json_serializable 6.11.2 解决冲突 + +## 清理步骤 + +如果遇到问题,执行以下清理步骤: + +```bash +# 1. 清理缓存 +rm -rf .dart_tool +rm -rf pubspec.lock + +# 2. 重新获取依赖 +flutter pub get + +# 3. 清理构建文件 +flutter clean + +# 4. 重新运行 build_runner +flutter pub run build_runner build --delete-conflicting-outputs +``` + +## 注意事项 + +1. **Freezed 3.x 迁移**: + - 建议使用 Dart 3 的原生 `switch` 表达式 + - 旧的 `when`/`map` 方法仍然可用,但已标记为遗留功能 + +2. **analyzer 版本**: + - 当前配置使用 analyzer 8.x + - 避免使用需要 analyzer 9.x 的包版本 + +3. **pub.dev 镜像**: + - 如果使用国内镜像 (pub.flutter-io.cn),可能需要等待同步 + - 建议使用官方源或清华镜像 + +## 参考链接 + +- [build_runner](https://pub.dev/packages/build_runner) +- [json_serializable](https://pub.dev/packages/json_serializable) +- [retrofit_generator](https://pub.dev/packages/retrofit_generator) +- [freezed](https://pub.dev/packages/freezed) + diff --git a/README.md b/README.md index 1676756..8e57325 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,9 @@ ### 🎯 专为 Flutter 优化 - **Dio + Retrofit 集成**:完美适配主流网络架构 +- **现代数据模型 (Freezed)**:生成基于 Freezed 的不可变数据模型,自动获得 `copyWith`、`toString`、`==/hashCode` 等功能。 - **类型安全**:生成强类型的 API 接口和模型 -- **JSON 序列化**:自动生成 json_serializable 代码 -- **String 默认值**:自动为 String 类型字段添加默认值,提升容错性 +- **JSON 序列化**:与 `json_serializable` 无缝集成,自动生成序列化代码 - **文件上传支持**:完整的 multipart/form-data 支持 ### 🔧 高级特性 @@ -54,8 +54,13 @@ ### 📦 作为 dev_dependencies 使用(推荐) -1) 在宿主项目 `pubspec.yaml` 添加依赖 +1) 在宿主项目 `pubspec.yaml` 添加依赖 ```yaml +dependencies: + # Freezed 模型需要 + freezed_annotation: ^2.4.1 + json_annotation: ^4.8.1 + dev_dependencies: swagger_generator_flutter: git: @@ -64,9 +69,15 @@ dev_dependencies: # 或在开发阶段使用本地路径 # swagger_generator_flutter: # path: ../swagger_generator_flutter + + # 代码生成工具 + build_runner: ^2.4.7 + freezed: ^2.4.7 + json_serializable: ^6.7.1 + retrofit_generator: ^8.0.0 # 如果使用 Retrofit ``` -2) 在宿主项目根目录准备 `generator_config.yaml`(复制 `generator_config.template.yaml`) +2) 在宿主项目根目录准备 `generator_config.yaml`(复制 `generator_config.template.yaml`) 重点字段: - `input.swagger_urls`:可配置多个 URL,后面的会覆盖前面的同名模型/路径(适合 v1 → v2 迭代) - `output.base_dir/api_dir/models_dir`:输出目录,支持相对路径(基于配置文件所在目录) @@ -75,10 +86,13 @@ dev_dependencies: - `generation.api.client`:设置 ApiClient 类名/文件名 - `generation.api.base_result_import / base_page_result_import`:接入自定义响应包装类型 -3) 生成代码 +3) 生成代码(两步) ```bash +# 步骤 1: 生成 Swagger API 和 Freezed 模型定义 flutter pub get dart run swagger_generator_flutter generate --all + +# 步骤 2: 运行 build_runner 生成 Freezed 和 json_serializable 的 part 文件 dart run build_runner build --delete-conflicting-outputs ``` CLI 里的 `--included-tags/--excluded-tags/--split-by-tags` 优先级高于配置文件。生成结果会按照版本落在 `api//` 下,模型分类在 `api_models/`。 @@ -137,7 +151,7 @@ Future main() async { - `--api` / `-r`: 只生成 Retrofit 风格 API 接口 - `--models` / `-m`: 只生成数据模型 - `--docs` / `-d`: 只生成 API 文档 -- `--simple` / `-s`: 使用简洁版模型生成 + - `--split-by-tags` / `-t`: 按 tags 分组生成多个 API 文件(默认启用) - `--output-dir` / `-o`: 指定输出目录(默认:generator) diff --git a/analysis_options.yaml b/analysis_options.yaml index 88747fd..3a75773 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,77 +1,18 @@ -# 1. 继承 Lint 规则集 (必选其一) -# -------------------------------------------------------------------------- -# 强烈推荐!根据你的项目类型选择一个 Lint 规则集作为起点。 -# 这能大大减少手动配置的工作量,并与社区最佳实践保持一致。 -# Linter 规则 - -# https://dart.ac.cn/tools/linter-rules - -# 如果是 Flutter 项目,推荐使用: -include: package:flutter_lints/flutter.yaml - -# 如果是纯 Dart 项目,推荐使用: -# include: package:lints/recommended.yaml - -# 2. 配置分析器 (必选) -# -------------------------------------------------------------------------- -# 分析器用于检查代码的语法和潜在问题。 -# 强烈建议启用所有规则,以确保代码质量和一致性。 +include: package:very_good_analysis/analysis_options.yaml analyzer: - errors: - require_trailing_commas: ignore - # 排除不想被分析的文件或目录。 - # 对于由代码生成工具(如 json_serializable, freezed)生成的 *.g.dart 文件, - # 强烈建议排除,因为你通常不需要对它们进行 Lint 检查。 exclude: - - '**/*.g.dart' # 排除所有以 .g.dart 结尾的文件 - - 'lib/generated/**' # 排除 lib/generated/ 目录下的所有文件 (如果你的生成文件都在这里) - - 'build/**' # 排除 Flutter/Dart 构建输出目录 + # 排除所有生成的文件 + - "**/*.g.dart" + - "**/*.freezed.dart" + # 如果还有其他生成文件,也可以添加 + # - "**/*.gr.dart" # auto_route 生成的文件 + # - "**/*.config.dart" # injectable 生成的文件 - -# 3. 配置 Lint 规则 -# -------------------------------------------------------------------------- linter: - # 在此处启用或禁用特定的 Lint 规则。 - # `include` 中的规则集已经包含了大部分常用规则,这里可以进行微调。 rules: - # 常用且推荐启用的规则 (即使默认集没有包含,也建议手动添加) - - avoid_empty_else # 避免空的 else 块 - # - avoid_print # 在生产代码中避免使用 print (可根据项目需求启用/禁用) - - avoid_relative_lib_imports # 避免从 'lib/' 相对导入 - - directives_ordering # 强制 import/export 指令排序 - # - avoid_return_and_type_annotation # 避免冗余的返回类型注解 - - curly_braces_in_flow_control_structures # 控制流语句强制使用大括号 - - empty_catches # 避免空的 catch 块 - - empty_constructor_bodies # 避免空的构造函数体 - - empty_statements # 避免空的语句 - - file_names # 文件名使用小写下划线命名 (my_file.dart) - # - prefer_const_constructors # 尽可能使用 const 构造函数 - # - prefer_const_declarations # 尽可能使用 const 声明 - # - prefer_const_literals_to_create_immutables # 尽可能使用 const 创建不可变集合 - # - prefer_single_quotes # 优先使用单引号 (或 prefer_double_quotes) - - prefer_final_fields # 类中的私有字段尽可能使用 final - - prefer_final_locals # 局部变量尽可能使用 final - # - prefer_for_elements_to_map_fromIterable # 优先使用 for 元素创建 Map - # - prefer_is_empty # 优先使用 .isEmpty - # - prefer_is_not_empty # 优先使用 .isNotEmpty - - unnecessary_new # Dart 2.0 后 new 关键字是可选的,推荐省略 - - unnecessary_this # 避免不必要的 this 关键字 - # - use_key_in_widget_constructors # Flutter Widget 构造函数中推荐使用 Key - - # 根据项目特性考虑启用的规则 (可能需要团队讨论) - # - annotate_overrides # 推荐:覆写方法添加 @override 注解 (如果 flutter_lints 已包含则无需重复) - # - lines_longer_than_80_chars # 强制行长 80 字符 (默认是警告,但通常较严格) - # - public_member_api_docs # 推荐:为公共 API 编写文档注释 (对库项目非常重要,应用项目可酌情) - - require_trailing_commas # 强制多行参数列表使用尾随逗号 (有助于格式化) - # - sort_constructors_first # 构造函数在类中声明在前 - # - sort_declarations_as_members # 类成员按字母顺序排序 - # - sort_pub_dependencies # pubspec.yaml 依赖按字母排序 - -# 4. 格式化器配置 -# -------------------------------------------------------------------------- -formatter: - # 设置 `dart format` 工具的行宽。 - # Dart 官方推荐 80,但许多团队会使用 100 或 120 以适应现代宽屏显示器。 - # 最重要的是整个团队**保持一致**。 - page_width: 80 + # 关闭强制文档注释 (很多业务开发觉得这条太累赘) + public_member_api_docs: false + + # 可选:如果你不喜欢强制构造函数必须写在最前面,也可以关掉 + # sort_constructors_first: false \ No newline at end of file diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index 86a565f..591c3bc 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -268,135 +268,23 @@ print(report); ### 基本使用流程 -```dart -import 'dart:io'; -import 'package:swagger_generator_flutter/swagger_generator_flutter.dart'; +```bash +# 步骤 1: 生成 API 和 Freezed 模型 +dart run swagger_generator_flutter generate --all -Future generateApiCode() async { - // 1. 创建解析器 - final parser = PerformanceParser( - config: ParseConfig( - enablePerformanceStats: true, - enableCaching: true, - ), - ); - - // 2. 创建验证器 - final validator = EnhancedValidator( - includeWarnings: true, - ); - - // 3. 创建生成器 - final generator = RetrofitApiGenerator( - className: 'ApiService', - splitByTags: true, - ); - - try { - // 4. 解析文档 - final jsonString = await File('swagger.json').readAsString(); - final document = await parser.parseDocument(jsonString); - - // 5. 验证文档 - final isValid = validator.validateDocument(document); - if (!isValid) { - final report = validator.errorReporter.generateReport(); - print('验证失败:\n$report'); - return; - } - - // 6. 生成代码 - final generatedCode = generator.generateFromDocument(document); - - // 7. 保存文件 - await File('lib/api/api_service.dart').writeAsString(generatedCode); - - print('✅ 代码生成完成!'); - - // 8. 显示性能统计 - final stats = parser.lastStats; - if (stats != null) { - print('解析时间: ${stats.totalTime.inMilliseconds}ms'); - print('生成的路径数: ${stats.pathCount}'); - } - } catch (e, stackTrace) { - print('❌ 生成失败: $e'); - print('堆栈跟踪: $stackTrace'); - } -} +# 步骤 2: 运行 build_runner 生成序列化代码 +dart run build_runner build --delete-conflicting-outputs ``` ### 高级使用流程 (企业级) -```dart -Future generateEnterpriseApiCode() async { - // 1. 配置高性能解析器 - final parser = PerformanceParser( - config: ParseConfig( - enablePerformanceStats: true, - enableParallelParsing: true, - enableCaching: true, - maxConcurrency: 8, - enableMemoryOptimization: true, - ), - ); +```bash +# 步骤 1: 生成 API 和 Freezed 模型 +# 使用 --included-tags 或 --excluded-tags 过滤范围 +dart run swagger_generator_flutter generate --all --excluded-tags=Internal,Debug - // 2. 配置增强验证器 - final validator = EnhancedValidator( - strictMode: true, - includeWarnings: true, - enableAutoFix: true, - customRules: [ - RequiredFieldRule(), - NamingConventionRule(), - TypeConsistencyRule(), - ], - ); - - // 3. 配置生成器(推荐) - final generator = RetrofitApiGenerator( - className: 'ApiService', - splitByTags: true, - ); - - try { - // 4. 解析文档 - final jsonString = await File('swagger.json').readAsString(); - final document = await parser.parseDocument(jsonString); - - // 5. 验证文档 - final isValid = validator.validateDocument(document); - if (!isValid) { - final errors = validator.errorReporter - .getErrorsBySeverity(ErrorSeverity.critical); - if (errors.isNotEmpty) { - throw Exception('文档包含严重错误,无法继续生成'); - } - } - - // 6. 生成代码(单文件或模块化) - final code = generator.generateFromDocument(document); - - // 7. 保存生成的文件 - await File('lib/api/api_service.dart').writeAsString(code); - - print('✅ 企业级代码生成完成!'); - - // 9. 生成性能报告 - final performanceReport = parser.generatePerformanceReport(); - await File('reports/performance_report.md') - .writeAsString(performanceReport); - - // 10. 生成验证报告 - final validationReport = validator.errorReporter.generateReport(); - await File('reports/validation_report.md') - .writeAsString(validationReport); - - } catch (e, stackTrace) { - print('❌ 企业级生成失败: $e'); - print('堆栈跟踪: $stackTrace'); - } -} +# 步骤 2: 运行 build_runner 生成序列化代码 +dart run build_runner build --delete-conflicting-outputs ``` --- diff --git a/docs/PROJECT_OVERVIEW.md b/docs/PROJECT_OVERVIEW.md index 11d8ca1..8cf7d92 100644 --- a/docs/PROJECT_OVERVIEW.md +++ b/docs/PROJECT_OVERVIEW.md @@ -42,7 +42,7 @@ SwaggerDataParser(下载/解析+缓存+性能监测) - **SchemaValidator / EnhancedValidator**: 基础与增强校验器,用于在生成前验证文档一致性 #### 4. 生成器 (Generators) -- **ModelCodeGenerator**: 按 request/result/enums/parameters 分类生成模型,分页模型自动替换为 `BasePageResult`,响应模型补全字符串/列表默认值 +- **ModelCodeGenerator**: 生成基于 Freezed 的不可变数据模型,自动获得 `copyWith`、`toString`、`==/hashCode` 等功能,并与 `json_serializable` 无缝集成。 - **RetrofitApiGenerator**: 支持按 tag 拆分、多版本目录与统一 ApiClient,自动生成查询参数实体并处理版本化类名 - **DocumentationGenerator**: 输出 Markdown API 文档与统计摘要 diff --git a/docs/USAGE_GUIDE.md b/docs/USAGE_GUIDE.md index a872d14..65b8715 100644 --- a/docs/USAGE_GUIDE.md +++ b/docs/USAGE_GUIDE.md @@ -22,25 +22,16 @@ flutter pub get ### 基础使用 -#### 命令行方式 (推荐新手) +在项目根目录下(`generator_config.yaml` 所在目录)运行以下命令: ```bash -# 克隆或下载项目 -git clone -cd swagger_generator_flutter +# 步骤 1: 生成 API 定义和 Freezed 模型 +# 这会根据 swagger.json 生成 *.dart 文件,但它们还不完整 +dart run swagger_generator_flutter generate --all -# 安装依赖 -flutter pub get - -# 将你的 swagger.json 放在项目根目录 - -# 生成所有代码 -sh run_swagger.sh all - -# 或者分别生成 -sh run_swagger.sh api # 只生成 API -sh run_swagger.sh models # 只生成模型 -sh run_swagger.sh docs # 只生成文档 +# 步骤 2: 运行 build_runner 完成代码生成 +# 这会生成 *.freezed.dart 和 *.g.dart 文件,补全模型和序列化逻辑 +dart run build_runner build --delete-conflicting-outputs ``` #### 编程方式 (推荐进阶用户) @@ -89,21 +80,17 @@ dependencies: # 网络请求 dio: ^5.4.0 retrofit: ^4.0.0 - - # JSON 序列化 + + # Freezed 模型 + freezed_annotation: ^2.4.1 json_annotation: ^4.8.1 - - # 其他依赖 - logging: ^1.2.0 dev_dependencies: # 代码生成 build_runner: ^2.4.7 retrofit_generator: ^8.0.0 json_serializable: ^6.7.1 - - # 测试 - test: ^1.24.0 + freezed: ^2.4.7 ``` ### 2. 项目结构 @@ -242,28 +229,6 @@ templates: ### 2. 代码生成最佳实践 -#### 选择合适的生成器 - -```dart -// 小型项目 - 基础生成器 -final generator = RetrofitApiGenerator( - className: 'ApiService', - splitByTags: true, -); - -// 中型项目 - 基础生成器(按标签拆分) -final generator = RetrofitApiGenerator( - className: 'ApiService', - splitByTags: true, -); - -// 大型项目 - 使用 RetrofitApiGenerator(按标签拆分),并在 CI 中并行处理多个模块 -final generator = RetrofitApiGenerator( - className: 'ApiService', - splitByTags: true, -); -``` - #### 配置合适的解析器 ```dart diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 24ba56d..3a75773 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -1,23 +1,18 @@ -include: package:flutter_lints/flutter.yaml - -linter: - rules: - - always_declare_return_types - - always_put_required_named_parameters_first - - avoid_print - - avoid_unnecessary_containers - - prefer_const_constructors - - prefer_const_literals_to_create_immutables - - prefer_final_fields - - prefer_single_quotes - - sort_child_properties_last - - use_key_in_widget_constructors +include: package:very_good_analysis/analysis_options.yaml analyzer: exclude: - - '**/*.g.dart' - - '**/*.freezed.dart' - - 'lib/generated/**' - errors: - invalid_annotation_target: ignore + # 排除所有生成的文件 + - "**/*.g.dart" + - "**/*.freezed.dart" + # 如果还有其他生成文件,也可以添加 + # - "**/*.gr.dart" # auto_route 生成的文件 + # - "**/*.config.dart" # injectable 生成的文件 +linter: + rules: + # 关闭强制文档注释 (很多业务开发觉得这条太累赘) + public_member_api_docs: false + + # 可选:如果你不喜欢强制构造函数必须写在最前面,也可以关掉 + # sort_constructors_first: false \ No newline at end of file diff --git a/example/generate_api.sh b/example/generate_api.sh index 8ca5a0a..e40e66a 100644 --- a/example/generate_api.sh +++ b/example/generate_api.sh @@ -57,9 +57,9 @@ echo "" # 步骤 3: 修复和排序 imports echo -e "${CYAN}🔧 步骤 3/4: 修复和排序 imports...${NC}" -dart fix --apply lib/common/api +dart fix --apply lib/src/api FIX_API_EXIT_CODE=$? -dart fix --apply lib/common/api_models +dart fix --apply lib/src/api_models FIX_MODELS_EXIT_CODE=$? if [ $FIX_API_EXIT_CODE -ne 0 ] || [ $FIX_MODELS_EXIT_CODE -ne 0 ]; then @@ -72,7 +72,7 @@ echo "" # 步骤 4: 格式化代码 echo -e "${CYAN}📐 步骤 4/4: 格式化代码...${NC}" -dart format lib/common/api lib/common/api_models --set-exit-if-changed +dart format lib/src/api lib/src/api_models --set-exit-if-changed if [ $? -ne 0 ]; then echo -e "${YELLOW}⚠️ 代码格式化失败,请检查错误信息${NC}" @@ -86,8 +86,8 @@ echo -e "${GREEN}✨ 代码生成完成!${NC}" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo -e "${CYAN}📋 生成的文件位置:${NC}" -echo " - API 接口: lib/common/api/" -echo " - API 模型: lib/common/api_models/" +echo " - API 接口: lib/src/api/" +echo " - API 模型: lib/src/api_models/" echo "" echo -e "${CYAN}💡 提示:${NC}" echo " - 如果生成的文件有错误,请检查并修复后重新运行 build_runner" diff --git a/example/lib/common/base_page_result.dart b/example/lib/common/base_page_result.dart index 306f11d..739806a 100644 --- a/example/lib/common/base_page_result.dart +++ b/example/lib/common/base_page_result.dart @@ -19,7 +19,8 @@ class BasePageResult extends Object { factory BasePageResult.fromJson( Map json, T Function(dynamic json) fromJsonT, - ) => _$BasePageResultFromJson(json, fromJsonT); + ) => + _$BasePageResultFromJson(json, fromJsonT); Map toJson(Object Function(T value) toJsonT) => _$BasePageResultToJson(this, toJsonT); diff --git a/example/lib/common/base_result.dart b/example/lib/common/base_result.dart index 66ae63c..407b740 100644 --- a/example/lib/common/base_result.dart +++ b/example/lib/common/base_result.dart @@ -43,7 +43,8 @@ class BaseResult extends BaseContainsParametersAbstract { factory BaseResult.fromJson( Map json, T Function(dynamic json) fromJsonT, - ) => _$BaseResultFromJson(json, fromJsonT); + ) => + _$BaseResultFromJson(json, fromJsonT); @override Map toJson(Object Function(T value) toJsonT) => diff --git a/example/pubspec.lock b/example/pubspec.lock index 165d32c..d7e85f4 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,18 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f + sha256: f0bb5d1648339c8308cc0b9838d8456b3cfe5c91f9dc1a735b4d003269e5da9a url: "https://pub.flutter-io.cn" source: hosted - version: "85.0.0" + version: "88.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d" + sha256: "0b7b9c329d2879f8f05d6c05b32ee9ec025f39b077864bdb5ac9a7b63418a98f" url: "https://pub.flutter-io.cn" source: hosted - version: "7.7.1" + version: "8.1.1" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.3" args: dependency: transitive description: @@ -45,18 +53,18 @@ packages: dependency: transitive description: name: build - sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" + sha256: c1668065e9ba04752570ad7e038288559d1e2ca5c6d0131c0f5f55e39e777413 url: "https://pub.flutter-io.cn" source: hosted - version: "2.5.4" + version: "4.0.3" build_config: dependency: transitive description: name: build_config - sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187" url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.2" + version: "1.2.0" build_daemon: dependency: transitive description: @@ -65,30 +73,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "4.1.0" - 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" + sha256: "110c56ef29b5eb367b4d17fc79375fa8c18a6cd7acd92c05bb3986c17a079057" 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" + version: "2.10.4" built_collection: dependency: transitive description: @@ -165,10 +157,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb" + sha256: c87dfe3d56f183ffe9106a18aebc6db431fc7c98c31a54b952a77f3d54a85697 url: "https://pub.flutter-io.cn" source: hosted - version: "3.1.1" + version: "3.1.2" dio: dependency: "direct main" description: @@ -218,15 +210,31 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.2" + version: "6.0.0" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + freezed: + dependency: "direct dev" + description: + name: freezed + sha256: "13065f10e135263a4f5a4391b79a8efc5fb8106f8dd555a9e49b750b45393d77" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.3" + freezed_annotation: + dependency: "direct main" + description: + name: freezed_annotation + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.0" frontend_server_client: dependency: transitive description: @@ -251,6 +259,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.3.2" + hotreloader: + dependency: transitive + description: + name: hotreloader + sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.3.0" http: dependency: transitive description: @@ -283,14 +299,6 @@ packages: 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: @@ -303,10 +311,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c + sha256: c5b2ee75210a0f263c6c7b9eeea80553dbae96ea1bf57f02484e806a3ffdffa3 url: "https://pub.flutter-io.cn" source: hosted - version: "6.9.5" + version: "6.11.2" leak_tracker: dependency: transitive description: @@ -331,14 +339,22 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" + lean_builder: + dependency: transitive + description: + name: lean_builder + sha256: ef5cd5f907157eb7aa87d1704504b5a6386d2cbff88a3c2b3344477bab323ee9 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.2" lints: dependency: transitive description: name: lints - sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.0" + version: "6.0.0" logger: dependency: "direct main" description: @@ -415,10 +431,10 @@ packages: dependency: transitive description: name: protobuf - sha256: de9c9eb2c33f8e933a42932fe1dc504800ca45ebc3d673e6ed7f39754ee4053e + sha256: "2fcc8a202ca7ec17dab7c97d6b6d91cf03aa07fe6f65f8afbb6dfa52cc5bd902" url: "https://pub.flutter-io.cn" source: hosted - version: "4.2.0" + version: "5.1.0" pub_semver: dependency: transitive description: @@ -439,18 +455,18 @@ packages: dependency: "direct main" description: name: retrofit - sha256: "7d78824afa6eeeaf6ac58220910ee7a97597b39e93360d4bda230b7c6df45089" + sha256: "84063c18a00d55af41d6b8401edf8473e8c215bd7068ef7ec5e34c60657ffdbe" url: "https://pub.flutter-io.cn" source: hosted - version: "4.9.0" + version: "4.9.1" retrofit_generator: dependency: "direct dev" description: name: retrofit_generator - sha256: "9abcf21acb95bf7040546eafff87f60cf0aee20b05101d71f99876fc4df1f522" + sha256: "7ec323f3329ad2ca0bcdc96fe02ec7f2486ecfac6cd2d035b03c398ef6f42308" url: "https://pub.flutter-io.cn" source: hosted - version: "9.7.0" + version: "10.2.0" shelf: dependency: transitive description: @@ -476,18 +492,18 @@ packages: dependency: transitive description: name: source_gen - sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" + sha256: "07b277b67e0096c45196cbddddf2d8c6ffc49342e88bf31d460ce04605ddac75" url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.0" + version: "4.1.1" source_helper: dependency: transitive description: name: source_helper - sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" + sha256: "6a3c6cc82073a8797f8c4dc4572146114a39652851c157db37e964d9c7038723" url: "https://pub.flutter-io.cn" source: hosted - version: "1.3.5" + version: "1.3.8" source_span: dependency: transitive description: @@ -534,7 +550,7 @@ packages: path: ".." relative: true source: path - version: "2.1.1" + version: "3.0.0" term_glyph: dependency: transitive description: @@ -551,14 +567,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.7.6" - 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: @@ -575,6 +583,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" + very_good_analysis: + dependency: "direct dev" + description: + name: very_good_analysis + sha256: "96245839dbcc45dfab1af5fa551603b5c7a282028a64746c19c547d21a7f1e3a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "10.0.0" vm_service: dependency: transitive description: @@ -615,6 +631,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.0.3" + xxh3: + dependency: transitive + description: + name: xxh3 + sha256: "399a0438f5d426785723c99da6b16e136f4953fb1e9db0bf270bd41dd4619916" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0" yaml: dependency: transitive description: @@ -624,5 +648,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.8.0 <4.0.0" + dart: ">=3.9.0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index b68bf2c..212a3cd 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -4,18 +4,19 @@ description: Example Flutter app using swagger_generator_flutter as dev_dependen version: 1.0.0 environment: - sdk: '>=3.0.0 <4.0.0' + sdk: ^3.8.0 dependencies: flutter: sdk: flutter # HTTP 客户端 - dio: ^5.0.0 - retrofit: ^4.0.0 - + dio: ^5.9.0 + # API 客户端 + retrofit: ^4.9.1 # JSON 序列化 json_annotation: ^4.9.0 + freezed_annotation: ^3.1.0 # 其他依赖 logger: ^2.0.0 @@ -29,12 +30,14 @@ dev_dependencies: path: ../ # 代码生成工具 - build_runner: ^2.4.7 - retrofit_generator: ^9.0.0 - json_serializable: ^6.7.1 + build_runner: ^2.10.4 + retrofit_generator: ^10.2.0 + json_serializable: ^6.11.2 + freezed: ^3.2.3 # 代码分析 - flutter_lints: ^3.0.0 + flutter_lints: 6.0.0 + very_good_analysis: ^10.0.0 flutter: uses-material-design: true diff --git a/fix_cascades.py b/fix_cascades.py new file mode 100644 index 0000000..61669d8 --- /dev/null +++ b/fix_cascades.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +""" +自动修复 Dart 代码中的 cascade_invocations lint 问题 +将连续的 buffer.writeln() 和 buffer.write() 调用转换为使用级联操作符 (..) +""" + +import re +import sys +from pathlib import Path + + +def fix_cascade_invocations(content: str) -> str: + """修复 cascade_invocations 问题""" + lines = content.split('\n') + result = [] + i = 0 + + while i < len(lines): + line = lines[i] + + # 检测是否是 buffer.writeln() 或 buffer.write() 调用 + if re.match(r'\s+buffer\.(writeln|write)\(', line): + # 收集连续的 buffer 调用 + buffer_calls = [line] + indent = len(line) - len(line.lstrip()) + j = i + 1 + + # 查找连续的 buffer 调用 + while j < len(lines): + next_line = lines[j] + # 跳过空行和注释 + if not next_line.strip() or next_line.strip().startswith('//'): + j += 1 + continue + # 检查是否是相同缩进的 buffer 调用 + if re.match(r'\s+buffer\.(writeln|write)\(', next_line): + next_indent = len(next_line) - len(next_line.lstrip()) + if next_indent == indent: + buffer_calls.append(next_line) + j += 1 + else: + break + else: + break + + # 如果有多个连续的 buffer 调用,转换为级联 + if len(buffer_calls) >= 2: + # 第一行改为 buffer + first_line = buffer_calls[0] + indent_str = ' ' * indent + + # 提取第一个调用 + match = re.match(r'(\s+)buffer\.(writeln|write)\((.*)\);?', first_line) + if match: + method = match.group(2) + args = match.group(3) + + result.append(f'{indent_str}buffer') + result.append(f'{indent_str} ..{method}({args})') + + # 处理后续调用 + for call in buffer_calls[1:]: + match = re.match(r'\s+buffer\.(writeln|write)\((.*)\);?', call) + if match: + method = match.group(1) + args = match.group(2) + result.append(f'{indent_str} ..{method}({args})') + + # 添加分号 + result[-1] = result[-1].rstrip() + ';' + + i = j + continue + + result.append(line) + i += 1 + + return '\n'.join(result) + + +def process_file(file_path: Path) -> bool: + """处理单个文件""" + try: + content = file_path.read_text(encoding='utf-8') + fixed_content = fix_cascade_invocations(content) + + if content != fixed_content: + file_path.write_text(fixed_content, encoding='utf-8') + print(f'✓ Fixed: {file_path}') + return True + else: + print(f'- Skipped: {file_path} (no changes needed)') + return False + except Exception as e: + print(f'✗ Error processing {file_path}: {e}') + return False + + +def main(): + """主函数""" + if len(sys.argv) < 2: + print('Usage: python fix_cascades.py ') + sys.exit(1) + + path = Path(sys.argv[1]) + + if not path.exists(): + print(f'Error: {path} does not exist') + sys.exit(1) + + files_to_process = [] + + if path.is_file(): + files_to_process = [path] + else: + # 递归查找所有 .dart 文件 + files_to_process = list(path.rglob('*.dart')) + + print(f'Processing {len(files_to_process)} files...\n') + + fixed_count = 0 + for file_path in files_to_process: + if process_file(file_path): + fixed_count += 1 + + print(f'\n✓ Fixed {fixed_count} files') + + +if __name__ == '__main__': + main() diff --git a/lib/commands/base_command.dart b/lib/commands/base_command.dart index 5d55f02..8519ac5 100644 --- a/lib/commands/base_command.dart +++ b/lib/commands/base_command.dart @@ -1,4 +1,4 @@ -import '../core/exceptions.dart'; +import 'package:swagger_generator_flutter/core/exceptions.dart'; /// 命令基类 /// 实现命令模式,提供统一的命令接口 @@ -125,36 +125,34 @@ abstract class BaseCommand { /// 命令选项 class CommandOption { + const CommandOption({ + required this.name, + required this.description, + this.shortName, + this.required = false, + this.defaultValue, + this.type = OptionType.flag, + }); 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, }); + final String name; + final String description; + final bool required; + final dynamic defaultValue; } /// 选项类型 @@ -204,16 +202,15 @@ class ParsedArguments { /// 参数解析器 class ArgumentParser { - final BaseCommand command; - ArgumentParser(this.command); + final BaseCommand command; /// 解析参数 ParsedArguments parse(List args) { final result = ParsedArguments(); final argQueue = List.from(args); - int argumentIndex = 0; + var argumentIndex = 0; while (argQueue.isNotEmpty) { final current = argQueue.removeAt(0); @@ -343,11 +340,10 @@ class ArgumentParser { /// 命令异常 class CommandException implements Exception { + const CommandException(this.message, {this.details}); 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 index 0f5187d..e083c0a 100644 --- a/lib/commands/generate_command.dart +++ b/lib/commands/generate_command.dart @@ -1,15 +1,14 @@ import 'dart:io'; import 'package:path/path.dart' as path; - -import '../core/config.dart'; -import '../core/config_loader.dart'; -import '../core/models.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'; +import 'package:swagger_generator_flutter/commands/base_command.dart'; +import 'package:swagger_generator_flutter/core/config.dart'; +import 'package:swagger_generator_flutter/core/config_loader.dart'; +import 'package:swagger_generator_flutter/core/models.dart'; +import 'package:swagger_generator_flutter/generators/model_code_generator.dart'; +import 'package:swagger_generator_flutter/generators/retrofit_api_generator.dart'; +import 'package:swagger_generator_flutter/parsers/swagger_data_parser.dart'; +import 'package:swagger_generator_flutter/utils/file_utils.dart'; /// Generate命令 /// 用于生成各种代码文件 @@ -29,31 +28,21 @@ class GenerateCommand extends BaseCommand { name: 'models', shortName: 'm', description: '生成数据模型', - 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', @@ -96,7 +85,7 @@ class GenerateCommand extends BaseCommand { final urls = SwaggerConfig.swaggerJsonUrls; progress('URL 处理顺序: ${urls.join(" -> ")}'); - for (int i = 0; i < urls.length; i++) { + for (var i = 0; i < urls.length; i++) { final url = urls[i]; progress(' [${i + 1}/${urls.length}] 正在解析: $url'); @@ -124,7 +113,8 @@ class GenerateCommand extends BaseCommand { if (overlappingModels.isNotEmpty) { progress( - ' 发现 ${overlappingModels.length} 个同名模型将被覆盖: ${overlappingModels.take(5).join(", ")}${overlappingModels.length > 5 ? "..." : ""}'); + ' 发现 ${overlappingModels.length} 个同名模型将被覆盖: ${overlappingModels.take(5).join(", ")}${overlappingModels.length > 5 ? "..." : ""}', + ); } mergedDocument = SwaggerDocument( @@ -140,12 +130,14 @@ class GenerateCommand extends BaseCommand { final afterModelCount = mergedDocument.models.length; progress( - ' 合并后: $beforeModelCount + $currentModelCount -> $afterModelCount 个模型'); + ' 合并后: $beforeModelCount + $currentModelCount -> $afterModelCount 个模型', + ); // 验证同名模型是否被正确覆盖 if (overlappingModels.isNotEmpty) { progress( - ' 同名模型列表: ${overlappingModels.take(10).join(", ")}${overlappingModels.length > 10 ? "..." : ""}'); + ' 同名模型列表: ${overlappingModels.take(10).join(", ")}${overlappingModels.length > 10 ? "..." : ""}', + ); } } } @@ -181,15 +173,12 @@ class GenerateCommand extends BaseCommand { await FileUtils.ensureDirectoryExists(apiDir); await FileUtils.ensureDirectoryExists(modelsDir); - int generatedFiles = 0; + var generatedFiles = 0; // 生成模型代码 if (options.generateModels) { progress('正在生成数据模型...'); - final generator = ModelCodeGenerator( - document, - useSimpleModels: options.useSimpleModels, - ); + final generator = ModelCodeGenerator(document); await FileUtils.ensureDirectoryExists(modelsDir); @@ -224,7 +213,8 @@ class GenerateCommand extends BaseCommand { } progress( - '检测到 ${pathsByVersion.keys.length} 个版本: ${pathsByVersion.keys.join(", ")}'); + '检测到 ${pathsByVersion.keys.length} 个版本: ${pathsByVersion.keys.join(", ")}', + ); // ✨ 按版本分别生成 API 文件 final versionedFiles = >{}; @@ -238,9 +228,9 @@ class GenerateCommand extends BaseCommand { // 筛选出当前版本实际使用的 controllers final versionTags = versionPaths.expand((p) => p.tags).toSet(); final versionControllers = { - for (var tag in versionTags) + for (final tag in versionTags) if (document.controllers.containsKey(tag)) - tag: document.controllers[tag]! + tag: document.controllers[tag]!, }; // 创建该版本的临时文档 @@ -248,7 +238,7 @@ class GenerateCommand extends BaseCommand { title: document.title, description: document.description, version: document.version, - paths: {for (var p in versionPaths) p.path: p}, + paths: {for (final p in versionPaths) p.path: p}, models: document.models, controllers: versionControllers, // 使用过滤后的 controllers ); @@ -257,10 +247,6 @@ class GenerateCommand extends BaseCommand { final apiClientClassName = ConfigLoader.getApiClientClassName(); final generator = RetrofitApiGenerator( className: apiClientClassName, - useRetrofit: true, - useDio: true, - splitByTags: true, - versionedApi: true, ); generator.document = versionDocument; @@ -337,8 +323,6 @@ class GenerateCommand extends BaseCommand { // 生成参数实体类文件(使用最后一个生成器) final lastGenerator = RetrofitApiGenerator( className: apiClientClassName, - useRetrofit: true, - useDio: true, ); lastGenerator.document = document; lastGenerator.ensureParameterEntitiesGenerated(); @@ -459,7 +443,6 @@ class GenerateCommand extends BaseCommand { generateApi: hasAnyFlag ? (args.getOption('api') ?? false) : (args.getOption('all') ?? true), - useSimpleModels: args.getOption('simple') ?? false, splitByTags: splitByTags, includedTags: includedTags, excludedTags: excludedTags, @@ -549,13 +532,13 @@ class GenerateCommand extends BaseCommand { buffer.writeln('// 基于 Swagger API 文档: '); buffer.writeln('// 由 xy_swagger_generator by max 生成'); buffer.writeln('// Copyright (C) 2025 YuanXuan. All rights reserved.'); - buffer.writeln(''); - buffer.writeln(''); + buffer.writeln(); + buffer.writeln(); buffer.writeln('library;'); - buffer.writeln(''); + buffer.writeln(); for (final fileName in dartFiles) { - buffer.writeln('export \'$fileName\';'); + buffer.writeln("export '$fileName';"); } // 写入文件 @@ -576,29 +559,29 @@ class GenerateCommand extends BaseCommand { buffer.writeln('// 基于 Swagger API 文档: '); buffer.writeln('// 由 xy_swagger_generator by max 生成'); buffer.writeln('// Copyright (C) 2025 YuanXuan. All rights reserved.'); - buffer.writeln(''); + buffer.writeln(); buffer.writeln('library;'); - buffer.writeln(''); + buffer.writeln(); // 导出 base_result 和 base_page_result(如果配置了) final baseResultImport = SwaggerConfig.baseResultImport; final basePageResultImport = SwaggerConfig.basePageResultImport; if (baseResultImport.isNotEmpty) { - buffer.writeln('export \'$baseResultImport\';'); + buffer.writeln("export '$baseResultImport';"); } if (basePageResultImport.isNotEmpty) { - buffer.writeln('export \'$basePageResultImport\';'); + buffer.writeln("export '$basePageResultImport';"); } if ((baseResultImport.isNotEmpty || basePageResultImport.isNotEmpty) && fileNames.isNotEmpty) { - buffer.writeln(''); + buffer.writeln(); } // 导出所有文件 for (final fileName in fileNames) { - buffer.writeln('export \'$fileName\';'); + buffer.writeln("export '$fileName';"); } return buffer.toString(); @@ -608,20 +591,21 @@ class GenerateCommand extends BaseCommand { void _generateSummary(SwaggerDocument document, String outputDir) { final summary = StringBuffer(); summary.writeln('# 代码生成摘要'); - 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('## 统计信息'); summary.writeln('- 控制器数量: ${document.controllers.length}'); summary.writeln('- API路径数量: ${document.paths.length}'); summary.writeln('- 数据模型数量: ${document.models.length}'); - summary.writeln(''); + summary.writeln(); summary.writeln('## 控制器列表'); document.controllers.forEach((name, controller) { summary.writeln( - '- **$name**: ${controller.description} (${controller.paths.length} 个路径)'); + '- **$name**: ${controller.description} (${controller.paths.length} 个路径)', + ); }); FileUtils.writeFile('$outputDir/SUMMARY.md', summary.toString()); @@ -644,7 +628,7 @@ class GenerateCommand extends BaseCommand { } } catch (e) { // 如果正则表达式无效,使用默认模式 - final defaultPattern = r'/api/v(\d+)/'; + const defaultPattern = r'/api/v(\d+)/'; final versionMatch = RegExp(defaultPattern).firstMatch(path); if (versionMatch != null) { return 'v${versionMatch.group(1)}'; @@ -700,7 +684,8 @@ class GenerateCommand extends BaseCommand { /// 生成版本化的 ApiClient String _generateVersionedApiClient( - Map> versionedFiles) { + Map> versionedFiles, + ) { final buffer = StringBuffer(); // 文件头 @@ -709,7 +694,7 @@ class GenerateCommand extends BaseCommand { buffer.writeln('// 由 xy_swagger_generator by max 生成'); buffer.writeln('// Copyright (C) 2025 YuanXuan. All rights reserved.'); buffer.writeln(); - buffer.writeln('import \'package:dio/dio.dart\';'); + buffer.writeln("import 'package:dio/dio.dart';"); buffer.writeln(); // 收集所有 API 类 @@ -736,9 +721,12 @@ class GenerateCommand extends BaseCommand { final className = fileName .replaceAll('.dart', '') .split('_') - .map((word) => - word.isEmpty ? '' : (word[0].toUpperCase() + word.substring(1))) - .join(''); + .map( + (word) => word.isEmpty + ? '' + : (word[0].toUpperCase() + word.substring(1)), + ) + .join(); apiClasses[version]!.add(className); } } @@ -746,7 +734,7 @@ class GenerateCommand extends BaseCommand { // 导入所有版本的 index.dart final versions = apiClasses.keys.toList()..sort(); for (final version in versions) { - buffer.writeln('import \'$version/index.dart\';'); + buffer.writeln("import '$version/index.dart';"); } buffer.writeln(); @@ -768,7 +756,8 @@ class GenerateCommand extends BaseCommand { for (final className in versionEntry.value) { final suffix = version == 'v1' ? '' : versionUpper; buffer.writeln( - ' late final $className$suffix _${_toLowerCamelCase(className)}$suffix;'); + ' late final $className$suffix _${_toLowerCamelCase(className)}$suffix;', + ); } } @@ -812,7 +801,8 @@ class GenerateCommand extends BaseCommand { final fieldName = _toLowerCamelCase(className); final suffix = version == 'v1' ? '' : versionUpper; buffer.writeln( - ' $className$suffix get $fieldName$suffix => _$fieldName$suffix;'); + ' $className$suffix get $fieldName$suffix => _$fieldName$suffix;', + ); } buffer.writeln(); } @@ -847,7 +837,9 @@ class GenerateCommand extends BaseCommand { /// 生成版本目录的 index.dart Future _generateVersionIndexFile( - String versionDir, List fileNames) async { + String versionDir, + List fileNames, + ) async { final buffer = StringBuffer(); // 文件头 @@ -859,7 +851,7 @@ class GenerateCommand extends BaseCommand { // 导出所有 API 文件 final sortedFiles = fileNames.toList()..sort(); for (final fileName in sortedFiles) { - buffer.writeln('export \'$fileName\';'); + buffer.writeln("export '$fileName';"); } final indexPath = '$versionDir/index.dart'; @@ -943,7 +935,7 @@ class GenerateCommand extends BaseCommand { final filteredControllers = {}; for (final entry in document.controllers.entries) { final tagName = entry.key; - bool shouldKeep = true; + var shouldKeep = true; if (hasIncludes && !includedTags.contains(tagName)) { shouldKeep = false; } @@ -956,7 +948,8 @@ class GenerateCommand extends BaseCommand { } progress( - ' 保留了 ${filteredControllers.length}/${document.controllers.length} 个控制器'); + ' 保留了 ${filteredControllers.length}/${document.controllers.length} 个控制器', + ); // 返回过滤后的文档 return SwaggerDocument( @@ -976,8 +969,8 @@ class GenerateCommand extends BaseCommand { void _collectUsedModels(ApiPath path, Set usedModelNames) { // 递归地从 schema 中提取模型名称 void extractModelsFromSchema(Map schema) { - if (schema.containsKey('\$ref')) { - final modelName = _extractModelNameFromRef(schema['\$ref']); + if (schema.containsKey(r'$ref')) { + final modelName = _extractModelNameFromRef(schema[r'$ref'] as String); if (modelName != null) { usedModelNames.add(modelName); } @@ -987,11 +980,11 @@ class GenerateCommand extends BaseCommand { if (schema.containsKey('type')) { final type = schema['type']; if (type == 'array' && schema.containsKey('items')) { - extractModelsFromSchema(schema['items']); + extractModelsFromSchema(schema['items'] as Map); } else if (type == 'object' && schema.containsKey('properties')) { final properties = schema['properties'] as Map; for (final propSchema in properties.values) { - extractModelsFromSchema(propSchema); + extractModelsFromSchema(propSchema as Map); } } } @@ -1000,7 +993,7 @@ class GenerateCommand extends BaseCommand { if (schema.containsKey(key)) { final subSchemas = schema[key] as List; for (final subSchema in subSchemas) { - extractModelsFromSchema(subSchema); + extractModelsFromSchema(subSchema as Map); } } } @@ -1035,7 +1028,7 @@ class GenerateCommand extends BaseCommand { for (final property in model.properties.values) { // 使用 reference 字段 if (property.reference != null && - !checkedModels.contains(property.reference!)) { + !checkedModels.contains(property.reference)) { modelsToCheck.add(property.reference!); } @@ -1050,7 +1043,7 @@ class GenerateCommand extends BaseCommand { // 处理嵌套属性中的引用 for (final nestedProp in property.nestedProperties.values) { if (nestedProp.reference != null && - !checkedModels.contains(nestedProp.reference!)) { + !checkedModels.contains(nestedProp.reference)) { modelsToCheck.add(nestedProp.reference!); } } @@ -1082,19 +1075,16 @@ class GenerateCommand extends BaseCommand { /// 生成选项 class GenerateOptions { - final bool generateModels; - final bool generateApi; - final bool useSimpleModels; - final bool splitByTags; - final List? includedTags; - final List? excludedTags; - const GenerateOptions({ required this.generateModels, required this.generateApi, - required this.useSimpleModels, required this.splitByTags, this.includedTags, this.excludedTags, }); + final bool generateModels; + final bool generateApi; + final bool splitByTags; + final List? includedTags; + final List? excludedTags; } diff --git a/lib/core/config.dart b/lib/core/config.dart index ec4b3c1..974ee4c 100644 --- a/lib/core/config.dart +++ b/lib/core/config.dart @@ -1,4 +1,4 @@ -import 'config_loader.dart'; +import 'package:swagger_generator_flutter/core/config_loader.dart'; /// Swagger配置管理 /// 集中管理所有Swagger相关的配置项 diff --git a/lib/core/config_loader.dart b/lib/core/config_loader.dart index 880ffc8..85b7162 100644 --- a/lib/core/config_loader.dart +++ b/lib/core/config_loader.dart @@ -1,7 +1,8 @@ import 'dart:io'; + import 'package:path/path.dart' as path; +import 'package:swagger_generator_flutter/core/config.dart'; import 'package:yaml/yaml.dart'; -import 'config.dart'; /// 配置加载器 /// 负责从 generator_config.yaml 文件读取配置 @@ -74,7 +75,7 @@ class ConfigLoader { /// 从当前工作目录向上查找 generator_config.yaml static String? _findConfigFile() { var currentDir = Directory.current; - final maxDepth = 10; // 最多向上查找 10 层 + const maxDepth = 10; // 最多向上查找 10 层 var depth = 0; while (depth < maxDepth) { @@ -200,11 +201,11 @@ class ConfigLoader { final ignoredDirs = getIgnoredDirectories(config); if (ignoredDirs.isNotEmpty) { // 标准化路径,统一使用正斜杠 - final normalizedPath = filePath.replaceAll('\\', '/'); + final normalizedPath = filePath.replaceAll(r'\', '/'); for (final ignoredDir in ignoredDirs) { // 标准化忽略目录路径 - var normalizedDir = ignoredDir.toString().replaceAll('\\', '/'); + var normalizedDir = ignoredDir.replaceAll(r'\', '/'); // 移除末尾的斜杠(如果有) if (normalizedDir.endsWith('/')) { @@ -236,7 +237,7 @@ class ConfigLoader { final fileName = path.basename(filePath); for (final ignoredFile in ignoredFiles) { - final ignoredFileName = ignoredFile.toString(); + final ignoredFileName = ignoredFile; // 精确匹配文件名 if (fileName == ignoredFileName) { diff --git a/lib/core/error_reporter.dart b/lib/core/error_reporter.dart index e22635a..5ac628a 100644 --- a/lib/core/error_reporter.dart +++ b/lib/core/error_reporter.dart @@ -77,6 +77,14 @@ extension ErrorCategoryExtension on ErrorCategory { /// 错误位置信息 class ErrorLocation { + const ErrorLocation({ + required this.jsonPath, + this.line, + this.column, + this.offset, + this.snippet, + }); + /// JSON 路径(如 "paths./users.get.responses.200") final String jsonPath; @@ -92,14 +100,6 @@ class ErrorLocation { /// 相关的 JSON 片段 final String? snippet; - const ErrorLocation({ - required this.jsonPath, - this.line, - this.column, - this.offset, - this.snippet, - }); - @override String toString() { final buffer = StringBuffer(); @@ -119,6 +119,13 @@ class ErrorLocation { /// 修复建议 class FixSuggestion { + const FixSuggestion({ + required this.description, + this.codeExample, + this.documentationUrl, + this.autoFix, + }); + /// 建议描述 final String description; @@ -130,17 +137,22 @@ class FixSuggestion { /// 自动修复函数(如果支持) final String Function(String original)? autoFix; - - const FixSuggestion({ - required this.description, - this.codeExample, - this.documentationUrl, - this.autoFix, - }); } /// 详细错误报告 class DetailedError { + DetailedError({ + required this.id, + required this.title, + required this.description, + required this.severity, + required this.category, + required this.location, + this.suggestions = const [], + this.relatedErrors = const [], + DateTime? timestamp, + }) : timestamp = timestamp ?? DateTime.now(); + /// 错误 ID(用于查找和分类) final String id; @@ -168,18 +180,6 @@ class DetailedError { /// 错误发生时间 final DateTime timestamp; - DetailedError({ - required this.id, - required this.title, - required this.description, - required this.severity, - required this.category, - required this.location, - this.suggestions = const [], - this.relatedErrors = const [], - DateTime? timestamp, - }) : timestamp = timestamp ?? DateTime.now(); - @override String toString() { final buffer = StringBuffer(); @@ -205,7 +205,7 @@ class DetailedError { // 修复建议 if (suggestions.isNotEmpty) { buffer.writeln('Suggestions:'); - for (int i = 0; i < suggestions.length; i++) { + for (var i = 0; i < suggestions.length; i++) { final suggestion = suggestions[i]; buffer.writeln(' ${i + 1}. ${suggestion.description}'); @@ -232,11 +232,10 @@ class DetailedError { /// 错误报告器 class ErrorReporter { + ErrorReporter(); final List _errors = []; final Map _errorCounts = {}; - ErrorReporter(); - /// 添加错误 void addError(DetailedError error) { _errors.add(error); @@ -306,9 +305,11 @@ class ErrorReporter { _errors.any((e) => e.severity == ErrorSeverity.critical); /// 检查是否有错误(不包括警告和信息) - bool get hasErrorsOrCritical => _errors.any((e) => - e.severity == ErrorSeverity.error || - e.severity == ErrorSeverity.critical); + bool get hasErrorsOrCritical => _errors.any( + (e) => + e.severity == ErrorSeverity.error || + e.severity == ErrorSeverity.critical, + ); /// 清除所有错误 void clear() { @@ -360,7 +361,9 @@ class ErrorReporter { /// 按类别生成报告 void _generateReportByCategory( - StringBuffer buffer, List errors) { + StringBuffer buffer, + List errors, + ) { final errorsByCategory = >{}; for (final error in errors) { @@ -383,7 +386,7 @@ class ErrorReporter { buffer.writeln('🔍 Detailed Error Report'); buffer.writeln('=' * 50); - for (int i = 0; i < errors.length; i++) { + for (var i = 0; i < errors.length; i++) { buffer.writeln('Error ${i + 1}/${errors.length}:'); buffer.writeln(errors[i].toString()); @@ -402,28 +405,32 @@ class ErrorReporter { 'by_severity': getErrorStatistics().map((k, v) => MapEntry(k.name, v)), }, 'errors': _errors - .map((error) => { - 'id': error.id, - 'title': error.title, - 'description': error.description, - 'severity': error.severity.name, - 'category': error.category.name, - 'location': { - 'json_path': error.location.jsonPath, - 'line': error.location.line, - 'column': error.location.column, - 'snippet': error.location.snippet, - }, - 'suggestions': error.suggestions - .map((suggestion) => { - 'description': suggestion.description, - 'code_example': suggestion.codeExample, - 'documentation_url': suggestion.documentationUrl, - }) - .toList(), - 'related_errors': error.relatedErrors, - 'timestamp': error.timestamp.toIso8601String(), - }) + .map( + (error) => { + 'id': error.id, + 'title': error.title, + 'description': error.description, + 'severity': error.severity.name, + 'category': error.category.name, + 'location': { + 'json_path': error.location.jsonPath, + 'line': error.location.line, + 'column': error.location.column, + 'snippet': error.location.snippet, + }, + 'suggestions': error.suggestions + .map( + (suggestion) => { + 'description': suggestion.description, + 'code_example': suggestion.codeExample, + 'documentation_url': suggestion.documentationUrl, + }, + ) + .toList(), + 'related_errors': error.relatedErrors, + 'timestamp': error.timestamp.toIso8601String(), + }, + ) .toList(), }; diff --git a/lib/core/error_rules.dart b/lib/core/error_rules.dart index 23cc2ee..2fbd43d 100644 --- a/lib/core/error_rules.dart +++ b/lib/core/error_rules.dart @@ -2,18 +2,10 @@ /// 定义常见的错误模式和修复建议 library; -import 'error_reporter.dart'; +import 'package:swagger_generator_flutter/core/error_reporter.dart'; /// 错误规则定义 class ErrorRule { - final String id; - final String pattern; - final ErrorSeverity severity; - final ErrorCategory category; - final String title; - final String description; - final List suggestions; - const ErrorRule({ required this.id, required this.pattern, @@ -23,6 +15,13 @@ class ErrorRule { required this.description, this.suggestions = const [], }); + final String id; + final String pattern; + final ErrorSeverity severity; + final ErrorCategory category; + final String title; + final String description; + final List suggestions; } /// OpenAPI 错误规则库 @@ -200,7 +199,7 @@ class OpenApiErrorRules { description: 'Consider using allOf or breaking the circular dependency', codeExample: - '"allOf": [{ "\$ref": "#/components/schemas/BaseModel" }]', + r'"allOf": [{ "$ref": "#/components/schemas/BaseModel" }]', ), ], ), @@ -327,7 +326,7 @@ class OpenApiErrorRules { FixSuggestion( description: 'Consider using composition with allOf', codeExample: - '"allOf": [{ "\$ref": "#/components/schemas/BaseModel" }, { "type": "object", "properties": {...} }]', + r'"allOf": [{ "$ref": "#/components/schemas/BaseModel" }, { "type": "object", "properties": {...} }]', ), ], ), @@ -390,7 +389,9 @@ class OpenApiErrorRules { /// 创建常见错误的快捷方法 static DetailedError createMissingFieldError( - String fieldPath, String fieldName) { + String fieldPath, + String fieldName, + ) { return DetailedError( id: 'MISSING_FIELD', title: 'Missing Required Field', @@ -408,7 +409,10 @@ class OpenApiErrorRules { } static DetailedError createInvalidTypeError( - String fieldPath, String expectedType, String actualType) { + String fieldPath, + String expectedType, + String actualType, + ) { return DetailedError( id: 'INVALID_TYPE', title: 'Invalid Field Type', @@ -427,7 +431,10 @@ class OpenApiErrorRules { } static DetailedError createUnknownFieldError( - String fieldPath, String fieldName, List validFields) { + String fieldPath, + String fieldName, + List validFields, + ) { return DetailedError( id: 'UNKNOWN_FIELD', title: 'Unknown Field', @@ -446,7 +453,9 @@ class OpenApiErrorRules { } static DetailedError createReferenceError( - String fieldPath, String reference) { + String fieldPath, + String reference, + ) { return DetailedError( id: 'INVALID_REFERENCE', title: 'Invalid Reference', @@ -461,7 +470,7 @@ class OpenApiErrorRules { ), const FixSuggestion( description: 'Verify the reference path is correct', - codeExample: '"\$ref": "#/components/schemas/ComponentName"', + codeExample: r'"$ref": "#/components/schemas/ComponentName"', ), ], ); diff --git a/lib/core/exceptions.dart b/lib/core/exceptions.dart index 8be7129..a54c059 100644 --- a/lib/core/exceptions.dart +++ b/lib/core/exceptions.dart @@ -2,12 +2,11 @@ import 'dart:io'; /// Swagger CLI 基础异常类 abstract class SwaggerException implements Exception { + SwaggerException(this.message, {this.details}) : timestamp = DateTime.now(); final String message; final String? details; final DateTime timestamp; - SwaggerException(this.message, {this.details}) : timestamp = DateTime.now(); - @override String toString() { if (details != null) { @@ -19,10 +18,6 @@ abstract class SwaggerException implements Exception { /// Swagger解析异常 class SwaggerParseException extends SwaggerException { - final String? url; - final int? statusCode; - final String? operation; - SwaggerParseException( super.message, { super.details, @@ -30,6 +25,9 @@ class SwaggerParseException extends SwaggerException { this.statusCode, this.operation, }); + final String? url; + final int? statusCode; + final String? operation; @override String toString() { @@ -58,10 +56,6 @@ class SwaggerParseException extends SwaggerException { /// 代码生成异常 class CodeGenerationException extends SwaggerException { - final String? generatorType; - final String? modelName; - final String? phase; - CodeGenerationException( super.message, { super.details, @@ -69,6 +63,9 @@ class CodeGenerationException extends SwaggerException { this.modelName, this.phase, }); + final String? generatorType; + final String? modelName; + final String? phase; @override String toString() { @@ -97,10 +94,6 @@ class CodeGenerationException extends SwaggerException { /// 文件操作异常 class FileOperationException extends SwaggerException { - final String? filePath; - final String? operation; - final int? errorCode; - FileOperationException( super.message, { super.details, @@ -108,6 +101,9 @@ class FileOperationException extends SwaggerException { this.operation, this.errorCode, }); + final String? filePath; + final String? operation; + final int? errorCode; @override String toString() { @@ -136,10 +132,6 @@ class FileOperationException extends SwaggerException { /// 命令异常 class CommandException extends SwaggerException { - final String? commandName; - final List? arguments; - final int? exitCode; - CommandException( super.message, { super.details, @@ -147,6 +139,9 @@ class CommandException extends SwaggerException { this.arguments, this.exitCode, }); + final String? commandName; + final List? arguments; + final int? exitCode; @override String toString() { @@ -175,10 +170,6 @@ class CommandException extends SwaggerException { /// 验证异常 class ValidationException extends SwaggerException { - final String? field; - final dynamic value; - final String? rule; - ValidationException( super.message, { super.details, @@ -186,6 +177,9 @@ class ValidationException extends SwaggerException { this.value, this.rule, }); + final String? field; + final dynamic value; + final String? rule; @override String toString() { @@ -214,10 +208,6 @@ class ValidationException extends SwaggerException { /// 配置异常 class ConfigurationException extends SwaggerException { - final String? configKey; - final dynamic configValue; - final String? source; - ConfigurationException( super.message, { super.details, @@ -225,6 +215,9 @@ class ConfigurationException extends SwaggerException { this.configValue, this.source, }); + final String? configKey; + final dynamic configValue; + final String? source; @override String toString() { @@ -253,11 +246,6 @@ class ConfigurationException extends SwaggerException { /// 网络异常 class NetworkException extends SwaggerException { - final String? url; - final int? statusCode; - final String? method; - final Duration? timeout; - NetworkException( super.message, { super.details, @@ -266,6 +254,10 @@ class NetworkException extends SwaggerException { this.method, this.timeout, }); + final String? url; + final int? statusCode; + final String? method; + final Duration? timeout; @override String toString() { @@ -298,10 +290,6 @@ class NetworkException extends SwaggerException { /// 缓存异常 class CacheException extends SwaggerException { - final String? cacheKey; - final String? operation; - final String? cacheType; - CacheException( super.message, { super.details, @@ -309,6 +297,9 @@ class CacheException extends SwaggerException { this.operation, this.cacheType, }); + final String? cacheKey; + final String? operation; + final String? cacheType; @override String toString() { @@ -337,10 +328,6 @@ class CacheException extends SwaggerException { /// 性能异常 class PerformanceException extends SwaggerException { - final String? operation; - final Duration? duration; - final Duration? threshold; - PerformanceException( super.message, { super.details, @@ -348,6 +335,9 @@ class PerformanceException extends SwaggerException { this.duration, this.threshold, }); + final String? operation; + final Duration? duration; + final Duration? threshold; @override String toString() { @@ -376,11 +366,6 @@ class PerformanceException extends SwaggerException { /// 类型异常 class TypeException extends SwaggerException { - final String? propertyName; - final String? expectedType; - final String? actualType; - final dynamic value; - TypeException( super.message, { super.details, @@ -389,6 +374,10 @@ class TypeException extends SwaggerException { this.actualType, this.value, }); + final String? propertyName; + final String? expectedType; + final String? actualType; + final dynamic value; @override String toString() { @@ -443,7 +432,7 @@ class ExceptionHandler { /// 默认异常处理 static void _defaultHandler(SwaggerException exception) { - print('🚨 异常: ${exception.toString()}'); + print('🚨 异常: $exception'); print('时间: ${exception.timestamp.toIso8601String()}'); print(''); } diff --git a/lib/core/models.dart b/lib/core/models.dart index 4ff2e61..b88b299 100644 --- a/lib/core/models.dart +++ b/lib/core/models.dart @@ -61,15 +61,6 @@ enum ModelUsageType { /// API服务器信息 (OpenAPI 3.0) class ApiServer { - /// 服务器URL - final String url; - - /// 服务器描述 - final String description; - - /// 服务器变量 - final Map variables; - const ApiServer({ required this.url, this.description = '', @@ -93,10 +84,36 @@ class ApiServer { variables: variables, ); } + + /// 服务器URL + final String url; + + /// 服务器描述 + final String description; + + /// 服务器变量 + final Map variables; } /// API服务器变量 (OpenAPI 3.0) class ApiServerVariable { + const ApiServerVariable({ + required this.defaultValue, + this.enumValues = const [], + this.description = '', + }); + + /// 从JSON创建ApiServerVariable + factory ApiServerVariable.fromJson(Map json) { + return ApiServerVariable( + enumValues: + (json['enum'] as List?)?.map((e) => e.toString()).toList() ?? + [], + defaultValue: json['default'] as String? ?? '', + description: json['description'] as String? ?? '', + ); + } + /// 变量的可选值 final List enumValues; @@ -105,21 +122,6 @@ class ApiServerVariable { /// 变量描述 final String description; - - const ApiServerVariable({ - this.enumValues = const [], - required this.defaultValue, - this.description = '', - }); - - /// 从JSON创建ApiServerVariable - factory ApiServerVariable.fromJson(Map json) { - return ApiServerVariable( - enumValues: List.from(json['enum'] ?? []), - defaultValue: json['default'] as String? ?? '', - description: json['description'] as String? ?? '', - ); - } } /// 属性类型枚举 @@ -211,43 +213,16 @@ enum ParameterLocation { /// OpenAPI 3.0 文档信息 /// 描述整个 API 的元数据和结构。 class SwaggerDocument { - /// 文档标题 - final String title; - - /// 版本号 - final String version; - - /// 文档描述 - final String description; - - /// 服务器配置 (OpenAPI 3.0) - final List servers; - - /// 可重用组件 (OpenAPI 3.0) - final ApiComponents components; - - /// 路径定义 - final Map paths; - - /// 数据模型定义 (从 components.schemas 提取) - final Map models; - - /// 控制器定义 - final Map controllers; - - /// 全局安全要求 - final List security; - /// 构造函数 const SwaggerDocument({ required this.title, required this.version, required this.description, - this.servers = const [], - this.components = const ApiComponents(), required this.paths, required this.models, required this.controllers, + this.servers = const [], + this.components = const ApiComponents(), this.security = const [], }); @@ -284,30 +259,65 @@ class SwaggerDocument { description: info['description'] as String? ?? '', servers: servers, components: components, - paths: {}, + paths: _parsePaths(json['paths'] as Map? ?? {}), models: components.schemas, // 从 components 中提取 schemas controllers: {}, security: security, ); } + + /// 文档标题 + final String title; + + /// 版本号 + final String version; + + /// 文档描述 + final String description; + + /// 服务器配置 (OpenAPI 3.0) + final List servers; + + /// 可重用组件 (OpenAPI 3.0) + final ApiComponents components; + + /// 路径定义 + final Map paths; + + /// 数据模型定义 (从 components.schemas 提取) + final Map models; + + /// 控制器定义 + final Map controllers; + + /// 全局安全要求 + final List security; + + /// 从JSON解析API路径 (静态辅助方法) + static Map _parsePaths(Map pathsJson) { + final paths = {}; + pathsJson.forEach((path, pathJson) { + final pathData = pathJson as Map; + pathData.forEach((method, methodJson) { + if (HttpMethod.values.any((m) => m.name == method)) { + final httpMethod = + HttpMethod.values.firstWhere((m) => m.name == method); + // This is a simplified parser for tests. It might overwrite paths if a path has multiple methods. + // The main parser in SwaggerDataParser handles this by creating unique keys. + paths[path] = ApiPath.fromJson( + path, + httpMethod, + methodJson as Map, + ); + } + }); + }); + return paths; + } } /// 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; - - /// 安全要求 - final List security; - const ApiPath({ required this.path, required this.method, @@ -325,16 +335,18 @@ class ApiPath { /// 从JSON创建ApiPath factory ApiPath.fromJson( String path, - String method, + HttpMethod method, Map json, ) { return ApiPath( path: path, - method: HttpMethod.fromString(method), + method: method, summary: json['summary'] as String? ?? '', description: json['description'] as String? ?? '', operationId: json['operationId'] as String? ?? '', - tags: List.from(json['tags'] ?? []), + tags: + (json['tags'] as List?)?.map((e) => e.toString()).toList() ?? + [], parameters: (json['parameters'] as List?) ?.map((p) => ApiParameter.fromJson(p as Map)) .toList() ?? @@ -351,25 +363,31 @@ class ApiPath { : null, deprecated: json['deprecated'] as bool? ?? false, security: (json['security'] as List?) - ?.map((s) => - ApiSecurityRequirement.fromJson(s as Map)) + ?.map( + (s) => + ApiSecurityRequirement.fromJson(s as Map), + ) .toList() ?? [], ); } + 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; + + /// 安全要求 + final List security; } /// 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, @@ -398,30 +416,18 @@ class ApiParameter { defaultValue: schema?['default'] ?? json['default'], ); } + final String name; + final ParameterLocation location; + final bool required; + final PropertyType type; + final String description; + final String? format; + final dynamic example; + final dynamic defaultValue; } /// API响应信息 (OpenAPI 3.0) class ApiResponse { - /// 响应状态码 - final String code; - - /// 响应描述 - final String description; - - /// 响应头定义 - final Map headers; - - /// 内容类型映射 (media type -> MediaTypeObject) - final Map content; - - /// 响应链接 - final Map links; - - /// Schema 定义 (Swagger 2.0 兼容,已弃用) - @Deprecated( - 'Use content instead. This field is for Swagger 2.0 compatibility only.') - final Map? schema; - const ApiResponse({ required this.code, required this.description, @@ -471,6 +477,27 @@ class ApiResponse { ); } + /// 响应状态码 + final String code; + + /// 响应描述 + final String description; + + /// 响应头定义 + final Map headers; + + /// 内容类型映射 (media type -> MediaTypeObject) + final Map content; + + /// 响应链接 + final Map links; + + /// Schema 定义 (Swagger 2.0 兼容,已弃用) + @Deprecated( + 'Use content instead. This field is for Swagger 2.0 compatibility only.', + ) + final Map? schema; + /// 获取支持的媒体类型列表 List get supportedMediaTypes => content.keys.toList(); @@ -491,15 +518,6 @@ class ApiResponse { /// API请求体信息 (OpenAPI 3.0) class ApiRequestBody { - /// 请求体描述 - final String description; - - /// 是否必需 - final bool required; - - /// 内容类型映射 (media type -> MediaTypeObject) - final Map content; - const ApiRequestBody({ this.description = '', this.required = false, @@ -524,6 +542,15 @@ class ApiRequestBody { ); } + /// 请求体描述 + final String description; + + /// 是否必需 + final bool required; + + /// 内容类型映射 (media type -> MediaTypeObject) + final Map content; + /// 获取支持的媒体类型列表 List get supportedMediaTypes => content.keys.toList(); @@ -747,24 +774,6 @@ extension MediaTypeExtension on MediaType { /// API媒体类型信息 (OpenAPI 3.0) class ApiMediaType { - /// Schema 定义 - final Map? schema; - - /// 示例数据 - final dynamic example; - - /// 多个示例数据 - final Map examples; - - /// 编码信息 (用于 multipart 和 form data) - final Map encoding; - - /// 媒体类型 - final MediaType mediaType; - - /// 原始媒体类型字符串 - final String rawMediaType; - const ApiMediaType({ this.schema, this.example, @@ -775,8 +784,10 @@ class ApiMediaType { }); /// 从JSON创建ApiMediaType - factory ApiMediaType.fromJson(Map json, - [String? contentType]) { + factory ApiMediaType.fromJson( + Map json, [ + String? contentType, + ]) { final examplesJson = json['examples'] as Map? ?? {}; final examples = {}; @@ -809,6 +820,24 @@ class ApiMediaType { ); } + /// Schema 定义 + final Map? schema; + + /// 示例数据 + final dynamic example; + + /// 多个示例数据 + final Map examples; + + /// 编码信息 (用于 multipart 和 form data) + final Map encoding; + + /// 媒体类型 + final MediaType mediaType; + + /// 原始媒体类型字符串 + final String rawMediaType; + /// 检查是否是 JSON 类型 bool get isJson => mediaType == MediaType.json; @@ -836,18 +865,6 @@ class ApiMediaType { /// API示例信息 (OpenAPI 3.0) class ApiExample { - /// 示例摘要 - final String summary; - - /// 示例描述 - final String description; - - /// 示例值 - final dynamic value; - - /// 外部示例URL - final String? externalValue; - const ApiExample({ this.summary = '', this.description = '', @@ -864,25 +881,22 @@ class ApiExample { externalValue: json['externalValue'] as String?, ); } + + /// 示例摘要 + final String summary; + + /// 示例描述 + final String description; + + /// 示例值 + final dynamic value; + + /// 外部示例URL + final String? externalValue; } /// API编码信息 (OpenAPI 3.0) class ApiEncoding { - /// 内容类型 - final String? contentType; - - /// 头部信息 - final Map headers; - - /// 样式 - final String? style; - - /// 是否展开 - final bool explode; - - /// 是否允许保留字符 - final bool allowReserved; - const ApiEncoding({ this.contentType, this.headers = const {}, @@ -911,6 +925,21 @@ class ApiEncoding { ); } + /// 内容类型 + final String? contentType; + + /// 头部信息 + final Map headers; + + /// 样式 + final String? style; + + /// 是否展开 + final bool explode; + + /// 是否允许保留字符 + final bool allowReserved; + /// 检查是否是文件类型 bool get isFile { if (contentType == null) return false; @@ -947,21 +976,6 @@ class ApiEncoding { /// API头部信息 (OpenAPI 3.0) class ApiHeader { - /// 头部描述 - final String description; - - /// 是否必需 - final bool required; - - /// 是否已弃用 - final bool deprecated; - - /// Schema 定义 - final Map? schema; - - /// 示例值 - final dynamic example; - const ApiHeader({ this.description = '', this.required = false, @@ -980,28 +994,25 @@ class ApiHeader { example: json['example'], ); } + + /// 头部描述 + final String description; + + /// 是否必需 + final bool required; + + /// 是否已弃用 + final bool deprecated; + + /// Schema 定义 + final Map? schema; + + /// 示例值 + final dynamic example; } /// API链接信息 (OpenAPI 3.0) class ApiLink { - /// 链接描述 - final String description; - - /// 操作引用 - final String? operationRef; - - /// 操作ID - final String? operationId; - - /// 参数映射 - final Map parameters; - - /// 请求体映射 - final dynamic requestBody; - - /// 服务器信息 - final ApiServer? server; - const ApiLink({ this.description = '', this.operationRef, @@ -1025,38 +1036,29 @@ class ApiLink { server: server, ); } + + /// 链接描述 + final String description; + + /// 操作引用 + final String? operationRef; + + /// 操作ID + final String? operationId; + + /// 参数映射 + final Map parameters; + + /// 请求体映射 + final dynamic requestBody; + + /// 服务器信息 + final ApiServer? server; } /// API组件信息 (OpenAPI 3.0) /// 包含可重用的组件定义 class ApiComponents { - /// Schema 定义 - final Map schemas; - - /// 响应定义 - final Map responses; - - /// 参数定义 - final Map parameters; - - /// 示例定义 - final Map examples; - - /// 请求体定义 - final Map requestBodies; - - /// 头部定义 - final Map headers; - - /// 安全方案定义 - final Map securitySchemes; - - /// 链接定义 - final Map links; - - /// 回调定义 - final Map callbacks; - const ApiComponents({ this.schemas = const {}, this.responses = const {}, @@ -1166,10 +1168,70 @@ class ApiComponents { callbacks: callbacks, ); } + + /// Schema 定义 + final Map schemas; + + /// 响应定义 + final Map responses; + + /// 参数定义 + final Map parameters; + + /// 示例定义 + final Map examples; + + /// 请求体定义 + final Map requestBodies; + + /// 头部定义 + final Map headers; + + /// 安全方案定义 + final Map securitySchemes; + + /// 链接定义 + final Map links; + + /// 回调定义 + final Map callbacks; } /// API安全方案信息 (OpenAPI 3.0) class ApiSecurityScheme { + const ApiSecurityScheme({ + required this.type, + this.description = '', + this.name, + this.location, + this.scheme, + this.bearerFormat, + this.flows, + this.openIdConnectUrl, + }); + + /// 从JSON创建ApiSecurityScheme + factory ApiSecurityScheme.fromJson(Map json) { + final type = SecuritySchemeTypeExtension.fromString( + json['type'] as String? ?? 'apiKey', + ); + + return ApiSecurityScheme( + type: type, + description: json['description'] as String? ?? '', + name: json['name'] as String?, + location: json['in'] != null + ? ApiKeyLocationExtension.fromString(json['in'] as String) + : null, + scheme: json['scheme'] as String?, + bearerFormat: json['bearerFormat'] as String?, + flows: json['flows'] != null + ? OAuth2Flows.fromJson(json['flows'] as Map) + : null, + openIdConnectUrl: json['openIdConnectUrl'] as String?, + ); + } + /// 安全方案类型 final SecuritySchemeType type; @@ -1194,38 +1256,6 @@ class ApiSecurityScheme { /// OpenID Connect URL (用于 openIdConnect) final String? openIdConnectUrl; - const ApiSecurityScheme({ - required this.type, - this.description = '', - this.name, - this.location, - this.scheme, - this.bearerFormat, - this.flows, - this.openIdConnectUrl, - }); - - /// 从JSON创建ApiSecurityScheme - factory ApiSecurityScheme.fromJson(Map json) { - final type = SecuritySchemeTypeExtension.fromString( - json['type'] as String? ?? 'apiKey'); - - return ApiSecurityScheme( - type: type, - description: json['description'] as String? ?? '', - name: json['name'] as String?, - location: json['in'] != null - ? ApiKeyLocationExtension.fromString(json['in'] as String) - : null, - scheme: json['scheme'] as String?, - bearerFormat: json['bearerFormat'] as String?, - flows: json['flows'] != null - ? OAuth2Flows.fromJson(json['flows'] as Map) - : null, - openIdConnectUrl: json['openIdConnectUrl'] as String?, - ); - } - /// 检查是否是 API Key 认证 bool get isApiKey => type == SecuritySchemeType.apiKey; @@ -1245,7 +1275,7 @@ class ApiSecurityScheme { bool get isBasic => isHttp && scheme?.toLowerCase() == 'basic'; /// 检查是否有 OAuth2 流程 - bool get hasOAuth2Flows => flows?.hasAnyFlow == true; + bool get hasOAuth2Flows => flows?.hasAnyFlow ?? false; /// 获取 API Key 的完整配置信息 String get apiKeyInfo { @@ -1265,9 +1295,6 @@ class ApiSecurityScheme { /// API回调信息 (OpenAPI 3.0) class ApiCallback { - /// 回调表达式映射 - final Map expressions; - const ApiCallback({ this.expressions = const {}, }); @@ -1286,25 +1313,13 @@ class ApiCallback { expressions: expressions, ); } + + /// 回调表达式映射 + final Map expressions; } /// API路径项信息 (OpenAPI 3.0) class ApiPathItem { - /// 路径摘要 - final String summary; - - /// 路径描述 - final String description; - - /// 操作映射 (HTTP方法 -> 操作) - final Map operations; - - /// 服务器信息 - final List servers; - - /// 参数信息 - final List parameters; - const ApiPathItem({ this.summary = '', this.description = '', @@ -1324,7 +1339,7 @@ class ApiPathItem { 'options', 'head', 'patch', - 'trace' + 'trace', ]; for (final method in httpMethods) { @@ -1352,34 +1367,25 @@ class ApiPathItem { parameters: parameters, ); } + + /// 路径摘要 + final String summary; + + /// 路径描述 + final String description; + + /// 操作映射 (HTTP方法 -> 操作) + final Map operations; + + /// 服务器信息 + final List servers; + + /// 参数信息 + final List parameters; } /// API操作信息 (OpenAPI 3.0) class ApiOperation { - /// 操作摘要 - final String summary; - - /// 操作描述 - final String description; - - /// 操作ID - final String? operationId; - - /// 标签 - final List tags; - - /// 参数 - final List parameters; - - /// 请求体 - final ApiRequestBody? requestBody; - - /// 响应 - final Map responses; - - /// 安全要求 - final List security; - const ApiOperation({ this.summary = '', this.description = '', @@ -1420,24 +1426,44 @@ class ApiOperation { summary: json['summary'] as String? ?? '', description: json['description'] as String? ?? '', operationId: json['operationId'] as String?, - tags: List.from(json['tags'] ?? []), + tags: + (json['tags'] as List?)?.map((e) => e.toString()).toList() ?? + [], parameters: parameters, requestBody: requestBody, responses: responses, security: security, ); } + + /// 操作摘要 + final String summary; + + /// 操作描述 + final String description; + + /// 操作ID + final String? operationId; + + /// 标签 + final List tags; + + /// 参数 + final List parameters; + + /// 请求体 + final ApiRequestBody? requestBody; + + /// 响应 + final Map responses; + + /// 安全要求 + final List security; } /// API 判别器信息 (OpenAPI 3.0) /// 用于多态类型的判别 class ApiDiscriminator { - /// 判别器属性名 - final String propertyName; - - /// 映射表 (值 -> schema 引用) - final Map mapping; - const ApiDiscriminator({ required this.propertyName, this.mapping = const {}, @@ -1460,6 +1486,12 @@ class ApiDiscriminator { ); } + /// 判别器属性名 + final String propertyName; + + /// 映射表 (值 -> schema 引用) + final Map mapping; + /// 检查是否有映射表 bool get hasMapping => mapping.isNotEmpty; @@ -1470,6 +1502,166 @@ class ApiDiscriminator { /// API Schema 信息 (OpenAPI 3.0) /// 表示一个 JSON Schema 对象,支持组合模式 class ApiSchema { + const ApiSchema({ + this.type, + this.format, + this.description = '', + this.properties = const {}, + this.required = const [], + this.items, + this.reference, + this.enumValues = const [], + this.allOf = const [], + this.oneOf = const [], + this.anyOf = const [], + this.not, + this.discriminator, + this.additionalProperties, + this.patternProperties = const {}, + this.propertyNames, + this.dependencies = const {}, + this.constValue, + this.ifSchema, + this.thenSchema, + this.elseSchema, + this.minimum, + this.maximum, + this.exclusiveMinimum, + this.exclusiveMaximum, + this.minLength, + this.maxLength, + this.pattern, + this.minItems, + this.maxItems, + this.uniqueItems, + this.nullable = false, + this.example, + this.defaultValue, + }); + + /// 从JSON创建ApiSchema + factory ApiSchema.fromJson(Map json) { + // 解析 properties + final propertiesJson = json['properties'] as Map? ?? {}; + final properties = {}; + final requiredFields = (json['required'] as List?) + ?.map((e) => e.toString()) + .toList() ?? + []; + + propertiesJson.forEach((propName, propData) { + if (propData is Map) { + properties[propName] = ApiProperty.fromJson( + propName, + propData, + requiredFields, + ); + } + }); + + // 解析 items (用于数组类型) + final itemsJson = json['items'] as Map?; + final items = itemsJson != null ? ApiSchema.fromJson(itemsJson) : null; + + // 解析组合模式 + final allOfJson = json['allOf'] as List? ?? []; + final allOf = allOfJson + .map((schema) => ApiSchema.fromJson(schema as Map)) + .toList(); + + final oneOfJson = json['oneOf'] as List? ?? []; + final oneOf = oneOfJson + .map((schema) => ApiSchema.fromJson(schema as Map)) + .toList(); + + final anyOfJson = json['anyOf'] as List? ?? []; + final anyOf = anyOfJson + .map((schema) => ApiSchema.fromJson(schema as Map)) + .toList(); + + final notJson = json['not'] as Map?; + final not = notJson != null ? ApiSchema.fromJson(notJson) : null; + + // 解析 discriminator + final discriminatorJson = json['discriminator'] as Map?; + final discriminator = discriminatorJson != null + ? ApiDiscriminator.fromJson(discriminatorJson) + : null; + + // 解析 patternProperties + final patternPropertiesJson = + json['patternProperties'] as Map? ?? {}; + final patternProperties = {}; + patternPropertiesJson.forEach((pattern, schemaData) { + if (schemaData is Map) { + patternProperties[pattern] = ApiSchema.fromJson(schemaData); + } + }); + + // 解析 propertyNames + final propertyNamesJson = json['propertyNames'] as Map?; + final propertyNames = propertyNamesJson != null + ? ApiSchema.fromJson(propertyNamesJson) + : null; + + // 解析 dependencies + final dependencies = json['dependencies'] as Map? ?? {}; + + // 解析条件 Schema (if/then/else) + final ifJson = json['if'] as Map?; + final ifSchema = ifJson != null ? ApiSchema.fromJson(ifJson) : null; + + final thenJson = json['then'] as Map?; + final thenSchema = thenJson != null ? ApiSchema.fromJson(thenJson) : null; + + final elseJson = json['else'] as Map?; + final elseSchema = elseJson != null ? ApiSchema.fromJson(elseJson) : null; + + // 处理引用 + String? reference; + if (json[r'$ref'] != null) { + final ref = json[r'$ref'] as String; + reference = ref.split('/').last; + } + + return ApiSchema( + type: json['type'] as String?, + format: json['format'] as String?, + description: json['description'] as String? ?? '', + properties: properties, + required: requiredFields, + items: items, + reference: reference, + enumValues: (json['enum'] as List?) ?? [], + allOf: allOf, + oneOf: oneOf, + anyOf: anyOf, + not: not, + discriminator: discriminator, + additionalProperties: json['additionalProperties'], + patternProperties: patternProperties, + propertyNames: propertyNames, + dependencies: dependencies, + constValue: json['const'], + ifSchema: ifSchema, + thenSchema: thenSchema, + elseSchema: elseSchema, + minimum: json['minimum'] as num?, + maximum: json['maximum'] as num?, + exclusiveMinimum: json['exclusiveMinimum'] as bool?, + exclusiveMaximum: json['exclusiveMaximum'] as bool?, + minLength: json['minLength'] as int?, + maxLength: json['maxLength'] as int?, + pattern: json['pattern'] as String?, + minItems: json['minItems'] as int?, + maxItems: json['maxItems'] as int?, + uniqueItems: json['uniqueItems'] as bool?, + nullable: json['nullable'] as bool? ?? false, + example: json['example'], + defaultValue: json['default'], + ); + } + /// Schema 类型 final String? type; @@ -1548,163 +1740,6 @@ class ApiSchema { /// 默认值 final dynamic defaultValue; - const ApiSchema({ - this.type, - this.format, - this.description = '', - this.properties = const {}, - this.required = const [], - this.items, - this.reference, - this.enumValues = const [], - this.allOf = const [], - this.oneOf = const [], - this.anyOf = const [], - this.not, - this.discriminator, - this.additionalProperties, - this.patternProperties = const {}, - this.propertyNames, - this.dependencies = const {}, - this.constValue, - this.ifSchema, - this.thenSchema, - this.elseSchema, - this.minimum, - this.maximum, - this.exclusiveMinimum, - this.exclusiveMaximum, - this.minLength, - this.maxLength, - this.pattern, - this.minItems, - this.maxItems, - this.uniqueItems, - this.nullable = false, - this.example, - this.defaultValue, - }); - - /// 从JSON创建ApiSchema - factory ApiSchema.fromJson(Map json) { - // 解析 properties - final propertiesJson = json['properties'] as Map? ?? {}; - final properties = {}; - final requiredFields = List.from(json['required'] ?? []); - - propertiesJson.forEach((propName, propData) { - if (propData is Map) { - properties[propName] = ApiProperty.fromJson( - propName, - propData, - requiredFields, - ); - } - }); - - // 解析 items (用于数组类型) - final itemsJson = json['items'] as Map?; - final items = itemsJson != null ? ApiSchema.fromJson(itemsJson) : null; - - // 解析组合模式 - final allOfJson = json['allOf'] as List? ?? []; - final allOf = allOfJson - .map((schema) => ApiSchema.fromJson(schema as Map)) - .toList(); - - final oneOfJson = json['oneOf'] as List? ?? []; - final oneOf = oneOfJson - .map((schema) => ApiSchema.fromJson(schema as Map)) - .toList(); - - final anyOfJson = json['anyOf'] as List? ?? []; - final anyOf = anyOfJson - .map((schema) => ApiSchema.fromJson(schema as Map)) - .toList(); - - final notJson = json['not'] as Map?; - final not = notJson != null ? ApiSchema.fromJson(notJson) : null; - - // 解析 discriminator - final discriminatorJson = json['discriminator'] as Map?; - final discriminator = discriminatorJson != null - ? ApiDiscriminator.fromJson(discriminatorJson) - : null; - - // 解析 patternProperties - final patternPropertiesJson = - json['patternProperties'] as Map? ?? {}; - final patternProperties = {}; - patternPropertiesJson.forEach((pattern, schemaData) { - if (schemaData is Map) { - patternProperties[pattern] = ApiSchema.fromJson(schemaData); - } - }); - - // 解析 propertyNames - final propertyNamesJson = json['propertyNames'] as Map?; - final propertyNames = propertyNamesJson != null - ? ApiSchema.fromJson(propertyNamesJson) - : null; - - // 解析 dependencies - final dependencies = json['dependencies'] as Map? ?? {}; - - // 解析条件 Schema (if/then/else) - final ifJson = json['if'] as Map?; - final ifSchema = ifJson != null ? ApiSchema.fromJson(ifJson) : null; - - final thenJson = json['then'] as Map?; - final thenSchema = thenJson != null ? ApiSchema.fromJson(thenJson) : null; - - final elseJson = json['else'] as Map?; - final elseSchema = elseJson != null ? ApiSchema.fromJson(elseJson) : null; - - // 处理引用 - String? reference; - if (json['\$ref'] != null) { - final ref = json['\$ref'] as String; - reference = ref.split('/').last; - } - - return ApiSchema( - type: json['type'] as String?, - format: json['format'] as String?, - description: json['description'] as String? ?? '', - properties: properties, - required: requiredFields, - items: items, - reference: reference, - enumValues: List.from(json['enum'] ?? []), - allOf: allOf, - oneOf: oneOf, - anyOf: anyOf, - not: not, - discriminator: discriminator, - additionalProperties: json['additionalProperties'], - patternProperties: patternProperties, - propertyNames: propertyNames, - dependencies: dependencies, - constValue: json['const'], - ifSchema: ifSchema, - thenSchema: thenSchema, - elseSchema: elseSchema, - minimum: json['minimum'] as num?, - maximum: json['maximum'] as num?, - exclusiveMinimum: json['exclusiveMinimum'] as bool?, - exclusiveMaximum: json['exclusiveMaximum'] as bool?, - minLength: json['minLength'] as int?, - maxLength: json['maxLength'] as int?, - pattern: json['pattern'] as String?, - minItems: json['minItems'] as int?, - maxItems: json['maxItems'] as int?, - uniqueItems: json['uniqueItems'] as bool?, - nullable: json['nullable'] as bool? ?? false, - example: json['example'], - defaultValue: json['default'], - ); - } - /// 检查是否是组合模式 bool get isComposition => allOf.isNotEmpty || oneOf.isNotEmpty || anyOf.isNotEmpty; @@ -1770,27 +1805,6 @@ class ApiSchema { /// API模型信息 class ApiModel { - final String name; - final String description; - final Map properties; - final List required; - final bool isEnum; - final List enumValues; - final PropertyType? enumType; - - /// 组合模式支持 (OpenAPI 3.0) - final List allOf; - final List oneOf; - final List anyOf; - final ApiSchema? not; - - /// 多态类型判别器 (OpenAPI 3.0) - final ApiDiscriminator? discriminator; - - /// 模型用途类型 - /// 标识该模型在 API 中的实际用途(请求/响应/通用/未知) - final ModelUsageType usageType; - const ApiModel({ required this.name, required this.description, @@ -1814,11 +1828,15 @@ class ApiModel { ModelUsageType usageType = ModelUsageType.unknown, }) { final isEnum = json['enum'] != null; - final enumValues = isEnum ? List.from(json['enum']) : []; + final enumValues = + isEnum ? (json['enum'] as List?) ?? [] : []; final properties = json['properties'] as Map? ?? {}; List required; if (json.containsKey('required')) { - required = List.from(json['required']); + required = (json['required'] as List?) + ?.map((e) => e.toString()) + .toList() ?? + []; } else { // 没有 required 字段时,凡 nullable != true 的都视为 required required = properties.entries @@ -1879,6 +1897,26 @@ class ApiModel { ), ); } + final String name; + final String description; + final Map properties; + final List required; + final bool isEnum; + final List enumValues; + final PropertyType? enumType; + + /// 组合模式支持 (OpenAPI 3.0) + final List allOf; + final List oneOf; + final List anyOf; + final ApiSchema? not; + + /// 多态类型判别器 (OpenAPI 3.0) + final ApiDiscriminator? discriminator; + + /// 模型用途类型 + /// 标识该模型在 API 中的实际用途(请求/响应/通用/未知) + final ModelUsageType usageType; /// 检查是否使用了组合模式 bool get isComposition => @@ -1922,32 +1960,12 @@ class ApiModel { /// 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; // 用于数组类型 - - /// 嵌套对象属性 (用于 object 类型) - final Map nestedProperties; - - /// 嵌套对象的必需字段 - final List nestedRequired; - - /// Schema 定义 (用于复杂类型) - final ApiSchema? schema; - const ApiProperty({ required this.name, required this.type, - this.format, required this.description, required this.required, + this.format, this.nullable = false, this.example, this.defaultValue, @@ -1979,13 +1997,13 @@ class ApiProperty { final type = PropertyType.fromString(json['type'] as String? ?? 'string'); String? reference; ApiModel? items; - final Map nestedProperties = {}; - List nestedRequired = []; + final nestedProperties = {}; + var nestedRequired = []; ApiSchema? schema; // 处理引用类型 - if (json['\$ref'] != null) { - final ref = json['\$ref'] as String; + if (json[r'$ref'] != null) { + final ref = json[r'$ref'] as String; reference = ref.split('/').last; } @@ -1999,7 +2017,10 @@ class ApiProperty { // 处理嵌套对象类型 if (type == PropertyType.object && json['properties'] != null) { final propertiesJson = json['properties'] as Map; - nestedRequired = List.from(json['required'] ?? []); + nestedRequired = (json['required'] as List?) + ?.map((e) => e.toString()) + .toList() ?? + []; propertiesJson.forEach((propName, propData) { if (propData is Map) { @@ -2030,21 +2051,23 @@ class ApiProperty { final itemsJson = json['items'] as Map; // 如果 items 是引用类型 - if (itemsJson['\$ref'] != null) { - final itemRef = itemsJson['\$ref'] as String; + if (itemsJson[r'$ref'] != null) { + final itemRef = itemsJson[r'$ref'] as String; final itemRefName = itemRef.split('/').last; items = ApiModel( name: itemRefName, description: '', properties: {}, required: [], - isEnum: false, ); } else if (itemsJson['type'] == 'object' && itemsJson['properties'] != null) { // 如果 items 是嵌套对象 final itemProperties = {}; - final itemRequired = List.from(itemsJson['required'] ?? []); + final itemRequired = (itemsJson['required'] as List?) + ?.map((e) => e.toString()) + .toList() ?? + []; final itemPropertiesJson = itemsJson['properties'] as Map; @@ -2086,7 +2109,6 @@ class ApiProperty { description: '', properties: {}, required: [], - isEnum: false, ); } } @@ -2107,6 +2129,25 @@ class ApiProperty { schema: schema, ); } + 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; // 用于数组类型 + + /// 嵌套对象属性 (用于 object 类型) + final Map nestedProperties; + + /// 嵌套对象的必需字段 + final List nestedRequired; + + /// Schema 定义 (用于复杂类型) + final ApiSchema? schema; /// 检查是否有嵌套属性 bool get hasNestedProperties => nestedProperties.isNotEmpty; @@ -2117,10 +2158,6 @@ class ApiProperty { /// API控制器信息 class ApiController { - final String name; - final String description; - final List paths; - const ApiController({ required this.name, required this.description, @@ -2131,6 +2168,9 @@ class ApiController { factory ApiController.fromPaths(String name, List paths) { return ApiController(name: name, description: name, paths: paths); } + final String name; + final String description; + final List paths; } /// 安全方案类型 @@ -2244,18 +2284,6 @@ extension OAuth2FlowTypeExtension on OAuth2FlowType { /// OAuth2 流程配置 class OAuth2Flow { - /// 授权 URL (用于 authorizationCode 和 implicit 流程) - final String? authorizationUrl; - - /// 令牌 URL (用于 authorizationCode, password 和 clientCredentials 流程) - final String? tokenUrl; - - /// 刷新 URL (可选) - final String? refreshUrl; - - /// 可用的作用域 - final Map scopes; - const OAuth2Flow({ this.authorizationUrl, this.tokenUrl, @@ -2283,6 +2311,18 @@ class OAuth2Flow { ); } + /// 授权 URL (用于 authorizationCode 和 implicit 流程) + final String? authorizationUrl; + + /// 令牌 URL (用于 authorizationCode, password 和 clientCredentials 流程) + final String? tokenUrl; + + /// 刷新 URL (可选) + final String? refreshUrl; + + /// 可用的作用域 + final Map scopes; + /// 检查是否有授权 URL bool get hasAuthorizationUrl => authorizationUrl != null && authorizationUrl!.isNotEmpty; @@ -2299,18 +2339,6 @@ class OAuth2Flow { /// OAuth2 流程集合 class OAuth2Flows { - /// 授权码流程 - final OAuth2Flow? authorizationCode; - - /// 隐式流程 - final OAuth2Flow? implicit; - - /// 密码流程 - final OAuth2Flow? password; - - /// 客户端凭证流程 - final OAuth2Flow? clientCredentials; - const OAuth2Flows({ this.authorizationCode, this.implicit, @@ -2323,7 +2351,8 @@ class OAuth2Flows { return OAuth2Flows( authorizationCode: json['authorizationCode'] != null ? OAuth2Flow.fromJson( - json['authorizationCode'] as Map) + json['authorizationCode'] as Map, + ) : null, implicit: json['implicit'] != null ? OAuth2Flow.fromJson(json['implicit'] as Map) @@ -2333,11 +2362,24 @@ class OAuth2Flows { : null, clientCredentials: json['clientCredentials'] != null ? OAuth2Flow.fromJson( - json['clientCredentials'] as Map) + json['clientCredentials'] as Map, + ) : null, ); } + /// 授权码流程 + final OAuth2Flow? authorizationCode; + + /// 隐式流程 + final OAuth2Flow? implicit; + + /// 密码流程 + final OAuth2Flow? password; + + /// 客户端凭证流程 + final OAuth2Flow? clientCredentials; + /// 获取所有可用的流程 List get availableFlows { final flows = []; @@ -2368,9 +2410,6 @@ class OAuth2Flows { /// 安全要求 (单个安全方案的要求) class ApiSecurityRequirement { - /// 安全方案名称到作用域列表的映射 - final Map> requirements; - const ApiSecurityRequirement({ this.requirements = const {}, }); @@ -2390,6 +2429,9 @@ class ApiSecurityRequirement { return ApiSecurityRequirement(requirements: requirements); } + /// 安全方案名称到作用域列表的映射 + final Map> requirements; + /// 检查是否为空 bool get isEmpty => requirements.isEmpty; diff --git a/lib/core/performance_parser.dart b/lib/core/performance_parser.dart index a485c88..91b5da1 100644 --- a/lib/core/performance_parser.dart +++ b/lib/core/performance_parser.dart @@ -4,19 +4,10 @@ library; import 'dart:async'; import 'dart:convert'; -import 'models.dart'; +import 'package:swagger_generator_flutter/core/models.dart'; /// 解析性能统计 class ParsePerformanceStats { - final Duration totalTime; - final Duration parseTime; - final Duration validationTime; - final Duration modelCreationTime; - final int memoryUsage; - final int documentSize; - final int pathCount; - final int schemaCount; - const ParsePerformanceStats({ required this.totalTime, required this.parseTime, @@ -27,6 +18,14 @@ class ParsePerformanceStats { required this.pathCount, required this.schemaCount, }); + final Duration totalTime; + final Duration parseTime; + final Duration validationTime; + final Duration modelCreationTime; + final int memoryUsage; + final int documentSize; + final int pathCount; + final int schemaCount; double get pathsPerSecond => pathCount / totalTime.inMilliseconds * 1000; double get schemasPerSecond => schemaCount / totalTime.inMilliseconds * 1000; @@ -51,6 +50,17 @@ Performance Statistics: /// 解析配置 class ParseConfig { + const ParseConfig({ + this.enableParallelParsing = true, + this.enableStreamParsing = false, + this.enableIncrementalParsing = false, + this.enableCaching = true, + this.maxConcurrency = 4, + this.streamBufferSize = 8192, + this.enablePerformanceStats = false, + this.enableMemoryOptimization = true, + }); + /// 是否启用并行解析 final bool enableParallelParsing; @@ -74,28 +84,16 @@ class ParseConfig { /// 是否启用内存优化 final bool enableMemoryOptimization; - - const ParseConfig({ - this.enableParallelParsing = true, - this.enableStreamParsing = false, - this.enableIncrementalParsing = false, - this.enableCaching = true, - this.maxConcurrency = 4, - this.streamBufferSize = 8192, - this.enablePerformanceStats = false, - this.enableMemoryOptimization = true, - }); } /// 高性能解析器 class PerformanceParser { + PerformanceParser({ParseConfig? config}) + : _config = config ?? const ParseConfig(); final ParseConfig _config; final Map _cache = {}; ParsePerformanceStats? _lastStats; - PerformanceParser({ParseConfig? config}) - : _config = config ?? const ParseConfig(); - /// 获取最后一次解析的性能统计 ParsePerformanceStats? get lastStats => _lastStats; @@ -165,7 +163,7 @@ class PerformanceParser { // 模拟流式解析(实际实现会更复杂) final chunks = []; - for (int i = 0; i < jsonString.length; i += _config.streamBufferSize) { + for (var i = 0; i < jsonString.length; i += _config.streamBufferSize) { final end = (i + _config.streamBufferSize).clamp(0, jsonString.length); chunks.add(jsonString.substring(i, end)); } @@ -196,25 +194,31 @@ class PerformanceParser { /// 并行解析文档 Future _parseDocumentParallel( - Map json) async { - final futures = []; + Map json, + ) async { + final futures = >[]; final results = {}; // 并行解析不同部分 if (json.containsKey('paths')) { - futures.add(_parsePathsParallel(json['paths'] as Map) - .then((paths) => results['paths'] = paths)); + futures.add( + _parsePathsParallel(json['paths'] as Map) + .then((paths) => results['paths'] = paths), + ); } if (json.containsKey('components')) { futures.add( - _parseComponentsParallel(json['components'] as Map) - .then((components) => results['components'] = components)); + _parseComponentsParallel(json['components'] as Map) + .then((components) => results['components'] = components), + ); } if (json.containsKey('servers')) { - futures.add(_parseServersParallel(json['servers'] as List) - .then((servers) => results['servers'] = servers)); + futures.add( + _parseServersParallel(json['servers'] as List) + .then((servers) => results['servers'] = servers), + ); } // 等待所有并行任务完成 @@ -229,14 +233,15 @@ class PerformanceParser { /// 并行解析路径 Future> _parsePathsParallel( - Map pathsJson) async { + Map pathsJson, + ) async { if (pathsJson.length <= _config.maxConcurrency) { // 如果路径数量较少,直接解析 return _parsePathsSequential(pathsJson); } final chunks = _chunkMap(pathsJson, _config.maxConcurrency); - final futures = chunks.map((chunk) => _parsePathChunk(chunk)); + final futures = chunks.map(_parsePathChunk); final results = await Future.wait(futures); // 合并结果 @@ -250,20 +255,25 @@ class PerformanceParser { /// 并行解析组件 Future _parseComponentsParallel( - Map componentsJson) async { - final futures = []; + Map componentsJson, + ) async { + final futures = >[]; final results = {}; if (componentsJson.containsKey('schemas')) { - futures.add(_parseSchemasParallel( - componentsJson['schemas'] as Map) - .then((schemas) => results['schemas'] = schemas)); + futures.add( + _parseSchemasParallel( + componentsJson['schemas'] as Map, + ).then((schemas) => results['schemas'] = schemas), + ); } if (componentsJson.containsKey('securitySchemes')) { - futures.add(_parseSecuritySchemesParallel( - componentsJson['securitySchemes'] as Map) - .then((schemes) => results['securitySchemes'] = schemes)); + futures.add( + _parseSecuritySchemesParallel( + componentsJson['securitySchemes'] as Map, + ).then((schemes) => results['securitySchemes'] = schemes), + ); } await Future.wait(futures); @@ -276,7 +286,8 @@ class PerformanceParser { /// 并行解析服务器 Future> _parseServersParallel( - List serversJson) async { + List serversJson, + ) async { if (serversJson.length <= _config.maxConcurrency) { return serversJson .map((json) => ApiServer.fromJson(json as Map)) @@ -284,7 +295,7 @@ class PerformanceParser { } final chunks = _chunkList(serversJson, _config.maxConcurrency); - final futures = chunks.map((chunk) => _parseServerChunk(chunk)); + final futures = chunks.map(_parseServerChunk); final results = await Future.wait(futures); // 合并结果 @@ -298,7 +309,8 @@ class PerformanceParser { /// 解析路径块 Future> _parsePathChunk( - Map pathChunk) async { + Map pathChunk, + ) async { return _parsePathsSequential(pathChunk); } @@ -318,8 +330,11 @@ class PerformanceParser { pathData.forEach((method, operationData) { if (operationData is Map) { try { - final apiPath = - ApiPath.fromJson(pathPattern, method, operationData); + final apiPath = ApiPath.fromJson( + pathPattern, + HttpMethod.fromString(method), + operationData, + ); paths[pathPattern] = apiPath; } catch (e) { // 忽略解析错误的路径 @@ -334,13 +349,14 @@ class PerformanceParser { /// 并行解析 Schemas Future> _parseSchemasParallel( - Map schemasJson) async { + Map schemasJson, + ) async { if (schemasJson.length <= _config.maxConcurrency) { return _parseSchemasSequential(schemasJson); } final chunks = _chunkMap(schemasJson, _config.maxConcurrency); - final futures = chunks.map((chunk) => _parseSchemaChunk(chunk)); + final futures = chunks.map(_parseSchemaChunk); final results = await Future.wait(futures); // 合并结果 @@ -354,7 +370,8 @@ class PerformanceParser { /// 并行解析安全方案 Future> _parseSecuritySchemesParallel( - Map schemesJson) async { + Map schemesJson, + ) async { final schemes = {}; schemesJson.forEach((name, schemeData) { @@ -373,13 +390,15 @@ class PerformanceParser { /// 解析 Schema 块 Future> _parseSchemaChunk( - Map schemaChunk) async { + Map schemaChunk, + ) async { return _parseSchemasSequential(schemaChunk); } /// 顺序解析 Schemas Map _parseSchemasSequential( - Map schemasJson) { + Map schemasJson, + ) { final schemas = {}; schemasJson.forEach((name, schemaData) { @@ -400,12 +419,14 @@ class PerformanceParser { void _validateBasicStructure(Map json) { if (!json.containsKey('openapi') && !json.containsKey('swagger')) { throw const FormatException( - 'Invalid OpenAPI document: missing version field'); + 'Invalid OpenAPI document: missing version field', + ); } if (!json.containsKey('info')) { throw const FormatException( - 'Invalid OpenAPI document: missing info object'); + 'Invalid OpenAPI document: missing info object', + ); } final info = json['info'] as Map?; @@ -413,21 +434,24 @@ class PerformanceParser { !info.containsKey('title') || !info.containsKey('version')) { throw const FormatException( - 'Invalid OpenAPI document: info object must contain title and version'); + 'Invalid OpenAPI document: info object must contain title and version', + ); } } /// 将 Map 分块 List> _chunkMap( - Map map, int chunkSize) { + Map map, + int chunkSize, + ) { final chunks = >[]; final entries = map.entries.toList(); - for (int i = 0; i < entries.length; i += chunkSize) { + for (var i = 0; i < entries.length; i += chunkSize) { final end = (i + chunkSize).clamp(0, entries.length); final chunk = {}; - for (int j = i; j < end; j++) { + for (var j = i; j < end; j++) { final entry = entries[j]; chunk[entry.key] = entry.value; } @@ -442,7 +466,7 @@ class PerformanceParser { List> _chunkList(List list, int chunkSize) { final chunks = >[]; - for (int i = 0; i < list.length; i += chunkSize) { + for (var i = 0; i < list.length; i += chunkSize) { final end = (i + chunkSize).clamp(0, list.length); chunks.add(list.sublist(i, end)); } diff --git a/lib/generators/base_generator.dart b/lib/generators/base_generator.dart index 8fe32c4..3aa4359 100644 --- a/lib/generators/base_generator.dart +++ b/lib/generators/base_generator.dart @@ -1,7 +1,7 @@ -import '../core/config.dart'; -import '../core/exceptions.dart'; -import '../core/models.dart'; -import '../utils/string_utils.dart'; +import 'package:swagger_generator_flutter/core/config.dart'; +import 'package:swagger_generator_flutter/core/exceptions.dart'; +import 'package:swagger_generator_flutter/core/models.dart'; +import 'package:swagger_generator_flutter/utils/string_utils.dart'; /// 代码生成器基类 /// 定义通用的接口和功能 @@ -16,7 +16,7 @@ abstract class BaseGenerator { /// [description] 文件描述 /// [fileName] 文件名(可选) String generateFileHeader(String description, {String? fileName}) { - return StringUtils.generateFileHeader( + final header = StringUtils.generateFileHeader( description, SwaggerConfig.swaggerJsonUrls.isNotEmpty ? SwaggerConfig.swaggerJsonUrls.first @@ -24,6 +24,9 @@ abstract class BaseGenerator { fileName: fileName, fileType: description, ); + + // 添加 lint 忽略注释 + return '$header\n// ignore_for_file: type=lint, invalid_annotation_target\n'; } /// 生成类型安全的代码 @@ -37,6 +40,11 @@ abstract class BaseGenerator { ); } + // 确保文件以换行符结尾 + if (!code.endsWith('\n')) { + return '$code\n'; + } + return code; } catch (e) { throw CodeGenerationException( @@ -81,11 +89,10 @@ abstract class BaseGenerator { /// 模型代码生成器基类 abstract class ModelGenerator extends BaseGenerator { + ModelGenerator(this.document, {this.useSimpleModels = false}); final SwaggerDocument document; final bool useSimpleModels; - ModelGenerator(this.document, {this.useSimpleModels = false}); - @override String get generatorType => 'ModelGenerator'; @@ -104,7 +111,7 @@ abstract class ModelGenerator extends BaseGenerator { // 生成文件头 buffer.writeln(generateFileHeader('${model.name} 枚举定义')); - buffer.writeln(''); + buffer.writeln(); // 生成枚举类 if (model.description.isNotEmpty) { @@ -114,14 +121,14 @@ abstract class ModelGenerator extends BaseGenerator { buffer.writeln('enum $className {'); // 生成枚举值 - for (int i = 0; i < model.enumValues.length; i++) { + for (var 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\'),'); + buffer.writeln(" $enumName('$value'),"); } } @@ -130,14 +137,14 @@ abstract class ModelGenerator extends BaseGenerator { buffer.clear(); buffer.writeln(content.substring(0, content.lastIndexOf(','))); buffer.writeln(';'); - buffer.writeln(''); + buffer.writeln(); // 生成构造函数和方法 buffer.writeln(' const $className(this.value);'); buffer.writeln( ' final ${enumType == 'integer' || enumType == 'number' ? 'int' : 'String'} value;', ); - buffer.writeln(''); + buffer.writeln(); // 生成 fromValue 方法 buffer.writeln(' static $className fromValue(dynamic value) {'); @@ -146,19 +153,19 @@ abstract class ModelGenerator extends BaseGenerator { buffer.writeln(' return enumValue;'); buffer.writeln(' }'); buffer.writeln(' }'); - buffer.writeln(' throw ArgumentError(\'Unknown enum value: \$value\');'); + buffer.writeln(r" throw ArgumentError('Unknown enum value: $value');"); buffer.writeln(' }'); - buffer.writeln(''); + buffer.writeln(); // 生成 fromJson 方法 buffer.writeln(' factory $className.fromJson(dynamic json) {'); buffer.writeln(' return fromValue(json);'); buffer.writeln(' }'); - buffer.writeln(''); + buffer.writeln(); // 生成 toJson 方法 buffer.writeln(' dynamic toJson() => value;'); - buffer.writeln(''); + buffer.writeln(); buffer.writeln('}'); @@ -257,16 +264,6 @@ abstract class ModelGenerator extends BaseGenerator { /// 选项配置类 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, @@ -281,67 +278,58 @@ class GeneratorOptions { /// 从命令行参数创建选项 factory GeneratorOptions.fromArgs(List args) { - bool generateEndpoints = false; - bool generateModels = false; - bool generateDocs = false; - bool useSimpleModels = false; - const bool separateModelFiles = true; - String modelsDirectory = 'models'; - String outputDirectory = 'generator'; - String endpointsFileName = 'api_paths.dart'; - String docsFileName = 'api_documentation.md'; + var generateEndpoints = false; + var generateModels = false; + var generateDocs = false; + var useSimpleModels = false; + const separateModelFiles = true; + var modelsDirectory = 'models'; + var outputDirectory = 'generator'; + var endpointsFileName = 'api_paths.dart'; + var docsFileName = 'api_documentation.md'; - bool hasSpecificOption = false; + var hasSpecificOption = false; - for (int i = 0; i < args.length; i++) { + for (var 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; } } @@ -357,11 +345,19 @@ class GeneratorOptions { generateModels: generateModels, generateDocs: generateDocs, useSimpleModels: useSimpleModels, - separateModelFiles: separateModelFiles, modelsDirectory: modelsDirectory, outputDirectory: outputDirectory, endpointsFileName: endpointsFileName, docsFileName: docsFileName, ); } + 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; } diff --git a/lib/generators/model_code_generator.dart b/lib/generators/model_code_generator.dart index 1473f3a..c47a44b 100644 --- a/lib/generators/model_code_generator.dart +++ b/lib/generators/model_code_generator.dart @@ -1,56 +1,23 @@ -import '../core/config.dart'; -import '../core/models.dart'; -import '../utils/string_utils.dart'; -import 'base_generator.dart'; +import 'package:swagger_generator_flutter/core/config.dart'; +import 'package:swagger_generator_flutter/core/models.dart'; +import 'package:swagger_generator_flutter/generators/base_generator.dart'; +import 'package:swagger_generator_flutter/utils/string_utils.dart'; /// 模型代码生成器 /// 负责生成Dart模型类代码 class ModelCodeGenerator extends ModelGenerator { - ModelCodeGenerator(super.document, {super.useSimpleModels}); + ModelCodeGenerator(super.document); @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); - } - - // 只使用 JsonSerializable 注解版本 - return generateAnnotatedModelCode(model); + // This method is deprecated and will not be used. + // The generator now uses generateSeparateModelFiles. + throw UnimplementedError( + 'Single file model generation is no longer supported.', + ); } @override @@ -96,104 +63,15 @@ class ModelCodeGenerator extends ModelGenerator { } } - /// 生成带注解的模型代码 - 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(checked: true, includeIfNull: false)'); - buffer.writeln('class $className {'); - - // 生成属性 - model.properties.forEach((propName, property) { - final dartType = getDartPropertyType(property); - // 判断是否可空: - // 1. String 类型(非 date-time/date)强制为非空,忽略 Swagger 的 nullable 标记 - // 2. 如果有 defaultValue,则不可空(因为 json_annotation 会保证有值) - // 3. 否则根据 nullable 标记决定 - final isNormalString = property.type == PropertyType.string && - property.format != 'date-time' && - property.format != 'date'; - final hasDefaultValue = property.defaultValue != null || isNormalString; - final nullable = hasDefaultValue ? '' : (property.nullable ? '?' : ''); - final dartPropName = StringUtils.toDartPropertyName(propName); - - if (property.description.isNotEmpty) { - buffer.writeln( - ' ${StringUtils.generateComment(property.description)}', - ); - } - - // 添加JsonKey注解 - final needsJsonKey = - _needsJsonKeyAnnotation(dartPropName, propName, property, model); - 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); - // 判断是否需要 required 修饰符: - // 1. String 类型(非 date-time/date)强制需要 required,忽略 Swagger 的 nullable 标记 - // 2. 其他非可空字段需要 required - // 3. 可空字段不需要 required - final isNormalString = property.type == PropertyType.string && - property.format != 'date-time' && - property.format != 'date'; - final shouldBeRequired = isNormalString || !property.nullable; - final required = shouldBeRequired ? 'required ' : ''; - buffer.writeln(' ${required}this.$dartPropName,'); - }); - buffer.writeln(' });'); - } - buffer.writeln(''); - - // 生成 fromJson 工厂方法 - buffer.writeln( - ' factory $className.fromJson(Map json) =>', + @override + @Deprecated( + 'Use generateSingleModelFile or generateSeparateModelFiles instead', + ) + String generateModelCode(ApiModel model) { + // This method is deprecated and will not be used. + throw UnimplementedError( + 'generateModelCode is no longer supported. Use generateSingleModelFile.', ); - buffer.writeln(' _\$${className}FromJson(json);'); - buffer.writeln(''); - - // 生成 toJson 方法 - buffer.writeln( - ' Map toJson() => _\$${className}ToJson(this);'); - buffer.writeln(''); - - buffer.writeln('}'); - - return buffer.toString(); } /// 获取模型应该存放的子目录 @@ -259,33 +137,38 @@ class ModelCodeGenerator extends ModelGenerator { final buffer = StringBuffer(); // 生成文件头 - buffer.writeln(generateFileHeader( - '${model.name} 模型定义', - fileName: fileName ?? StringUtils.generateFileName(model.name), - )); - buffer.writeln(''); + buffer.writeln( + generateFileHeader( + '${model.name} 模型定义', + fileName: fileName ?? StringUtils.generateFileName(model.name), + ), + ); + buffer.writeln(); - // 枚举类需要导入 json_annotation 以使用 @JsonEnum 注解 - if (!useSimpleModels && model.isEnum) { + // Freezed 模型需要导入 freezed_annotation + if (!model.isEnum) { buffer.writeln( - 'import \'package:json_annotation/json_annotation.dart\';', + "import 'package:freezed_annotation/freezed_annotation.dart';", ); - buffer.writeln(''); + // json_annotation is already exported by freezed_annotation, so we don't need to import it explicitly + // unless we are using specific features not covered by freezed (which is rare for standard usage) + // buffer.writeln('import \'package:json_annotation/json_annotation.dart\';'); + buffer.writeln(); } - // 普通类且非简洁模式时导入 json_annotation - else if (!useSimpleModels && !model.isEnum) { + // 枚举类需要导入 json_annotation 以使用 @JsonEnum 注解 + else if (model.isEnum) { buffer.writeln( - 'import \'package:json_annotation/json_annotation.dart\';', + "import 'package:json_annotation/json_annotation.dart';", ); - buffer.writeln(''); + buffer.writeln(); } // 生成导入依赖 - 统一使用父目录的 index.dart // 因为模型现在在子目录中(如 result/user_result.dart),需要导入 '../index.dart' final importedTypes = getImportedTypes(model); if (importedTypes.isNotEmpty) { - buffer.writeln('import \'../index.dart\';'); - buffer.writeln(''); + buffer.writeln("import '../index.dart';"); + buffer.writeln(); } // 生成模型代码,但不包含导入语句和文件头(因为已经在上面生成了) @@ -320,14 +203,14 @@ class ModelCodeGenerator extends ModelGenerator { buffer.writeln('enum $className {'); // 生成枚举值 - for (int i = 0; i < model.enumValues.length; i++) { + for (var 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\'),'); + buffer.writeln(" $enumName('$value'),"); } } @@ -336,14 +219,14 @@ class ModelCodeGenerator extends ModelGenerator { buffer.clear(); buffer.writeln(content.substring(0, content.lastIndexOf(','))); buffer.writeln(';'); - buffer.writeln(''); + buffer.writeln(); // 生成构造函数和方法 buffer.writeln(' const $className(this.value);'); buffer.writeln( ' final ${enumType == 'integer' || enumType == 'number' ? 'int' : 'String'} value;', ); - buffer.writeln(''); + buffer.writeln(); // 生成 fromValue 方法 buffer.writeln(' static $className fromValue(dynamic value) {'); @@ -352,19 +235,19 @@ class ModelCodeGenerator extends ModelGenerator { buffer.writeln(' return enumValue;'); buffer.writeln(' }'); buffer.writeln(' }'); - buffer.writeln(' throw ArgumentError(\'Unknown enum value: \$value\');'); + buffer.writeln(r" throw ArgumentError('Unknown enum value: $value');"); buffer.writeln(' }'); - buffer.writeln(''); + buffer.writeln(); // 生成 fromJson 方法 buffer.writeln(' factory $className.fromJson(dynamic json) {'); buffer.writeln(' return fromValue(json);'); buffer.writeln(' }'); - buffer.writeln(''); + buffer.writeln(); // 生成 toJson 方法 buffer.writeln(' dynamic toJson() => value;'); - buffer.writeln(''); + buffer.writeln(); buffer.writeln('}'); @@ -380,25 +263,26 @@ class ModelCodeGenerator extends ModelGenerator { // 生成 part 声明 final partFileName = StringUtils.generateFileName(model.name); + final freezedPart = partFileName.replaceAll('.dart', '.freezed.dart'); final generatedPart = partFileName.replaceAll('.dart', '.g.dart'); - buffer.writeln('part \'$generatedPart\';'); - buffer.writeln(''); + buffer.writeln("part '$freezedPart';"); + buffer.writeln("part '$generatedPart';"); + buffer.writeln(); // 生成类注释 if (model.description.isNotEmpty) { buffer.writeln(StringUtils.generateComment(model.description)); } - buffer.writeln('@JsonSerializable(checked: true, includeIfNull: false)'); - buffer.writeln('class $className {'); + buffer.writeln('@freezed'); + buffer.writeln('abstract class $className with _\$$className {'); + + // 生成 factory 构造函数 + buffer.writeln(' const factory $className({'); // 生成属性 model.properties.forEach((propName, property) { final dartType = getDartPropertyType(property); - // 判断是否可空: - // 1. String 类型(非 date-time/date)强制为非空,忽略 Swagger 的 nullable 标记 - // 2. 如果有 defaultValue,则不可空(因为 json_annotation 会保证有值) - // 3. 否则根据 nullable 标记决定 final isNormalString = property.type == PropertyType.string && property.format != 'date-time' && property.format != 'date'; @@ -408,55 +292,32 @@ class ModelCodeGenerator extends ModelGenerator { if (property.description.isNotEmpty) { buffer.writeln( - ' ${StringUtils.generateComment(property.description)}', + ' ${StringUtils.generateComment(property.description)}', ); } // 添加JsonKey注解 - final needsJsonKey = + final jsonKeyAnnotations = _needsJsonKeyAnnotation(dartPropName, propName, property, model); - if (needsJsonKey.isNotEmpty) { - buffer.writeln(' @JsonKey($needsJsonKey)'); + if (jsonKeyAnnotations.isNotEmpty) { + buffer.writeln(' @JsonKey($jsonKeyAnnotations)'); } - buffer.writeln(' final $dartType$nullable $dartPropName;'); - buffer.writeln(''); + // 判断是否需要 required 修饰符 + final shouldBeRequired = isNormalString || !property.nullable; + final required = shouldBeRequired ? 'required ' : ''; + + buffer.writeln(' $required$dartType$nullable $dartPropName,'); }); - // 生成构造函数 - if (model.properties.isEmpty) { - buffer.writeln(' const $className();'); - } else { - buffer.writeln(' const $className({'); - model.properties.forEach((propName, property) { - final dartPropName = StringUtils.toDartPropertyName(propName); - // 判断是否需要 required 修饰符: - // 1. String 类型(非 date-time/date)强制需要 required,忽略 Swagger 的 nullable 标记 - // 2. 其他非可空字段需要 required - // 3. 可空字段不需要 required - final isNormalString = property.type == PropertyType.string && - property.format != 'date-time' && - property.format != 'date'; - final shouldBeRequired = isNormalString || !property.nullable; - final required = shouldBeRequired ? 'required ' : ''; - buffer.writeln(' ${required}this.$dartPropName,'); - }); - buffer.writeln(' });'); - } - buffer.writeln(''); + buffer.writeln(' }) = _$className;'); + 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(''); - buffer.writeln('}'); return buffer.toString(); @@ -468,33 +329,33 @@ class ModelCodeGenerator extends ModelGenerator { final buffer = StringBuffer(); buffer.writeln(generateFileHeader('API 模型导出文件')); - buffer.writeln(''); + buffer.writeln(); // 添加 library 声明 buffer.writeln('library;'); - buffer.writeln(''); + buffer.writeln(); // 导出 base_result 和 base_page_result(如果配置了) final baseResultImport = SwaggerConfig.baseResultImport; final basePageResultImport = SwaggerConfig.basePageResultImport; if (baseResultImport.isNotEmpty) { - buffer.writeln('export \'$baseResultImport\';'); + buffer.writeln("export '$baseResultImport';"); } if (basePageResultImport.isNotEmpty) { - buffer.writeln('export \'$basePageResultImport\';'); + buffer.writeln("export '$basePageResultImport';"); } if ((baseResultImport.isNotEmpty || basePageResultImport.isNotEmpty) && modelsByDirectory.isNotEmpty) { - buffer.writeln(''); + buffer.writeln(); } // 导出所有子目录的 index.dart final sortedDirs = modelsByDirectory.keys.toList()..sort(); for (final dir in sortedDirs) { - buffer.writeln('export \'$dir/index.dart\';'); + buffer.writeln("export '$dir/index.dart';"); } return generateTypeCheckedCode(buffer.toString()); @@ -505,11 +366,11 @@ class ModelCodeGenerator extends ModelGenerator { final buffer = StringBuffer(); buffer.writeln(generateFileHeader('模型导出文件')); - buffer.writeln(''); + buffer.writeln(); // 添加 library 声明 buffer.writeln('library;'); - buffer.writeln(''); + buffer.writeln(); // 按模型名排序并导出 final sortedModels = List.from(models) @@ -517,7 +378,7 @@ class ModelCodeGenerator extends ModelGenerator { for (final model in sortedModels) { final fileName = StringUtils.generateFileName(model.name); - buffer.writeln('export \'$fileName\';'); + buffer.writeln("export '$fileName';"); } return generateTypeCheckedCode(buffer.toString()); @@ -527,33 +388,33 @@ class ModelCodeGenerator extends ModelGenerator { final buffer = StringBuffer(); buffer.writeln(generateFileHeader('API 模型导出文件')); - buffer.writeln(''); + buffer.writeln(); // 添加 library 声明 buffer.writeln('library;'); - buffer.writeln(''); + buffer.writeln(); // 导出 base_result 和 base_page_result(如果配置了) final baseResultImport = SwaggerConfig.baseResultImport; final basePageResultImport = SwaggerConfig.basePageResultImport; if (baseResultImport.isNotEmpty) { - buffer.writeln('export \'$baseResultImport\';'); + buffer.writeln("export '$baseResultImport';"); } if (basePageResultImport.isNotEmpty) { - buffer.writeln('export \'$basePageResultImport\';'); + buffer.writeln("export '$basePageResultImport';"); } if ((baseResultImport.isNotEmpty || basePageResultImport.isNotEmpty) && modelFileNames.isNotEmpty) { - buffer.writeln(''); + buffer.writeln(); } // 按文件名排序并导出所有模型 final sortedFiles = List.from(modelFileNames)..sort(); for (final fileName in sortedFiles) { - buffer.writeln('export \'$fileName\';'); + buffer.writeln("export '$fileName';"); } return generateTypeCheckedCode(buffer.toString()); @@ -570,7 +431,7 @@ class ModelCodeGenerator extends ModelGenerator { // 属性名与JSON字段名不同时需要name参数 if (dartPropName != propName) { - annotations.add('name: \'$propName\''); + annotations.add("name: '$propName'"); } // ✨ 使用模型的 usageType 判断,而不是基于名字判断 @@ -587,10 +448,10 @@ class ModelCodeGenerator extends ModelGenerator { if (property.defaultValue != null) { // 如果OpenAPI文档中有明确的默认值,使用它 final defaultVal = property.defaultValue.toString(); - annotations.add('defaultValue: \'$defaultVal\''); + annotations.add("defaultValue: '$defaultVal'"); } else { // 如果没有默认值,使用空字符串作为默认值 - annotations.add('defaultValue: \'\''); + annotations.add("defaultValue: ''"); } } @@ -620,7 +481,7 @@ class ModelCodeGenerator extends ModelGenerator { annotations.add('defaultValue: $defaultVal'); } else { // 对于其他类型,将默认值作为字符串处理 - annotations.add('defaultValue: \'$defaultVal\''); + annotations.add("defaultValue: '$defaultVal'"); } } diff --git a/lib/generators/retrofit_api_generator.dart b/lib/generators/retrofit_api_generator.dart index fd3903e..2bae404 100644 --- a/lib/generators/retrofit_api_generator.dart +++ b/lib/generators/retrofit_api_generator.dart @@ -1,20 +1,11 @@ -import '../core/config_loader.dart'; -import '../core/models.dart'; -import '../utils/string_utils.dart'; -import 'base_generator.dart'; +import 'package:swagger_generator_flutter/core/config_loader.dart'; +import 'package:swagger_generator_flutter/core/models.dart'; +import 'package:swagger_generator_flutter/generators/base_generator.dart'; +import 'package:swagger_generator_flutter/utils/string_utils.dart'; /// Retrofit 风格的 API 生成器 /// 负责生成带有注解的 API 接口类 class RetrofitApiGenerator extends BaseGenerator { - final String className; - final bool useRetrofit; - final bool useDio; - final bool splitByTags; - final bool generateModels; - final bool versionedApi; // 是否启用版本化 API - - late SwaggerDocument document; - RetrofitApiGenerator({ this.className = 'ApiClient', this.useRetrofit = true, @@ -23,6 +14,14 @@ class RetrofitApiGenerator extends BaseGenerator { this.generateModels = true, this.versionedApi = true, // 默认启用版本化 }); + final String className; + final bool useRetrofit; + final bool useDio; + final bool splitByTags; + final bool generateModels; + final bool versionedApi; // 是否启用版本化 API + + late SwaggerDocument document; @override String get generatorType => 'RetrofitApiGenerator'; @@ -83,23 +82,22 @@ class RetrofitApiGenerator extends BaseGenerator { final buffer = StringBuffer(); // 生成文件头 - buffer.writeln(generateFileHeader('Retrofit 风格 API 接口定义')); - buffer.writeln(''); + buffer + ..writeln(generateFileHeader('Retrofit 风格 API 接口定义')) + ..writeln(); // 生成导入语句 _generateImports(buffer); // 生成安全方案相关代码 - buffer.write(_generateSecurityCode(document)); - - // 生成媒体类型处理代码 - buffer.write(_generateMediaTypeHandlers()); - - // 生成文件上传处理代码 - buffer.write(_generateFileUploadHandlers()); - - // 生成编码处理代码 - buffer.write(_generateEncodingHandlers()); + buffer + ..write(_generateSecurityCode(document)) + // 生成媒体类型处理代码 + ..write(_generateMediaTypeHandlers()) + // 生成文件上传处理代码 + ..write(_generateFileUploadHandlers()) + // 生成编码处理代码 + ..write(_generateEncodingHandlers()); // 生成 API 接口类 _generateApiInterface(buffer); @@ -112,8 +110,9 @@ class RetrofitApiGenerator extends BaseGenerator { final buffer = StringBuffer(); // 生成文件头 - buffer.writeln(generateFileHeader('主 API 接口定义 - 集合所有 Tag 的 API')); - buffer.writeln(''); + buffer + ..writeln(generateFileHeader('主 API 接口定义 - 集合所有 Tag 的 API')) + ..writeln(); // 生成导入语句 _generateMainImports(buffer); @@ -140,7 +139,7 @@ class RetrofitApiGenerator extends BaseGenerator { // 生成文件头(传入文件名) buffer .writeln(generateFileHeader('$tagName API 接口定义', fileName: fileName)); - buffer.writeln(''); + buffer.writeln(); // 生成导入语句 _generateTagImports(buffer, paths); @@ -168,31 +167,31 @@ class RetrofitApiGenerator extends BaseGenerator { // 添加配置的额外包导入 final packageImports = ConfigLoader.getPackageImports(); for (final import in packageImports) { - buffer.writeln('import \'$import\';'); + buffer.writeln("import '$import';"); } // 相对路径导入(api_models/index.dart 会导出 base_result 和 base_page_result) - buffer.writeln('import \'../../api_models/index.dart\';'); - buffer.writeln(''); - - // 生成 part 声明 - buffer.writeln('part \'api_client.g.dart\';'); - buffer.writeln(''); + buffer + ..writeln("import '../../api_models/index.dart';") + ..writeln() + ..writeln("part 'api_client.g.dart';") + ..writeln(); } /// 生成 API 接口类 void _generateApiInterface(StringBuffer buffer) { - buffer.writeln('/// $className API 接口'); - buffer.writeln('/// 使用 Retrofit 和 Dio 进行网络请求'); - buffer.writeln('/// 支持多种媒体类型、文件上传、认证等功能'); - + buffer + ..writeln('/// $className API 接口') + ..writeln('/// 使用 Retrofit 和 Dio 进行网络请求') + ..writeln('/// 支持多种媒体类型、文件上传、认证等功能'); if (useRetrofit) { // 添加 baseUrl(如果有的话) final baseUrl = document.servers.isNotEmpty ? document.servers.first.url : ''; if (baseUrl.isNotEmpty) { buffer.writeln( - '@RestApi(baseUrl: \'$baseUrl\', parser: Parser.JsonSerializable)'); + "@RestApi(baseUrl: '$baseUrl', parser: Parser.JsonSerializable)", + ); } else { buffer.writeln('@RestApi(parser: Parser.JsonSerializable)'); } @@ -201,17 +200,19 @@ class RetrofitApiGenerator extends BaseGenerator { buffer.writeln('abstract class $className {'); if (useRetrofit) { - buffer.writeln(' /// 创建 API 服务实例'); - buffer.writeln(' /// [dio] Dio 实例,可以预配置拦截器、超时等'); - buffer.writeln(' /// [baseUrl] 可选的基础 URL,会覆盖注解中的 baseUrl'); - buffer.writeln( - ' factory $className(Dio dio, {String? baseUrl}) = _$className;'); + buffer + ..writeln(' /// 创建 API 服务实例') + ..writeln(' /// [dio] Dio 实例,可以预配置拦截器、超时等') + ..writeln(' /// [baseUrl] 可选的基础 URL,会覆盖注解中的 baseUrl'); + ' factory $className(Dio dio, {String? baseUrl}) = _$className;', + ); } else { - buffer.writeln(' final Dio _dio;'); - buffer.writeln(' $className(this._dio);'); + buffer + ..writeln(' final Dio _dio;') + ..writeln(' $className(this._dio);'); } - buffer.writeln(''); + buffer.writeln(); // 按控制器分组生成接口方法 final controllerGroups = _groupPathsByController(); @@ -220,14 +221,14 @@ class RetrofitApiGenerator extends BaseGenerator { final controllerName = entry.key; final paths = entry.value; - buffer.writeln(' // ========== $controllerName 相关接口 =========='); - buffer.writeln(''); - + buffer + ..writeln(' // ========== $controllerName 相关接口 ==========') + ..writeln(); for (final path in paths) { _generateApiMethod(buffer, path); } - buffer.writeln(''); + buffer.writeln(); } buffer.writeln('}'); @@ -256,9 +257,9 @@ class RetrofitApiGenerator extends BaseGenerator { .toList(); if (paramsWithDescription.isNotEmpty) { - buffer.writeln(' ///'); - buffer.writeln(' /// 参数:'); - + buffer + ..writeln(' ///') + ..writeln(' /// 参数:'); for (final param in paramsWithDescription) { final commentParts = []; @@ -281,7 +282,7 @@ class RetrofitApiGenerator extends BaseGenerator { // 生成 HTTP 方法注解 if (useRetrofit) { - buffer.writeln(' @$httpMethod(\'$cleanPath\')'); + buffer.writeln(" @$httpMethod('$cleanPath')"); } // 生成方法签名 @@ -293,7 +294,7 @@ class RetrofitApiGenerator extends BaseGenerator { // 所有参数都使用命名参数,提高代码可读性 buffer.writeln(' {'); - for (int i = 0; i < parameters.length; i++) { + for (var i = 0; i < parameters.length; i++) { final param = parameters[i]; final isLast = i == parameters.length - 1; @@ -302,18 +303,21 @@ class RetrofitApiGenerator extends BaseGenerator { if (param.annotation.isNotEmpty) { buffer.writeln( - ' ${requiredKeyword}${param.annotation} ${param.type} ${param.name}${isLast ? '' : ','}'); + ' $requiredKeyword${param.annotation} ${param.type} ${param.name}${isLast ? '' : ','}', + ); } else { buffer.writeln( - ' ${requiredKeyword}${param.type} ${param.name}${isLast ? '' : ','}'); + ' $requiredKeyword${param.type} ${param.name}${isLast ? '' : ','}', + ); } } buffer.writeln(' }'); } - buffer.writeln(' );'); - buffer.writeln(''); + buffer + ..writeln(' );') + ..writeln(); } /// 生成简化的方法名称 @@ -332,7 +336,7 @@ class RetrofitApiGenerator extends BaseGenerator { } // 清理路径,移除 /api/v1 前缀 - String cleanedPath = path.path.replaceFirst(RegExp(r'^/api/v\d+'), ''); + var cleanedPath = path.path.replaceFirst(RegExp(r'^/api/v\d+'), ''); if (cleanedPath.isEmpty) { cleanedPath = path.path; } @@ -357,8 +361,7 @@ class RetrofitApiGenerator extends BaseGenerator { } // 最后的备用方案:使用完整路径 - final sanitizedPath = - pathParts.map((part) => StringUtils.toPascalCase(part)).join(''); + final sanitizedPath = pathParts.map(StringUtils.toPascalCase).join(); return StringUtils.toCamelCase(sanitizedPath); } @@ -382,8 +385,9 @@ class RetrofitApiGenerator extends BaseGenerator { } // 如果没有明确的 schema 定义,使用通用类型 + // 注意:为避免 Retrofit 生成 Map.fromJson 的编译错误,这里返回 dynamic // 这通常表示后端文档不完整,应该要求后端完善 swagger 文档 - return 'BaseResult>'; + return 'BaseResult'; } /// 包装返回类型为BaseResult或BasePageResult @@ -414,6 +418,10 @@ class RetrofitApiGenerator extends BaseGenerator { } // 对于其他类型,使用BaseResult包装 + // 避免 Map 作为泛型参数导致 Retrofit 生成 Map.fromJson 编译错误 + if (originalType.startsWith('Map<')) { + return 'BaseResult'; + } return 'BaseResult<$originalType>'; } @@ -505,7 +513,7 @@ class RetrofitApiGenerator extends BaseGenerator { final operationId = path.operationId.toLowerCase(); final tags = path.tags.map((tag) => tag.toLowerCase()).toList(); - double score = 0.0; + var score = 0.0; // 1. 基于查询参数判断(权重最高,因为这是最直接的证据) if (_hasPaginationParameters(path)) { @@ -532,8 +540,12 @@ class RetrofitApiGenerator extends BaseGenerator { } /// 检查是否包含分页相关的关键词 - bool _hasPaginationKeywords(String pathLower, String summaryLower, - String operationId, List tags) { + bool _hasPaginationKeywords( + String pathLower, + String summaryLower, + String operationId, + List tags, + ) { final paginationKeywords = [ 'page', 'pagination', @@ -547,7 +559,7 @@ class RetrofitApiGenerator extends BaseGenerator { 'filter', '筛选', 'find', - '查找' + '查找', ]; // 检查路径 @@ -567,7 +579,8 @@ class RetrofitApiGenerator extends BaseGenerator { // 检查标签 if (tags.any( - (tag) => paginationKeywords.any((keyword) => tag.contains(keyword)))) { + (tag) => paginationKeywords.any((keyword) => tag.contains(keyword)), + )) { return true; } @@ -590,7 +603,7 @@ class RetrofitApiGenerator extends BaseGenerator { 'currentpage', 'page_size', 'page_number', - 'page_index' + 'page_index', ]; final timeRangeParams = [ @@ -601,7 +614,7 @@ class RetrofitApiGenerator extends BaseGenerator { 'starttime', 'endtime', 'startdate', - 'enddate' + 'enddate', ]; final queryParams = path.parameters @@ -610,18 +623,22 @@ class RetrofitApiGenerator extends BaseGenerator { .toList(); // 检查是否有分页参数 - final hasPaginationParams = queryParams.any((param) => paginationParams - .any((paginationParam) => param.contains(paginationParam))); + 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')); + 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) { @@ -639,15 +656,15 @@ class RetrofitApiGenerator extends BaseGenerator { /// 检查返回类型名称是否暗示分页 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.*>'), + RegExp('List<.*Result>'), + RegExp('List<.*List.*>'), + RegExp('List<.*Page.*>'), + RegExp('List<.*Search.*>'), + RegExp('List<.*Filter.*>'), + RegExp('List<.*Task.*>'), + RegExp('List<.*User.*>'), + RegExp('List<.*School.*>'), + RegExp('List<.*Class.*>'), ]; return paginationTypePatterns.any((pattern) => pattern.hasMatch(type)); @@ -656,12 +673,12 @@ class RetrofitApiGenerator extends BaseGenerator { /// 检查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.*'), + RegExp('/get.*list'), + RegExp('/search.*'), + RegExp('/find.*'), + RegExp('/query.*'), + RegExp('/filter.*'), + RegExp('/page.*'), ]; return paginationPathPatterns.any((pattern) => pattern.hasMatch(pathLower)); @@ -737,8 +754,8 @@ class RetrofitApiGenerator extends BaseGenerator { } // 处理 $ref 引用 - if (schema['\$ref'] != null) { - final ref = schema['\$ref'] as String; + if (schema[r'$ref'] != null) { + final ref = schema[r'$ref'] as String; final parts = ref.split('/'); if (parts.isNotEmpty) { final refName = parts.last; @@ -750,7 +767,7 @@ class RetrofitApiGenerator extends BaseGenerator { // 提取 items 中的类型 final itemsProp = model.properties['items']; if (itemsProp != null && itemsProp.type == PropertyType.array) { - String itemType = 'dynamic'; + var itemType = 'dynamic'; if (itemsProp.reference != null) { itemType = StringUtils.generateClassName(itemsProp.reference!); } else if (itemsProp.items != null) { @@ -835,8 +852,10 @@ class RetrofitApiGenerator extends BaseGenerator { case 'string': // 检查是否有 format final format = schema['format'] as String?; + // 注意:为避免 Retrofit 生成器对 DateTime 误用 fromJson,这里一律返回 String + // 在业务侧可自行使用 DateTime.parse 进行转换 if (format == 'date-time' || format == 'date') { - return 'DateTime'; + return 'String'; } if (format == 'uuid') { return 'String'; @@ -906,14 +925,16 @@ class RetrofitApiGenerator extends BaseGenerator { .where((p) => p.location == ParameterLocation.path) .toList(); for (final param in pathParams) { - parameters.add(ApiMethodParameter( - name: StringUtils.toDartPropertyName(param.name), - type: _getDartType(param.type), - annotation: useRetrofit ? '@Path(\'${param.name}\')' : '', - required: param.required, - description: param.description, - defaultValue: param.defaultValue, - )); + parameters.add( + ApiMethodParameter( + name: StringUtils.toDartPropertyName(param.name), + type: _getDartType(param.type), + annotation: useRetrofit ? "@Path('${param.name}')" : '', + required: param.required, + description: param.description, + defaultValue: param.defaultValue, + ), + ); } // 处理查询参数 @@ -929,24 +950,28 @@ class RetrofitApiGenerator extends BaseGenerator { _generateParameterEntity(path, parameterEntityClassName, queryParams); // 添加参数实体类作为单个参数 - parameters.add(ApiMethodParameter( - name: 'parameters', - type: '$parameterEntityClassName?', - annotation: useRetrofit ? '@Queries()' : '', - required: false, - )); + 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.toDartPropertyName(param.name), - type: '${_getDartType(param.type)}$nullable', - annotation: useRetrofit ? '@Query(\'${param.name}\')' : '', - required: param.required, - description: param.description, - defaultValue: param.defaultValue, - )); + parameters.add( + ApiMethodParameter( + name: StringUtils.toDartPropertyName(param.name), + type: '${_getDartType(param.type)}$nullable', + annotation: useRetrofit ? "@Query('${param.name}')" : '', + required: param.required, + description: param.description, + defaultValue: param.defaultValue, + ), + ); } } @@ -956,15 +981,18 @@ class RetrofitApiGenerator extends BaseGenerator { .toList(); for (final param in bodyParams) { final bodyType = _inferRequestBodyType(path); - parameters.add(ApiMethodParameter( - name: StringUtils.toDartPropertyName( - param.name.isNotEmpty ? param.name : 'request'), - type: bodyType, - annotation: useRetrofit ? '@Body()' : '', - required: false, - description: param.description, - defaultValue: param.defaultValue, - )); + parameters.add( + ApiMethodParameter( + name: StringUtils.toDartPropertyName( + param.name.isNotEmpty ? param.name : 'request', + ), + type: bodyType, + annotation: useRetrofit ? '@Body()' : '', + required: false, + description: param.description, + defaultValue: param.defaultValue, + ), + ); } // 如果是 POST/PUT/PATCH 但没有明确的 body 参数,检查是否真的需要请求体 @@ -977,13 +1005,15 @@ class RetrofitApiGenerator extends BaseGenerator { final isRequired = path.requestBody?.required ?? false; final nullable = isRequired ? '' : '?'; - parameters.add(ApiMethodParameter( - name: 'request', - type: '$bodyType$nullable', - annotation: useRetrofit ? '@Body()' : '', - required: isRequired, - description: path.requestBody?.description ?? '', - )); + parameters.add( + ApiMethodParameter( + name: 'request', + type: '$bodyType$nullable', + annotation: useRetrofit ? '@Body()' : '', + required: isRequired, + description: path.requestBody?.description ?? '', + ), + ); } return parameters; @@ -1019,32 +1049,26 @@ class RetrofitApiGenerator extends BaseGenerator { 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(''); - - // 生成方法实现 + buffer + ..writeln() + ..writeln('/// $className 的手动实现') + ..writeln('/// 使用 Dio 进行网络请求') + ..writeln('class ${className}Impl implements $className {') + ..writeln(' final Dio _dio;') + ..writeln() + ..writeln(' ${className}Impl(this._dio);') + ..writeln(); final controllerGroups = _groupPathsByController(); for (final entry in controllerGroups.entries) { final controllerName = entry.key; final paths = entry.value; - buffer.writeln(' // ========== $controllerName 相关接口实现 =========='); - buffer.writeln(''); - + buffer + ..writeln(' // ========== $controllerName 相关接口实现 ==========') + ..writeln(); for (final path in paths) { _generateManualMethodImplementation(buffer, path); } @@ -1061,11 +1085,11 @@ class RetrofitApiGenerator extends BaseGenerator { final returnType = _generateReturnType(path); final parameters = _generateParameters(path); - buffer.writeln(' @override'); - buffer.writeln(' Future<$returnType> $methodName('); - + buffer + ..writeln(' @override') + ..writeln(' Future<$returnType> $methodName('); if (parameters.isNotEmpty) { - for (int i = 0; i < parameters.length; i++) { + for (var i = 0; i < parameters.length; i++) { final param = parameters[i]; final isLast = i == parameters.length - 1; buffer.writeln(' ${param.type} ${param.name}${isLast ? '' : ','}'); @@ -1080,30 +1104,33 @@ class RetrofitApiGenerator extends BaseGenerator { parameters.where((p) => p.annotation.contains('@Path')).toList(); if (pathParams.isNotEmpty) { - buffer.writeln(' String path = \'$requestPath\';'); + buffer.writeln(" String path = '$requestPath';"); for (final param in pathParams) { final paramName = param.name; final pathParamName = - param.annotation.replaceAll('@Path(\'', '').replaceAll('\')', ''); + param.annotation.replaceAll("@Path('", '').replaceAll("')", ''); buffer.writeln( - ' path = path.replaceAll(\'{$pathParamName}\', $paramName.toString());'); + " path = path.replaceAll('{$pathParamName}', $paramName.toString());", + ); } } else { - buffer.writeln(' const String path = \'$requestPath\';'); + 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 = {};'); + buffer + ..writeln() + ..writeln(' final Map queryParams = {};'); for (final param in queryParams) { final paramName = param.name; final queryParamName = - param.annotation.replaceAll('@Query(\'', '').replaceAll('\')', ''); + param.annotation.replaceAll("@Query('", '').replaceAll("')", ''); buffer.writeln( - ' if ($paramName != null) queryParams[\'$queryParamName\'] = $paramName;'); + " if ($paramName != null) queryParams['$queryParamName'] = $paramName;", + ); } } @@ -1115,10 +1142,10 @@ class RetrofitApiGenerator extends BaseGenerator { bodyParam = bodyParams.first.name; } - buffer.writeln(''); - buffer.writeln(' final response = await _dio.$httpMethod('); - buffer.writeln(' path,'); - + buffer + ..writeln() + ..writeln(' final response = await _dio.$httpMethod(') + ..writeln(' path,'); if (queryParams.isNotEmpty) { buffer.writeln(' queryParameters: queryParams,'); } @@ -1133,17 +1160,33 @@ class RetrofitApiGenerator extends BaseGenerator { } } - buffer.writeln(' );'); - buffer.writeln(''); - - // 处理响应 + buffer + ..writeln(' );') + ..writeln(); if (returnType.startsWith('List<')) { // 列表类型的处理 final itemType = returnType.substring(5, returnType.length - 1); - buffer.writeln(' final data = response.data as List;'); + if (itemType == 'DateTime') { + buffer.writeln( + ' return data.map((item) => DateTime.parse(item as String)).toList();', + ); + } else if (_isBasicType(itemType)) { + buffer.writeln( + ' return data.map((item) => item as $itemType).toList();', + ); + } else { + buffer.writeln( + ' return data.map((item) => $itemType.fromJson(item)).toList();', + ); + } + } else if (returnType == 'DateTime') { buffer.writeln( - ' return data.map((item) => $itemType.fromJson(item)).toList();'); - } else if (returnType != 'Map') { + ' return DateTime.parse(response.data as String);', + ); + } else if (_isBasicType(returnType)) { + buffer.writeln(' return response.data as $returnType;'); + } else if (returnType != 'Map' && + !returnType.startsWith('Map<')) { // 具体模型类型的处理 buffer.writeln(' return $returnType.fromJson(response.data);'); } else { @@ -1151,8 +1194,9 @@ class RetrofitApiGenerator extends BaseGenerator { buffer.writeln(' return response.data;'); } - buffer.writeln(' }'); - buffer.writeln(''); + buffer + ..writeln(' }') + ..writeln(); } /// 按控制器分组路径 @@ -1219,130 +1263,118 @@ class RetrofitApiGenerator extends BaseGenerator { /// 生成主文件的导入语句 void _generateMainImports(StringBuffer buffer) { if (useDio) { - buffer.writeln('import \'package:dio/dio.dart\';'); + buffer.writeln("import 'package:dio/dio.dart';"); } - buffer.writeln(''); - - // 相对路径导入(api_models/index.dart 会导出 base_result 和 base_page_result) - buffer.writeln('import \'../../api_models/index.dart\';'); - buffer.writeln(''); - - // 导入错误处理相关类 - buffer.writeln('import \'api_error.dart\';'); - buffer.writeln('import \'api_error_handler.dart\';'); - buffer.writeln(''); - - // 导入所有 tag 的 API 文件 + buffer + ..writeln() + ..writeln("import '../../api_models/index.dart';") + ..writeln() + ..writeln("import 'api_error.dart';") + ..writeln("import 'api_error_handler.dart';") + ..writeln(); final tagGroups = _groupPathsByTags(); for (final tagName in tagGroups.keys) { final fileName = _generateTagFileName(tagName); - buffer.writeln('import \'$fileName\';'); + buffer.writeln("import '$fileName';"); } - buffer.writeln(''); + buffer.writeln(); } /// 生成主 API 接口类 void _generateMainApiInterface(StringBuffer buffer) { - buffer.writeln('/// 统一API客户端类'); - buffer.writeln('/// 聚合所有分模块的API接口,提供统一的访问入口'); - buffer.writeln('class $className {'); - buffer.writeln(' final Dio _dio;'); - - // 生成各个子API的私有字段 + buffer + ..writeln('/// 统一API客户端类') + ..writeln('/// 聚合所有分模块的API接口,提供统一的访问入口') + ..writeln('class $className {') + ..writeln(' final Dio _dio;'); final tagGroups = _groupPathsByTags(); for (final tagName in tagGroups.keys) { final className = _generateTagClassName(tagName); buffer.writeln( - ' late final $className _${StringUtils.toDartPropertyName(tagName)}Api;'); + ' late final $className _${StringUtils.toDartPropertyName(tagName)}Api;', + ); } - buffer.writeln(''); - - // 生成构造函数 - buffer.writeln(' $className(this._dio, {String? baseUrl}) {'); + buffer + ..writeln() + ..writeln(' $className(this._dio, {String? baseUrl}) {'); for (final tagName in tagGroups.keys) { final className = _generateTagClassName(tagName); final fieldName = '_${StringUtils.toDartPropertyName(tagName)}Api'; buffer.writeln(' $fieldName = $className(_dio, baseUrl: baseUrl);'); } - buffer.writeln(' }'); - - buffer.writeln(''); - - // 为每个 tag 生成一个获取器 + buffer + ..writeln(' }') + ..writeln(); for (final tagName in tagGroups.keys) { final className = _generateTagClassName(tagName); final fieldName = '_${StringUtils.toDartPropertyName(tagName)}Api'; - buffer.writeln(' /// ${tagName}相关API'); - buffer.writeln( - ' $className get ${StringUtils.toDartPropertyName(tagName)} => $fieldName;'); - buffer.writeln(''); + buffer + ..writeln(' /// $tagName相关API'); + ' $className get ${StringUtils.toDartPropertyName(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('}'); + buffer + ..writeln(' /// 获取Dio实例') + ..writeln(' Dio get dio => _dio;') + ..writeln() + ..writeln(' /// 设置认证token') + ..writeln(' void setAuthToken(String token) {'); + r" _dio.options.headers['Authorization'] = 'Bearer $token';", + ); + buffer + ..writeln(' }') + ..writeln() + ..writeln(' /// 清除认证token') + ..writeln(' void clearAuthToken() {') + ..writeln(" _dio.options.headers.remove('Authorization');") + ..writeln(' }') + ..writeln() + ..writeln(' /// 设置基础URL') + ..writeln(' void setBaseUrl(String baseUrl) {') + ..writeln(' _dio.options.baseUrl = baseUrl;') + ..writeln(' }') + ..writeln() + ..writeln(' /// 添加请求拦截器') + ..writeln(' void addRequestInterceptor(Interceptor interceptor) {') + ..writeln(' _dio.interceptors.add(interceptor);') + ..writeln(' }') + ..writeln() + ..writeln(' /// 添加响应拦截器') + ..writeln(' void addResponseInterceptor(Interceptor interceptor) {') + ..writeln(' _dio.interceptors.add(interceptor);') + ..writeln(' }') + ..writeln() + ..writeln(' /// 添加错误拦截器') + ..writeln(' void addErrorInterceptor(Interceptor interceptor) {') + ..writeln(' _dio.interceptors.add(interceptor);') + ..writeln(' }') + ..writeln() + ..writeln(' /// 创建带错误处理的API调用'); + ' Future callWithErrorHandling(Future Function() apiCall) async {', + ); + buffer + ..writeln(' try {') + ..writeln(' return await apiCall();') + ..writeln(' } on DioException catch (e) {') + ..writeln(' final error = ApiErrorHandler.handleDioError(e);') + ..writeln(' throw error;') + ..writeln(' } catch (e) {') + ..writeln(' final error = ApiError(') + ..writeln(' type: ApiErrorType.unknown,') + ..writeln(r" message: '未知错误: $e',") + ..writeln(' code: -1,') + ..writeln(' originalError: e,') + ..writeln(' );') + ..writeln(' throw error;') + ..writeln(' }') + ..writeln(' }') + ..writeln('}'); } /// 生成特定 tag 的导入语句 @@ -1354,25 +1386,24 @@ class RetrofitApiGenerator extends BaseGenerator { // 4. 相对路径导入 // 每组之间用空行分隔 print( - 'DEBUG: _generateTagImports called. useRetrofit: $useRetrofit, useDio: $useDio'); + 'DEBUG: _generateTagImports called. useRetrofit: $useRetrofit, useDio: $useDio', + ); // dart: 标准库导入 // dart: 标准库导入 - buffer.writeln('import \'dart:convert\';'); - buffer.writeln('import \'dart:io\';'); - buffer.writeln('import \'dart:typed_data\';'); - buffer.writeln(''); - - // package: 第三方库导入(按字母顺序) + buffer + ..writeln("import 'dart:convert';") + ..writeln("import 'dart:io';") + ..writeln("import 'dart:typed_data';") + ..writeln(); if (useRetrofit) { - buffer.writeln('import \'package:retrofit/retrofit.dart\';'); buffer - .writeln('import \'package:json_annotation/json_annotation.dart\';'); - // Retrofit 依赖 Dio,所以必须导入 - buffer.writeln('import \'package:dio/dio.dart\';'); + ..writeln("import 'package:retrofit/retrofit.dart';") + ..writeln("import 'package:json_annotation/json_annotation.dart';") + ..writeln("import 'package:dio/dio.dart';"); } else if (useDio) { - buffer.writeln('import \'package:dio/dio.dart\';'); + buffer.writeln("import 'package:dio/dio.dart';"); } // 添加配置的额外包导入 @@ -1382,36 +1413,37 @@ class RetrofitApiGenerator extends BaseGenerator { if (!import.contains('dio/dio.dart') && !import.contains('retrofit/retrofit.dart') && !import.contains('json_annotation/json_annotation.dart')) { - buffer.writeln('import \'$import\';'); + buffer.writeln("import '$import';"); } } // 其他工具包导入 - buffer.writeln('import \'package:crypto/crypto.dart\';'); - buffer.writeln('import \'package:path/path.dart\' as path;'); - buffer.writeln('import \'package:http_parser/http_parser.dart\';'); - buffer.writeln(''); - - // 相对路径导入(api_models/index.dart 会导出 base_result 和 base_page_result) - buffer.writeln('import \'../../api_models/index.dart\';'); - buffer.writeln(''); - - // 生成 part 声明 + buffer + ..writeln("import 'package:crypto/crypto.dart';") + ..writeln("import 'package:path/path.dart' as path;") + ..writeln("import 'package:http_parser/http_parser.dart';") + ..writeln() + ..writeln("import '../../api_models/index.dart';") + ..writeln(); final tagName = paths.first.tags.first; final fileName = _generateTagFileName(tagName); final partFileName = fileName.replaceAll('.dart', '.g.dart'); - buffer.writeln('part \'$partFileName\';'); - buffer.writeln(''); + buffer + ..writeln("part '$partFileName';") + ..writeln(); } /// 生成特定 tag 的 API 接口类 void _generateTagApiInterface( - StringBuffer buffer, String tagName, List paths) { + StringBuffer buffer, + String tagName, + List paths, + ) { final className = _generateTagClassName(tagName); - buffer.writeln('/// $tagName API 接口'); - buffer.writeln('/// 负责处理 $tagName 相关的接口'); - + buffer + ..writeln('/// $tagName API 接口') + ..writeln('/// 负责处理 $tagName 相关的接口'); if (useRetrofit) { buffer.writeln('@RestApi(parser: Parser.JsonSerializable)'); } @@ -1420,14 +1452,14 @@ class RetrofitApiGenerator extends BaseGenerator { if (useRetrofit) { buffer.writeln( - ' factory $className(Dio dio, {String? baseUrl}) = _$className;'); + ' factory $className(Dio dio, {String? baseUrl}) = _$className;', + ); } - buffer.writeln(''); - - buffer.writeln(' // ========== $tagName 相关接口 =========='); - buffer.writeln(''); - + buffer + ..writeln() + ..writeln(' // ========== $tagName 相关接口 ==========') + ..writeln(); for (final path in paths) { _generateApiMethod(buffer, path); } @@ -1501,82 +1533,6 @@ class RetrofitApiGenerator extends BaseGenerator { } /// 获取指定路径列表所需的模型导入 - 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; - } - - // 检查是否是分页响应类型(以 PageResponse 结尾) - if (cleanType.endsWith('PageResponse')) { - return; // 跳过分页响应类型,因为我们已经使用 BasePageResult 替代 - } - - // 添加模型类型到导入列表(使用转换后的类名) - imports.add(StringUtils.generateClassName(cleanType)); - } /// 判断是否为基本类型 bool _isBasicType(String type) { @@ -1591,6 +1547,7 @@ class RetrofitApiGenerator extends BaseGenerator { 'void', 'BaseResult', 'BasePageResult', + 'DateTime', }; // 检查基本类型 @@ -1608,20 +1565,23 @@ class RetrofitApiGenerator extends BaseGenerator { } /// 生成 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(''); - + void _generateTagManualImplementation( + StringBuffer buffer, + String tagName, + String className, + List paths, + ) { + buffer + ..writeln() + ..writeln('/// $className 的手动实现') + ..writeln('/// 使用 Dio 进行网络请求') + ..writeln('class ${className}Impl implements $className {') + ..writeln(' final Dio _dio;') + ..writeln() + ..writeln(' ${className}Impl(this._dio);') + ..writeln() + ..writeln(' // ========== $tagName 相关接口实现 ==========') + ..writeln(); for (final path in paths) { _generateManualMethodImplementation(buffer, path); } @@ -1637,32 +1597,32 @@ class RetrofitApiGenerator extends BaseGenerator { /// 生成参数实体类 void _generateParameterEntity( - ApiPath path, String className, List queryParams) { + ApiPath path, + String className, + List queryParams, + ) { // 注意:如果类名已存在,会覆盖之前的定义 // 这样可以确保后面版本的路径覆盖前面版本的参数实体类定义 // 例如:V2 的参数实体类会覆盖 V1 的同名参数实体类 final buffer = StringBuffer(); // 生成文件头注释 - buffer.writeln(generateFileHeader( - '参数实体类 - $className', - fileName: '${StringUtils.toSnakeCase(className)}.dart', - )); + buffer.writeln( + generateFileHeader( + '参数实体类 - $className', + fileName: '${StringUtils.toSnakeCase(className)}.dart', + ), + ); 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(checked: true, includeIfNull: false)'); - buffer.writeln('class $className {'); - - // 生成属性 + buffer + ..writeln() + ..writeln("import 'package:json_annotation/json_annotation.dart';") + ..writeln() + ..writeln("part '${StringUtils.toSnakeCase(className)}.g.dart';") + ..writeln() + ..writeln('@JsonSerializable(checked: true, includeIfNull: false)') + ..writeln('class $className {'); for (final param in queryParams) { final dartName = StringUtils.toDartPropertyName(param.name); final dartType = _getDartType(param.type); @@ -1675,10 +1635,12 @@ class RetrofitApiGenerator extends BaseGenerator { .replaceAll('\r', ' ') .trim(); buffer.writeln( - ' /// ${cleanDescription.isNotEmpty ? cleanDescription : param.name}'); - buffer.writeln(' @JsonKey(name: \'${param.name}\')'); - buffer.writeln(' final $dartType$nullable $dartName;'); - buffer.writeln(''); + ' /// ${cleanDescription.isNotEmpty ? cleanDescription : param.name}', + ); + buffer + ..writeln(" @JsonKey(name: '${param.name}')") + ..writeln(' final $dartType$nullable $dartName;') + ..writeln(); } // 生成构造函数 @@ -1688,34 +1650,31 @@ class RetrofitApiGenerator extends BaseGenerator { final required = param.required ? 'required ' : ''; buffer.writeln(' ${required}this.$dartName,'); } - buffer.writeln(' });'); - buffer.writeln(''); - - // 生成 fromJson 方法 + buffer + ..writeln(' });') + ..writeln(); 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 = {};'); + buffer + ..writeln(' _\$${className}FromJson(json);') + ..writeln(); + ' Map toJson() => _\$${className}ToJson(this);', + ); + buffer + ..writeln() + ..writeln(' /// 转换为查询参数 Map') + ..writeln(' Map toQueryMap() {') + ..writeln(' final map = {};'); for (final param in queryParams) { final dartName = StringUtils.toDartPropertyName(param.name); buffer.writeln( - ' if ($dartName != null) map[\'${param.name}\'] = $dartName;'); + " if ($dartName != null) map['${param.name}'] = $dartName;", + ); } - buffer.writeln(' return map;'); - buffer.writeln(' }'); - - buffer.writeln('}'); - + buffer + ..writeln(' return map;') + ..writeln(' }') + ..writeln('}'); _generatedParameterEntities[className] = buffer.toString(); } @@ -1764,13 +1723,6 @@ class RetrofitApiGenerator extends BaseGenerator { /// API 方法参数 class ApiMethodParameter { - final String name; - final String type; - final String annotation; - final bool required; - final String description; - final dynamic defaultValue; - ApiMethodParameter({ required this.name, required this.type, @@ -1779,6 +1731,12 @@ class ApiMethodParameter { this.description = '', this.defaultValue, }); + final String name; + final String type; + final String annotation; + final bool required; + final String description; + final dynamic defaultValue; } /// 组合模式处理扩展 @@ -1821,8 +1779,8 @@ extension CompositionSchemaExtension on RetrofitApiGenerator { for (final schemaData in schemas) { if (schemaData is Map) { // 如果是引用,直接返回引用的类型 - if (schemaData['\$ref'] != null) { - final ref = schemaData['\$ref'] as String; + if (schemaData[r'$ref'] != null) { + final ref = schemaData[r'$ref'] as String; final refName = ref.split('/').last; return StringUtils.generateClassName(refName); } @@ -1862,8 +1820,8 @@ extension CompositionSchemaExtension on RetrofitApiGenerator { // 检查是否所有 schema 都是引用类型 final refTypes = []; for (final schemaData in schemas) { - if (schemaData is Map && schemaData['\$ref'] != null) { - final ref = schemaData['\$ref'] as String; + if (schemaData is Map && schemaData[r'$ref'] != null) { + final ref = schemaData[r'$ref'] as String; final refName = ref.split('/').last; refTypes.add(StringUtils.generateClassName(refName)); } @@ -1906,13 +1864,13 @@ extension CompositionSchemaExtension on RetrofitApiGenerator { // 如果有映射表,我们可以根据映射生成联合类型 if (mapping.isNotEmpty) { final mappedTypes = []; - mapping.values.forEach((value) { + for (final value in mapping.values) { if (value is String) { // 提取引用的类型名 final refName = value.split('/').last; mappedTypes.add(StringUtils.generateClassName(refName)); } - }); + } if (mappedTypes.isNotEmpty) { // 返回第一个类型作为基类,或者 Object @@ -2018,229 +1976,255 @@ extension CompositionSchemaExtension on RetrofitApiGenerator { // 生成安全方案常量 if (document.components.securitySchemes.isNotEmpty) { - buffer.writeln('// Security Schemes'); - buffer.writeln('class SecuritySchemes {'); - + buffer + ..writeln('// Security Schemes') + ..writeln('class SecuritySchemes {'); document.components.securitySchemes.forEach((name, scheme) { buffer.writeln(' /// ${scheme.description}'); if (scheme.isApiKey) { buffer.writeln( - ' static const String ${StringUtils.generateConstantName(name)} = \'$name\';'); + " static const String ${StringUtils.generateConstantName(name)} = '$name';", + ); buffer.writeln( - ' static const String ${StringUtils.generateConstantName(name)}_PARAM = \'${scheme.name}\';'); + " static const String ${StringUtils.generateConstantName(name)}_PARAM = '${scheme.name}';", + ); buffer.writeln( - ' static const String ${StringUtils.generateConstantName(name)}_LOCATION = \'${scheme.location?.value}\';'); + " static const String ${StringUtils.generateConstantName(name)}_LOCATION = '${scheme.location?.value}';", + ); } else if (scheme.isHttp) { buffer.writeln( - ' static const String ${StringUtils.generateConstantName(name)} = \'$name\';'); + " static const String ${StringUtils.generateConstantName(name)} = '$name';", + ); buffer.writeln( - ' static const String ${StringUtils.generateConstantName(name)}_SCHEME = \'${scheme.scheme}\';'); + " static const String ${StringUtils.generateConstantName(name)}_SCHEME = '${scheme.scheme}';", + ); if (scheme.bearerFormat != null) { buffer.writeln( - ' static const String ${StringUtils.generateConstantName(name)}_FORMAT = \'${scheme.bearerFormat}\';'); + " static const String ${StringUtils.generateConstantName(name)}_FORMAT = '${scheme.bearerFormat}';", + ); } } else if (scheme.isOAuth2) { buffer.writeln( - ' static const String ${StringUtils.generateConstantName(name)} = \'$name\';'); - if (scheme.flows?.hasAnyFlow == true) { + " static const String ${StringUtils.generateConstantName(name)} = '$name';", + ); + if (scheme.flows?.hasAnyFlow ?? false) { final flows = scheme.flows!; - flows.availableFlows.forEach((flowType) { + for (final flowType in flows.availableFlows) { final flow = flows.getFlow(flowType); if (flow != null) { final flowName = StringUtils.generateConstantName( - '${name}_${flowType.value}'); + '${name}_${flowType.value}', + ); if (flow.hasAuthorizationUrl) { buffer.writeln( - ' static const String ${flowName}_AUTH_URL = \'${flow.authorizationUrl}\';'); + " static const String ${flowName}_AUTH_URL = '${flow.authorizationUrl}';", + ); } if (flow.hasTokenUrl) { buffer.writeln( - ' static const String ${flowName}_TOKEN_URL = \'${flow.tokenUrl}\';'); + " static const String ${flowName}_TOKEN_URL = '${flow.tokenUrl}';", + ); } if (flow.hasScopes) { buffer.writeln( - ' static const Map ${flowName}_SCOPES = {'); + ' static const Map ${flowName}_SCOPES = {', + ); flow.scopes.forEach((scope, description) { - buffer.writeln(' \'$scope\': \'$description\','); + buffer.writeln(" '$scope': '$description',"); }); buffer.writeln(' };'); } } - }); + } } } buffer.writeln(); }); - buffer.writeln('}'); - buffer.writeln(); + buffer + ..writeln('}') + ..writeln(); } // 生成安全拦截器 - buffer.writeln('// Security Interceptors'); - buffer.writeln('class ApiKeyInterceptor extends Interceptor {'); - buffer.writeln(' final String apiKey;'); - buffer.writeln(' final String paramName;'); - buffer.writeln(' final String location;'); - buffer.writeln(); - buffer.writeln(' ApiKeyInterceptor({'); - buffer.writeln(' required this.apiKey,'); - buffer.writeln(' required this.paramName,'); - buffer.writeln(' required this.location,'); - buffer.writeln(' });'); - buffer.writeln(); - buffer.writeln(' @override'); + buffer + ..writeln('// Security Interceptors') + ..writeln('class ApiKeyInterceptor extends Interceptor {') + ..writeln(' final String apiKey;') + ..writeln(' final String paramName;') + ..writeln(' final String location;') + ..writeln() + ..writeln(' ApiKeyInterceptor({') + ..writeln(' required this.apiKey,') + ..writeln(' required this.paramName,') + ..writeln(' required this.location,') + ..writeln(' });') + ..writeln() + ..writeln(' @override'); + ' void onRequest(RequestOptions options, RequestInterceptorHandler handler) {', + ); + buffer + ..writeln(' switch (location) {') + ..writeln(" case 'header':") + ..writeln(' options.headers[paramName] = apiKey;') + ..writeln(' break;') + ..writeln(" case 'query':") + ..writeln(' options.queryParameters[paramName] = apiKey;') + ..writeln(' break;') + ..writeln(" case 'cookie':"); + r" options.headers['Cookie'] = '$paramName=$apiKey';", + ); + buffer + ..writeln(' break;') + ..writeln(' }') + ..writeln(' handler.next(options);') + ..writeln(' }') + ..writeln('}') + ..writeln() + ..writeln('class BearerTokenInterceptor extends Interceptor {') + ..writeln(' final String token;') + ..writeln(' final String? tokenPrefix;') + ..writeln() + ..writeln(' BearerTokenInterceptor({') + ..writeln(' required this.token,') + ..writeln(" this.tokenPrefix = 'Bearer',") + ..writeln(' });') + ..writeln() + ..writeln(' @override'); + ' void onRequest(RequestOptions options, RequestInterceptorHandler handler) {', + ); + buffer + ..writeln(' if (tokenPrefix != null && tokenPrefix!.isNotEmpty) {'); + r" options.headers['Authorization'] = '$tokenPrefix $token';", + ); + buffer + ..writeln(' } else {') + ..writeln(" options.headers['Authorization'] = token;") + ..writeln(' }') + ..writeln(' handler.next(options);') + ..writeln(' }') + ..writeln('}') + ..writeln() + ..writeln('class BasicAuthInterceptor extends Interceptor {') + ..writeln(' final String username;') + ..writeln(' final String password;') + ..writeln() + ..writeln(' BasicAuthInterceptor({') + ..writeln(' required this.username,') + ..writeln(' required this.password,') + ..writeln(' });') + ..writeln() + ..writeln(' @override'); + ' void onRequest(RequestOptions options, RequestInterceptorHandler handler) {', + ); buffer.writeln( - ' void onRequest(RequestOptions options, RequestInterceptorHandler handler) {'); - buffer.writeln(' switch (location) {'); - buffer.writeln(' case \'header\':'); - buffer.writeln(' options.headers[paramName] = apiKey;'); - buffer.writeln(' break;'); - buffer.writeln(' case \'query\':'); - buffer.writeln(' options.queryParameters[paramName] = apiKey;'); - buffer.writeln(' break;'); - buffer.writeln(' case \'cookie\':'); + r" final credentials = base64Encode(utf8.encode('$username:$password'));", + ); buffer.writeln( - ' options.headers[\'Cookie\'] = \'\$paramName=\$apiKey\';'); - buffer.writeln(' break;'); - buffer.writeln(' }'); - buffer.writeln(' handler.next(options);'); - buffer.writeln(' }'); - buffer.writeln('}'); - buffer.writeln(); - - buffer.writeln('class BearerTokenInterceptor extends Interceptor {'); - buffer.writeln(' final String token;'); - buffer.writeln(' final String? tokenPrefix;'); - buffer.writeln(); - buffer.writeln(' BearerTokenInterceptor({'); - buffer.writeln(' required this.token,'); - buffer.writeln(' this.tokenPrefix = \'Bearer\','); - buffer.writeln(' });'); - buffer.writeln(); - buffer.writeln(' @override'); + r" options.headers['Authorization'] = 'Basic $credentials';", + ); + buffer + ..writeln(' handler.next(options);') + ..writeln(' }') + ..writeln('}') + ..writeln() + ..writeln('class DigestAuthInterceptor extends Interceptor {') + ..writeln(' final String username;') + ..writeln(' final String password;') + ..writeln(' String? _realm;') + ..writeln(' String? _nonce;') + ..writeln(' String? _qop;') + ..writeln(' String? _opaque;') + ..writeln() + ..writeln(' DigestAuthInterceptor({') + ..writeln(' required this.username,') + ..writeln(' required this.password,') + ..writeln(' });') + ..writeln() + ..writeln(' @override'); + ' void onRequest(RequestOptions options, RequestInterceptorHandler handler) {', + ); + buffer + ..writeln(' if (_nonce != null) {') + ..writeln(' final uri = options.uri.toString();') + ..writeln(' final method = options.method;'); + r" final ha1 = md5.convert(utf8.encode('$username:$_realm:$password')).toString();", + ); buffer.writeln( - ' void onRequest(RequestOptions options, RequestInterceptorHandler handler) {'); - buffer.writeln(' if (tokenPrefix != null && tokenPrefix!.isNotEmpty) {'); + r" final ha2 = md5.convert(utf8.encode('$method:$uri')).toString();", + ); buffer.writeln( - ' options.headers[\'Authorization\'] = \'\$tokenPrefix \$token\';'); - buffer.writeln(' } else {'); - buffer.writeln(' options.headers[\'Authorization\'] = token;'); - buffer.writeln(' }'); - buffer.writeln(' handler.next(options);'); - buffer.writeln(' }'); - buffer.writeln('}'); - buffer.writeln(); - - buffer.writeln('class BasicAuthInterceptor extends Interceptor {'); - buffer.writeln(' final String username;'); - buffer.writeln(' final String password;'); - buffer.writeln(); - buffer.writeln(' BasicAuthInterceptor({'); - buffer.writeln(' required this.username,'); - buffer.writeln(' required this.password,'); - buffer.writeln(' });'); - buffer.writeln(); - buffer.writeln(' @override'); + r" final response = md5.convert(utf8.encode('$ha1:$_nonce:$ha2')).toString();", + ); + buffer + ..writeln(); + ' final authHeader = \'Digest username="\$username", realm="\$_realm", \' +', + ); buffer.writeln( - ' void onRequest(RequestOptions options, RequestInterceptorHandler handler) {'); + ' \'nonce="\$_nonce", uri="\$uri", response="\$response"\';', + ); + buffer + ..writeln() + ..writeln(' if (_qop != null) {') + ..writeln(' // TODO: Implement qop support') + ..writeln(' }') + ..writeln() + ..writeln(' if (_opaque != null) {') + ..writeln(' // authHeader += \', opaque="\$_opaque"\';') + ..writeln(' }') + ..writeln() + ..writeln(" options.headers['Authorization'] = authHeader;") + ..writeln(' }') + ..writeln(' handler.next(options);') + ..writeln(' }') + ..writeln() + ..writeln(' @override'); + ' void onError(DioException err, ErrorInterceptorHandler handler) {', + ); + buffer + ..writeln(' if (err.response?.statusCode == 401) {'); + " final wwwAuth = err.response?.headers['www-authenticate']?.first;", + ); buffer.writeln( - ' final credentials = base64Encode(utf8.encode(\'\$username:\$password\'));'); - buffer.writeln( - ' options.headers[\'Authorization\'] = \'Basic \$credentials\';'); - buffer.writeln(' handler.next(options);'); - buffer.writeln(' }'); - buffer.writeln('}'); - buffer.writeln(); - - buffer.writeln('class DigestAuthInterceptor extends Interceptor {'); - buffer.writeln(' final String username;'); - buffer.writeln(' final String password;'); - buffer.writeln(' String? _realm;'); - buffer.writeln(' String? _nonce;'); - buffer.writeln(' String? _qop;'); - buffer.writeln(' String? _opaque;'); - buffer.writeln(); - buffer.writeln(' DigestAuthInterceptor({'); - buffer.writeln(' required this.username,'); - buffer.writeln(' required this.password,'); - buffer.writeln(' });'); - buffer.writeln(); - buffer.writeln(' @override'); - buffer.writeln( - ' void onRequest(RequestOptions options, RequestInterceptorHandler handler) {'); - buffer.writeln(' if (_nonce != null) {'); - buffer.writeln(' final uri = options.uri.toString();'); - buffer.writeln(' final method = options.method;'); - buffer.writeln( - ' final ha1 = md5.convert(utf8.encode(\'\$username:\$_realm:\$password\')).toString();'); - buffer.writeln( - ' final ha2 = md5.convert(utf8.encode(\'\$method:\$uri\')).toString();'); - buffer.writeln( - ' final response = md5.convert(utf8.encode(\'\$ha1:\$_nonce:\$ha2\')).toString();'); - buffer.writeln(); - buffer.writeln( - ' final authHeader = \'Digest username="\$username", realm="\$_realm", \' +'); - buffer.writeln( - ' \'nonce="\$_nonce", uri="\$uri", response="\$response"\';'); - buffer.writeln(); - buffer.writeln(' if (_qop != null) {'); - buffer.writeln(' // TODO: Implement qop support'); - buffer.writeln(' }'); - buffer.writeln(); - buffer.writeln(' if (_opaque != null) {'); - buffer.writeln(' // authHeader += \', opaque="\$_opaque"\';'); - buffer.writeln(' }'); - buffer.writeln(); - buffer.writeln(' options.headers[\'Authorization\'] = authHeader;'); - buffer.writeln(' }'); - buffer.writeln(' handler.next(options);'); - buffer.writeln(' }'); - buffer.writeln(); - buffer.writeln(' @override'); - buffer.writeln( - ' void onError(DioException err, ErrorInterceptorHandler handler) {'); - buffer.writeln(' if (err.response?.statusCode == 401) {'); - buffer.writeln( - ' final wwwAuth = err.response?.headers[\'www-authenticate\']?.first;'); - buffer.writeln( - ' if (wwwAuth != null && wwwAuth.startsWith(\'Digest\')) {'); - buffer.writeln(' _parseDigestChallenge(wwwAuth);'); - buffer.writeln(' // Retry the request with digest auth'); - buffer.writeln(' final options = err.requestOptions;'); - buffer.writeln(' onRequest(options, RequestInterceptorHandler());'); - buffer.writeln( - ' // Note: In real implementation, you would retry the request here'); - buffer.writeln(' }'); - buffer.writeln(' }'); - buffer.writeln(' handler.next(err);'); - buffer.writeln(' }'); - buffer.writeln(); - buffer.writeln(' void _parseDigestChallenge(String challenge) {'); - buffer.writeln(' final regex = RegExp(r\'(\\w+)="([^"]+)"\');'); - buffer.writeln(' final matches = regex.allMatches(challenge);'); - buffer.writeln(' for (final match in matches) {'); - buffer.writeln(' final key = match.group(1);'); - buffer.writeln(' final value = match.group(2);'); - buffer.writeln(' switch (key) {'); - buffer.writeln(' case \'realm\':'); - buffer.writeln(' _realm = value;'); - buffer.writeln(' break;'); - buffer.writeln(' case \'nonce\':'); - buffer.writeln(' _nonce = value;'); - buffer.writeln(' break;'); - buffer.writeln(' case \'qop\':'); - buffer.writeln(' _qop = value;'); - buffer.writeln(' break;'); - buffer.writeln(' case \'opaque\':'); - buffer.writeln(' _opaque = value;'); - buffer.writeln(' break;'); - buffer.writeln(' }'); - buffer.writeln(' }'); - buffer.writeln(' }'); - buffer.writeln('}'); - buffer.writeln(); - + " if (wwwAuth != null && wwwAuth.startsWith('Digest')) {", + ); + buffer + ..writeln(' _parseDigestChallenge(wwwAuth);') + ..writeln(' // Retry the request with digest auth') + ..writeln(' final options = err.requestOptions;') + ..writeln(' onRequest(options, RequestInterceptorHandler());'); + ' // Note: In real implementation, you would retry the request here', + ); + buffer + ..writeln(' }') + ..writeln(' }') + ..writeln(' handler.next(err);') + ..writeln(' }') + ..writeln() + ..writeln(' void _parseDigestChallenge(String challenge) {') + ..writeln(' final regex = RegExp(r\'(\\w+)="([^"]+)"\');') + ..writeln(' final matches = regex.allMatches(challenge);') + ..writeln(' for (final match in matches) {') + ..writeln(' final key = match.group(1);') + ..writeln(' final value = match.group(2);') + ..writeln(' switch (key) {') + ..writeln(" case 'realm':") + ..writeln(' _realm = value;') + ..writeln(' break;') + ..writeln(" case 'nonce':") + ..writeln(' _nonce = value;') + ..writeln(' break;') + ..writeln(" case 'qop':") + ..writeln(' _qop = value;') + ..writeln(' break;') + ..writeln(" case 'opaque':") + ..writeln(' _opaque = value;') + ..writeln(' break;') + ..writeln(' }') + ..writeln(' }') + ..writeln(' }') + ..writeln('}') + ..writeln(); return buffer.toString(); } @@ -2248,114 +2232,115 @@ extension CompositionSchemaExtension on RetrofitApiGenerator { String _generateMediaTypeHandlers() { final buffer = StringBuffer(); - buffer.writeln('// Media Type Handlers'); - buffer.writeln('class MediaTypeHandler {'); - buffer.writeln(' /// 处理 JSON 数据'); - buffer.writeln(' static Map handleJson(dynamic data) {'); - buffer.writeln(' if (data is String) {'); - buffer.writeln(' return jsonDecode(data) as Map;'); - buffer.writeln(' } else if (data is Map) {'); - buffer.writeln(' return data;'); - buffer.writeln(' }'); - buffer.writeln(' throw ArgumentError(\'Invalid JSON data type\');'); - buffer.writeln(' }'); - buffer.writeln(); - - buffer.writeln(' /// 处理 XML 数据'); - buffer.writeln(' static String handleXml(dynamic data) {'); - buffer.writeln(' if (data is String) {'); - buffer.writeln(' return data;'); - buffer.writeln(' }'); - buffer.writeln(' return data.toString();'); - buffer.writeln(' }'); - buffer.writeln(); - - buffer.writeln(' /// 处理表单数据'); - buffer.writeln( - ' static FormData handleFormData(Map data) {'); - buffer.writeln(' final formData = FormData();'); - buffer.writeln(' data.forEach((key, value) {'); - buffer.writeln(' if (value is MultipartFile) {'); - buffer.writeln(' formData.files.add(MapEntry(key, value));'); - buffer.writeln(' } else if (value is List) {'); - buffer.writeln(' for (final file in value) {'); - buffer.writeln(' formData.files.add(MapEntry(key, file));'); - buffer.writeln(' }'); - buffer.writeln(' } else {'); - buffer.writeln( - ' formData.fields.add(MapEntry(key, value.toString()));'); - buffer.writeln(' }'); - buffer.writeln(' });'); - buffer.writeln(' return formData;'); - buffer.writeln(' }'); - buffer.writeln(); - - buffer.writeln(' /// 处理 URL 编码表单数据'); - buffer.writeln( - ' static String handleUrlEncodedForm(Map data) {'); - buffer.writeln(' final params = [];'); - buffer.writeln(' data.forEach((key, value) {'); - buffer.writeln(' final encodedKey = Uri.encodeComponent(key);'); - buffer.writeln( - ' final encodedValue = Uri.encodeComponent(value.toString());'); - buffer.writeln(' params.add(\'\$encodedKey=\$encodedValue\');'); - buffer.writeln(' });'); - buffer.writeln(' return params.join(\'&\');'); - buffer.writeln(' }'); - buffer.writeln(); - - buffer.writeln(' /// 处理二进制数据'); - buffer.writeln(' static List handleBinary(dynamic data) {'); - buffer.writeln(' if (data is List) {'); - buffer.writeln(' return data;'); - buffer.writeln(' } else if (data is String) {'); - buffer.writeln(' return utf8.encode(data);'); - buffer.writeln(' }'); - buffer.writeln(' throw ArgumentError(\'Invalid binary data type\');'); - buffer.writeln(' }'); - buffer.writeln(); - - buffer.writeln(' /// 处理文本数据'); - buffer.writeln(' static String handleText(dynamic data) {'); - buffer.writeln(' return data.toString();'); - buffer.writeln(' }'); - buffer.writeln(); - - buffer.writeln(' /// 根据媒体类型处理数据'); - buffer.writeln( - ' static dynamic handleByMediaType(String mediaType, dynamic data) {'); - buffer.writeln(' switch (mediaType.toLowerCase()) {'); - buffer.writeln(' case \'application/json\':'); - buffer.writeln(' return handleJson(data);'); - buffer.writeln(' case \'application/xml\':'); - buffer.writeln(' case \'text/xml\':'); - buffer.writeln(' return handleXml(data);'); - buffer.writeln(' case \'multipart/form-data\':'); - buffer.writeln( - ' return handleFormData(data as Map);'); - buffer.writeln(' case \'application/x-www-form-urlencoded\':'); - buffer.writeln( - ' return handleUrlEncodedForm(data as Map);'); - buffer.writeln(' case \'application/octet-stream\':'); - buffer.writeln(' case \'application/pdf\':'); - buffer.writeln(' case \'image/png\':'); - buffer.writeln(' case \'image/jpeg\':'); - buffer.writeln(' case \'image/gif\':'); - buffer.writeln(' case \'audio/mpeg\':'); - buffer.writeln(' case \'video/mp4\':'); - buffer.writeln(' return handleBinary(data);'); - buffer.writeln(' case \'text/plain\':'); - buffer.writeln(' case \'text/html\':'); - buffer.writeln(' case \'text/csv\':'); - buffer.writeln(' case \'image/svg+xml\':'); - buffer.writeln(' return handleText(data);'); - buffer.writeln(' default:'); - buffer.writeln(' return data;'); - buffer.writeln(' }'); - buffer.writeln(' }'); - buffer.writeln('}'); - buffer.writeln(); - + buffer + ..writeln('// Media Type Handlers') + ..writeln('class MediaTypeHandler {') + ..writeln(' /// 处理 JSON 数据') + ..writeln(' static Map handleJson(dynamic data) {') + ..writeln(' if (data is String) {') + ..writeln(' return jsonDecode(data) as Map;') + ..writeln(' } else if (data is Map) {') + ..writeln(' return data;') + ..writeln(' }') + ..writeln(" throw ArgumentError('Invalid JSON data type');") + ..writeln(' }') + ..writeln() + ..writeln(' /// 处理 XML 数据') + ..writeln(' static String handleXml(dynamic data) {') + ..writeln(' if (data is String) {') + ..writeln(' return data;') + ..writeln(' }') + ..writeln(' return data.toString();') + ..writeln(' }') + ..writeln() + ..writeln(' /// 处理表单数据'); + ' static FormData handleFormData(Map data) {', + ); + buffer + ..writeln(' final formData = FormData();') + ..writeln(' data.forEach((key, value) {') + ..writeln(' if (value is MultipartFile) {') + ..writeln(' formData.files.add(MapEntry(key, value));') + ..writeln(' } else if (value is List) {') + ..writeln(' for (final file in value) {') + ..writeln(' formData.files.add(MapEntry(key, file));') + ..writeln(' }') + ..writeln(' } else {'); + ' formData.fields.add(MapEntry(key, value.toString()));', + ); + buffer + ..writeln(' }') + ..writeln(' });') + ..writeln(' return formData;') + ..writeln(' }') + ..writeln() + ..writeln(' /// 处理 URL 编码表单数据'); + ' static String handleUrlEncodedForm(Map data) {', + ); + buffer + ..writeln(' final params = [];') + ..writeln(' data.forEach((key, value) {') + ..writeln(' final encodedKey = Uri.encodeComponent(key);'); + ' final encodedValue = Uri.encodeComponent(value.toString());', + ); + buffer + ..writeln(r" params.add('$encodedKey=$encodedValue');") + ..writeln(' });') + ..writeln(" return params.join('&');") + ..writeln(' }') + ..writeln() + ..writeln(' /// 处理二进制数据') + ..writeln(' static List handleBinary(dynamic data) {') + ..writeln(' if (data is List) {') + ..writeln(' return data;') + ..writeln(' } else if (data is String) {') + ..writeln(' return utf8.encode(data);') + ..writeln(' }') + ..writeln(" throw ArgumentError('Invalid binary data type');") + ..writeln(' }') + ..writeln() + ..writeln(' /// 处理文本数据') + ..writeln(' static String handleText(dynamic data) {') + ..writeln(' return data.toString();') + ..writeln(' }') + ..writeln() + ..writeln(' /// 根据媒体类型处理数据'); + ' static dynamic handleByMediaType(String mediaType, dynamic data) {', + ); + buffer + ..writeln(' switch (mediaType.toLowerCase()) {') + ..writeln(" case 'application/json':") + ..writeln(' return handleJson(data);') + ..writeln(" case 'application/xml':") + ..writeln(" case 'text/xml':") + ..writeln(' return handleXml(data);') + ..writeln(" case 'multipart/form-data':"); + ' return handleFormData(data as Map);', + ); + buffer + ..writeln(" case 'application/x-www-form-urlencoded':"); + ' return handleUrlEncodedForm(data as Map);', + ); + buffer + ..writeln(" case 'application/octet-stream':") + ..writeln(" case 'application/pdf':") + ..writeln(" case 'image/png':") + ..writeln(" case 'image/jpeg':") + ..writeln(" case 'image/gif':") + ..writeln(" case 'audio/mpeg':") + ..writeln(" case 'video/mp4':") + ..writeln(' return handleBinary(data);') + ..writeln(" case 'text/plain':") + ..writeln(" case 'text/html':") + ..writeln(" case 'text/csv':") + ..writeln(" case 'image/svg+xml':") + ..writeln(' return handleText(data);') + ..writeln(' default:') + ..writeln(' return data;') + ..writeln(' }') + ..writeln(' }') + ..writeln('}') + ..writeln(); return buffer.toString(); } @@ -2363,183 +2348,185 @@ extension CompositionSchemaExtension on RetrofitApiGenerator { String _generateFileUploadHandlers() { final buffer = StringBuffer(); - buffer.writeln('// File Upload Handlers'); - buffer.writeln('class FileUploadHandler {'); - buffer.writeln(' /// 创建单个文件的 MultipartFile'); - buffer.writeln(' static Future createMultipartFile({'); - buffer.writeln(' required String filePath,'); - buffer.writeln(' String? filename,'); - buffer.writeln(' String? contentType,'); - buffer.writeln(' }) async {'); - buffer.writeln(' return MultipartFile.fromFile('); - buffer.writeln(' filePath,'); - buffer.writeln(' filename: filename ?? path.basename(filePath),'); + buffer + ..writeln('// File Upload Handlers') + ..writeln('class FileUploadHandler {') + ..writeln(' /// 创建单个文件的 MultipartFile') + ..writeln(' static Future createMultipartFile({') + ..writeln(' required String filePath,') + ..writeln(' String? filename,') + ..writeln(' String? contentType,') + ..writeln(' }) async {') + ..writeln(' return MultipartFile.fromFile(') + ..writeln(' filePath,') + ..writeln(' filename: filename ?? path.basename(filePath),'); + ' contentType: contentType != null ? MediaType.parse(contentType) : null,', + ); + buffer + ..writeln(' );') + ..writeln(' }') + ..writeln() + ..writeln(' /// 从字节数组创建 MultipartFile') + ..writeln(' static MultipartFile createMultipartFileFromBytes({') + ..writeln(' required List bytes,') + ..writeln(' required String filename,') + ..writeln(' String? contentType,') + ..writeln(' }) {') + ..writeln(' return MultipartFile.fromBytes(') + ..writeln(' bytes,') + ..writeln(' filename: filename,'); + ' contentType: contentType != null ? MediaType.parse(contentType) : null,', + ); + buffer + ..writeln(' );') + ..writeln(' }') + ..writeln() + ..writeln(' /// 从流创建 MultipartFile') + ..writeln(' static MultipartFile createMultipartFileFromStream({') + ..writeln(' required Stream> stream,') + ..writeln(' required int length,') + ..writeln(' required String filename,') + ..writeln(' String? contentType,') + ..writeln(' }) {') + ..writeln(' return MultipartFile(') + ..writeln(' stream,') + ..writeln(' length,') + ..writeln(' filename: filename,'); + ' contentType: contentType != null ? MediaType.parse(contentType) : null,', + ); + buffer + ..writeln(' );') + ..writeln(' }') + ..writeln() + ..writeln(' /// 创建图片文件的 MultipartFile') + ..writeln(' static Future createImageFile({') + ..writeln(' required String filePath,') + ..writeln(' String? filename,') + ..writeln(' }) async {'); + ' final extension = path.extension(filePath).toLowerCase();', + ); + buffer + ..writeln(' String? contentType;') + ..writeln(' switch (extension) {') + ..writeln(" case '.jpg':") + ..writeln(" case '.jpeg':") + ..writeln(" contentType = 'image/jpeg';") + ..writeln(' break;') + ..writeln(" case '.png':") + ..writeln(" contentType = 'image/png';") + ..writeln(' break;') + ..writeln(" case '.gif':") + ..writeln(" contentType = 'image/gif';") + ..writeln(' break;') + ..writeln(" case '.svg':") + ..writeln(" contentType = 'image/svg+xml';") + ..writeln(' break;') + ..writeln(' default:') + ..writeln(" contentType = 'application/octet-stream';") + ..writeln(' }') + ..writeln(' return createMultipartFile(') + ..writeln(' filePath: filePath,') + ..writeln(' filename: filename,') + ..writeln(' contentType: contentType,') + ..writeln(' );') + ..writeln(' }') + ..writeln() + ..writeln(' /// 创建音频文件的 MultipartFile') + ..writeln(' static Future createAudioFile({') + ..writeln(' required String filePath,') + ..writeln(' String? filename,') + ..writeln(' }) async {'); + ' final extension = path.extension(filePath).toLowerCase();', + ); + buffer + ..writeln(' String? contentType;') + ..writeln(' switch (extension) {') + ..writeln(" case '.mp3':") + ..writeln(" contentType = 'audio/mpeg';") + ..writeln(' break;') + ..writeln(" case '.wav':") + ..writeln(" contentType = 'audio/wav';") + ..writeln(' break;') + ..writeln(" case '.ogg':") + ..writeln(" contentType = 'audio/ogg';") + ..writeln(' break;') + ..writeln(' default:') + ..writeln(" contentType = 'audio/mpeg';") + ..writeln(' }') + ..writeln(' return createMultipartFile(') + ..writeln(' filePath: filePath,') + ..writeln(' filename: filename,') + ..writeln(' contentType: contentType,') + ..writeln(' );') + ..writeln(' }') + ..writeln() + ..writeln(' /// 创建视频文件的 MultipartFile') + ..writeln(' static Future createVideoFile({') + ..writeln(' required String filePath,') + ..writeln(' String? filename,') + ..writeln(' }) async {'); + ' final extension = path.extension(filePath).toLowerCase();', + ); + buffer + ..writeln(' String? contentType;') + ..writeln(' switch (extension) {') + ..writeln(" case '.mp4':") + ..writeln(" contentType = 'video/mp4';") + ..writeln(' break;') + ..writeln(" case '.avi':") + ..writeln(" contentType = 'video/x-msvideo';") + ..writeln(' break;') + ..writeln(" case '.mov':") + ..writeln(" contentType = 'video/quicktime';") + ..writeln(' break;') + ..writeln(' default:') + ..writeln(" contentType = 'video/mp4';") + ..writeln(' }') + ..writeln(' return createMultipartFile(') + ..writeln(' filePath: filePath,') + ..writeln(' filename: filename,') + ..writeln(' contentType: contentType,') + ..writeln(' );') + ..writeln(' }') + ..writeln() + ..writeln(' /// 验证文件大小'); + ' static bool validateFileSize(String filePath, int maxSizeInBytes) {', + ); + buffer + ..writeln(' final file = File(filePath);') + ..writeln(' if (!file.existsSync()) return false;') + ..writeln(' return file.lengthSync() <= maxSizeInBytes;') + ..writeln(' }') + ..writeln() + ..writeln(' /// 验证文件类型'); + ' static bool validateFileType(String filePath, List allowedExtensions) {', + ); buffer.writeln( - ' contentType: contentType != null ? MediaType.parse(contentType) : null,'); - buffer.writeln(' );'); - buffer.writeln(' }'); - buffer.writeln(); - - buffer.writeln(' /// 从字节数组创建 MultipartFile'); - buffer.writeln(' static MultipartFile createMultipartFileFromBytes({'); - buffer.writeln(' required List bytes,'); - buffer.writeln(' required String filename,'); - buffer.writeln(' String? contentType,'); - buffer.writeln(' }) {'); - buffer.writeln(' return MultipartFile.fromBytes('); - buffer.writeln(' bytes,'); - buffer.writeln(' filename: filename,'); - buffer.writeln( - ' contentType: contentType != null ? MediaType.parse(contentType) : null,'); - buffer.writeln(' );'); - buffer.writeln(' }'); - buffer.writeln(); - - buffer.writeln(' /// 从流创建 MultipartFile'); - buffer.writeln(' static MultipartFile createMultipartFileFromStream({'); - buffer.writeln(' required Stream> stream,'); - buffer.writeln(' required int length,'); - buffer.writeln(' required String filename,'); - buffer.writeln(' String? contentType,'); - buffer.writeln(' }) {'); - buffer.writeln(' return MultipartFile('); - buffer.writeln(' stream,'); - buffer.writeln(' length,'); - buffer.writeln(' filename: filename,'); - buffer.writeln( - ' contentType: contentType != null ? MediaType.parse(contentType) : null,'); - buffer.writeln(' );'); - buffer.writeln(' }'); - buffer.writeln(); - - buffer.writeln(' /// 创建图片文件的 MultipartFile'); - buffer.writeln(' static Future createImageFile({'); - buffer.writeln(' required String filePath,'); - buffer.writeln(' String? filename,'); - buffer.writeln(' }) async {'); - buffer.writeln( - ' final extension = path.extension(filePath).toLowerCase();'); - buffer.writeln(' String? contentType;'); - buffer.writeln(' switch (extension) {'); - buffer.writeln(' case \'.jpg\':'); - buffer.writeln(' case \'.jpeg\':'); - buffer.writeln(' contentType = \'image/jpeg\';'); - buffer.writeln(' break;'); - buffer.writeln(' case \'.png\':'); - buffer.writeln(' contentType = \'image/png\';'); - buffer.writeln(' break;'); - buffer.writeln(' case \'.gif\':'); - buffer.writeln(' contentType = \'image/gif\';'); - buffer.writeln(' break;'); - buffer.writeln(' case \'.svg\':'); - buffer.writeln(' contentType = \'image/svg+xml\';'); - buffer.writeln(' break;'); - buffer.writeln(' default:'); - buffer.writeln(' contentType = \'application/octet-stream\';'); - buffer.writeln(' }'); - buffer.writeln(' return createMultipartFile('); - buffer.writeln(' filePath: filePath,'); - buffer.writeln(' filename: filename,'); - buffer.writeln(' contentType: contentType,'); - buffer.writeln(' );'); - buffer.writeln(' }'); - buffer.writeln(); - - buffer.writeln(' /// 创建音频文件的 MultipartFile'); - buffer.writeln(' static Future createAudioFile({'); - buffer.writeln(' required String filePath,'); - buffer.writeln(' String? filename,'); - buffer.writeln(' }) async {'); - buffer.writeln( - ' final extension = path.extension(filePath).toLowerCase();'); - buffer.writeln(' String? contentType;'); - buffer.writeln(' switch (extension) {'); - buffer.writeln(' case \'.mp3\':'); - buffer.writeln(' contentType = \'audio/mpeg\';'); - buffer.writeln(' break;'); - buffer.writeln(' case \'.wav\':'); - buffer.writeln(' contentType = \'audio/wav\';'); - buffer.writeln(' break;'); - buffer.writeln(' case \'.ogg\':'); - buffer.writeln(' contentType = \'audio/ogg\';'); - buffer.writeln(' break;'); - buffer.writeln(' default:'); - buffer.writeln(' contentType = \'audio/mpeg\';'); - buffer.writeln(' }'); - buffer.writeln(' return createMultipartFile('); - buffer.writeln(' filePath: filePath,'); - buffer.writeln(' filename: filename,'); - buffer.writeln(' contentType: contentType,'); - buffer.writeln(' );'); - buffer.writeln(' }'); - buffer.writeln(); - - buffer.writeln(' /// 创建视频文件的 MultipartFile'); - buffer.writeln(' static Future createVideoFile({'); - buffer.writeln(' required String filePath,'); - buffer.writeln(' String? filename,'); - buffer.writeln(' }) async {'); - buffer.writeln( - ' final extension = path.extension(filePath).toLowerCase();'); - buffer.writeln(' String? contentType;'); - buffer.writeln(' switch (extension) {'); - buffer.writeln(' case \'.mp4\':'); - buffer.writeln(' contentType = \'video/mp4\';'); - buffer.writeln(' break;'); - buffer.writeln(' case \'.avi\':'); - buffer.writeln(' contentType = \'video/x-msvideo\';'); - buffer.writeln(' break;'); - buffer.writeln(' case \'.mov\':'); - buffer.writeln(' contentType = \'video/quicktime\';'); - buffer.writeln(' break;'); - buffer.writeln(' default:'); - buffer.writeln(' contentType = \'video/mp4\';'); - buffer.writeln(' }'); - buffer.writeln(' return createMultipartFile('); - buffer.writeln(' filePath: filePath,'); - buffer.writeln(' filename: filename,'); - buffer.writeln(' contentType: contentType,'); - buffer.writeln(' );'); - buffer.writeln(' }'); - buffer.writeln(); - - buffer.writeln(' /// 验证文件大小'); - buffer.writeln( - ' static bool validateFileSize(String filePath, int maxSizeInBytes) {'); - buffer.writeln(' final file = File(filePath);'); - buffer.writeln(' if (!file.existsSync()) return false;'); - buffer.writeln(' return file.lengthSync() <= maxSizeInBytes;'); - buffer.writeln(' }'); - buffer.writeln(); - - buffer.writeln(' /// 验证文件类型'); - buffer.writeln( - ' static bool validateFileType(String filePath, List allowedExtensions) {'); - buffer.writeln( - ' final extension = path.extension(filePath).toLowerCase();'); - buffer.writeln(' return allowedExtensions.contains(extension);'); - buffer.writeln(' }'); - buffer.writeln(); - - buffer.writeln(' /// 批量创建文件'); + ' final extension = path.extension(filePath).toLowerCase();', + ); + buffer + ..writeln(' return allowedExtensions.contains(extension);') + ..writeln(' }') + ..writeln() + ..writeln(' /// 批量创建文件'); buffer .writeln(' static Future> createMultipleFiles({'); - buffer.writeln(' required List filePaths,'); - buffer.writeln(' String? contentType,'); - buffer.writeln(' }) async {'); - buffer.writeln(' final files = [];'); - buffer.writeln(' for (final filePath in filePaths) {'); - buffer.writeln(' final file = await createMultipartFile('); - buffer.writeln(' filePath: filePath,'); - buffer.writeln(' contentType: contentType,'); - buffer.writeln(' );'); - buffer.writeln(' files.add(file);'); - buffer.writeln(' }'); - buffer.writeln(' return files;'); - buffer.writeln(' }'); - buffer.writeln('}'); - buffer.writeln(); - + buffer + ..writeln(' required List filePaths,') + ..writeln(' String? contentType,') + ..writeln(' }) async {') + ..writeln(' final files = [];') + ..writeln(' for (final filePath in filePaths) {') + ..writeln(' final file = await createMultipartFile(') + ..writeln(' filePath: filePath,') + ..writeln(' contentType: contentType,') + ..writeln(' );') + ..writeln(' files.add(file);') + ..writeln(' }') + ..writeln(' return files;') + ..writeln(' }') + ..writeln('}') + ..writeln(); return buffer.toString(); } @@ -2547,180 +2534,186 @@ extension CompositionSchemaExtension on RetrofitApiGenerator { String _generateEncodingHandlers() { final buffer = StringBuffer(); - buffer.writeln('// Encoding Handlers'); - buffer.writeln('class EncodingHandler {'); - buffer.writeln(' /// 支持的字符编码'); + buffer + ..writeln('// Encoding Handlers') + ..writeln('class EncodingHandler {') + ..writeln(' /// 支持的字符编码'); buffer .writeln(' static const Map supportedEncodings = {'); - buffer.writeln(' \'utf-8\': utf8,'); - buffer.writeln(' \'utf8\': utf8,'); - buffer.writeln(' \'ascii\': ascii,'); - buffer.writeln(' \'latin1\': latin1,'); - buffer.writeln(' \'iso-8859-1\': latin1,'); - buffer.writeln(' };'); + buffer + ..writeln(" 'utf-8': utf8,") + ..writeln(" 'utf8': utf8,") + ..writeln(" 'ascii': ascii,") + ..writeln(" 'latin1': latin1,") + ..writeln(" 'iso-8859-1': latin1,") + ..writeln(' };') + ..writeln() + ..writeln(' /// 根据编码名称获取编码器') + ..writeln(' static Encoding getEncoding(String? encodingName) {') + ..writeln(' if (encodingName == null) return utf8;'); + " final normalizedName = encodingName.toLowerCase().replaceAll('_', '-');", + ); + buffer + ..writeln(' return supportedEncodings[normalizedName] ?? utf8;') + ..writeln(' }') + ..writeln() + ..writeln(' /// 编码字符串'); + ' static List encodeString(String data, [String? encodingName]) {', + ); + buffer + ..writeln(' final encoding = getEncoding(encodingName);') + ..writeln(' return encoding.encode(data);') + ..writeln(' }') + ..writeln() + ..writeln(' /// 解码字节数组'); + ' static String decodeBytes(List bytes, [String? encodingName]) {', + ); + buffer + ..writeln(' final encoding = getEncoding(encodingName);') + ..writeln(' return encoding.decode(bytes);') + ..writeln(' }') + ..writeln() + ..writeln(' /// Base64 编码') + ..writeln(' static String encodeBase64(List bytes) {') + ..writeln(' return base64Encode(bytes);') + ..writeln(' }') + ..writeln() + ..writeln(' /// Base64 解码') + ..writeln(' static List decodeBase64(String data) {') + ..writeln(' return base64Decode(data);') + ..writeln(' }') + ..writeln() + ..writeln(' /// URL 编码') + ..writeln(' static String encodeUrl(String data) {') + ..writeln(' return Uri.encodeComponent(data);') + ..writeln(' }') + ..writeln() + ..writeln(' /// URL 解码') + ..writeln(' static String decodeUrl(String data) {') + ..writeln(' return Uri.decodeComponent(data);') + ..writeln(' }') + ..writeln() + ..writeln(' /// 处理 Content-Encoding'); + ' static List handleContentEncoding(List data, String? encoding) {', + ); + buffer + ..writeln(' if (encoding == null) return data;') + ..writeln(' switch (encoding.toLowerCase()) {') + ..writeln(" case 'gzip':") + ..writeln(' return gzip.decode(data);') + ..writeln(" case 'deflate':") + ..writeln(' return zlib.decode(data);') + ..writeln(" case 'br':") + ..writeln(' // Brotli 解码需要额外的包支持'); + " throw UnsupportedError('Brotli encoding not supported');", + ); + buffer + ..writeln(" case 'identity':") + ..writeln(' default:') + ..writeln(' return data;') + ..writeln(' }') + ..writeln(' }') + ..writeln() + ..writeln(' /// 处理 Transfer-Encoding'); + ' static List handleTransferEncoding(List data, String? encoding) {', + ); + buffer + ..writeln(' if (encoding == null) return data;') + ..writeln(' switch (encoding.toLowerCase()) {') + ..writeln(" case 'chunked':") + ..writeln(' return _decodeChunked(data);') + ..writeln(" case 'compress':"); + " throw UnsupportedError('Compress transfer encoding not supported');", + ); + buffer + ..writeln(" case 'deflate':") + ..writeln(' return zlib.decode(data);') + ..writeln(" case 'gzip':") + ..writeln(' return gzip.decode(data);') + ..writeln(" case 'identity':") + ..writeln(' default:') + ..writeln(' return data;') + ..writeln(' }') + ..writeln(' }') + ..writeln() + ..writeln(' /// 解码分块传输编码') + ..writeln(' static List _decodeChunked(List data) {') + ..writeln(' final result = [];') + ..writeln(' var offset = 0;') + ..writeln(' while (offset < data.length) {') + ..writeln(' // 查找块大小行的结束') + ..writeln(' var lineEnd = offset;') + ..writeln(' while (lineEnd < data.length - 1) {'); + r' if (data[lineEnd] == 13 && data[lineEnd + 1] == 10) break; // \r\n', + ); + buffer + ..writeln(' lineEnd++;') + ..writeln(' }') + ..writeln(' if (lineEnd >= data.length - 1) break;') + ..writeln() + ..writeln(' // 解析块大小'); + ' final sizeHex = String.fromCharCodes(data.sublist(offset, lineEnd));', + ); + buffer.writeln( + ' final chunkSize = int.tryParse(sizeHex, radix: 16) ?? 0;', + ); + buffer + ..writeln(' if (chunkSize == 0) break; // 最后一个块') + ..writeln() + ..writeln(r' // 跳过 \r\n') + ..writeln(' offset = lineEnd + 2;') + ..writeln() + ..writeln(' // 读取块数据') + ..writeln(' if (offset + chunkSize <= data.length) {'); + ' result.addAll(data.sublist(offset, offset + chunkSize));', + ); + buffer + ..writeln(r' offset += chunkSize + 2; // 跳过块数据后的 \r\n') + ..writeln(' } else {') + ..writeln(' break;') + ..writeln(' }') + ..writeln(' }') + ..writeln(' return result;') + ..writeln(' }') + ..writeln() + ..writeln(' /// 检测字符编码'); + buffer + ..writeln(' static String? detectEncoding(List bytes) {') + ..writeln(' // 检测 BOM') + ..writeln(' if (bytes.length >= 3) {') + ..writeln( + ' if (bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF) {', + ) + ..writeln(" return 'utf-8';") + ..writeln(' }') + ..writeln(' }') + ..writeln(' if (bytes.length >= 2) {') + ..writeln(' if (bytes[0] == 0xFF && bytes[1] == 0xFE) {') + ..writeln(" return 'utf-16le';") + ..writeln(' }') + ..writeln(' if (bytes[0] == 0xFE && bytes[1] == 0xFF) {') + ..writeln(" return 'utf-16be';") + ..writeln(' }') + ..writeln(' }') + ..writeln(' // 默认假设为 UTF-8') + ..writeln(" return 'utf-8';") + ..writeln(' }'); buffer.writeln(); - buffer.writeln(' /// 根据编码名称获取编码器'); - buffer.writeln(' static Encoding getEncoding(String? encodingName) {'); - buffer.writeln(' if (encodingName == null) return utf8;'); - buffer.writeln( - ' final normalizedName = encodingName.toLowerCase().replaceAll(\'_\', \'-\');'); - buffer.writeln(' return supportedEncodings[normalizedName] ?? utf8;'); - buffer.writeln(' }'); - buffer.writeln(); - - buffer.writeln(' /// 编码字符串'); - buffer.writeln( - ' static List encodeString(String data, [String? encodingName]) {'); - buffer.writeln(' final encoding = getEncoding(encodingName);'); - buffer.writeln(' return encoding.encode(data);'); - buffer.writeln(' }'); - buffer.writeln(); - - buffer.writeln(' /// 解码字节数组'); - buffer.writeln( - ' static String decodeBytes(List bytes, [String? encodingName]) {'); - buffer.writeln(' final encoding = getEncoding(encodingName);'); - buffer.writeln(' return encoding.decode(bytes);'); - buffer.writeln(' }'); - buffer.writeln(); - - buffer.writeln(' /// Base64 编码'); - buffer.writeln(' static String encodeBase64(List bytes) {'); - buffer.writeln(' return base64Encode(bytes);'); - buffer.writeln(' }'); - buffer.writeln(); - - buffer.writeln(' /// Base64 解码'); - buffer.writeln(' static List decodeBase64(String data) {'); - buffer.writeln(' return base64Decode(data);'); - buffer.writeln(' }'); - buffer.writeln(); - - buffer.writeln(' /// URL 编码'); - buffer.writeln(' static String encodeUrl(String data) {'); - buffer.writeln(' return Uri.encodeComponent(data);'); - buffer.writeln(' }'); - buffer.writeln(); - - buffer.writeln(' /// URL 解码'); - buffer.writeln(' static String decodeUrl(String data) {'); - buffer.writeln(' return Uri.decodeComponent(data);'); - buffer.writeln(' }'); - buffer.writeln(); - - buffer.writeln(' /// 处理 Content-Encoding'); - buffer.writeln( - ' static List handleContentEncoding(List data, String? encoding) {'); - buffer.writeln(' if (encoding == null) return data;'); - buffer.writeln(' switch (encoding.toLowerCase()) {'); - buffer.writeln(' case \'gzip\':'); - buffer.writeln(' return gzip.decode(data);'); - buffer.writeln(' case \'deflate\':'); - buffer.writeln(' return zlib.decode(data);'); - buffer.writeln(' case \'br\':'); - buffer.writeln(' // Brotli 解码需要额外的包支持'); - buffer.writeln( - ' throw UnsupportedError(\'Brotli encoding not supported\');'); - buffer.writeln(' case \'identity\':'); - buffer.writeln(' default:'); - buffer.writeln(' return data;'); - buffer.writeln(' }'); - buffer.writeln(' }'); - buffer.writeln(); - - buffer.writeln(' /// 处理 Transfer-Encoding'); - buffer.writeln( - ' static List handleTransferEncoding(List data, String? encoding) {'); - buffer.writeln(' if (encoding == null) return data;'); - buffer.writeln(' switch (encoding.toLowerCase()) {'); - buffer.writeln(' case \'chunked\':'); - buffer.writeln(' return _decodeChunked(data);'); - buffer.writeln(' case \'compress\':'); - buffer.writeln( - ' throw UnsupportedError(\'Compress transfer encoding not supported\');'); - buffer.writeln(' case \'deflate\':'); - buffer.writeln(' return zlib.decode(data);'); - buffer.writeln(' case \'gzip\':'); - buffer.writeln(' return gzip.decode(data);'); - buffer.writeln(' case \'identity\':'); - buffer.writeln(' default:'); - buffer.writeln(' return data;'); - buffer.writeln(' }'); - buffer.writeln(' }'); - buffer.writeln(); - - buffer.writeln(' /// 解码分块传输编码'); - buffer.writeln(' static List _decodeChunked(List data) {'); - buffer.writeln(' final result = [];'); - buffer.writeln(' var offset = 0;'); - buffer.writeln(' while (offset < data.length) {'); - buffer.writeln(' // 查找块大小行的结束'); - buffer.writeln(' var lineEnd = offset;'); - buffer.writeln(' while (lineEnd < data.length - 1) {'); - buffer.writeln( - ' if (data[lineEnd] == 13 && data[lineEnd + 1] == 10) break; // \\r\\n'); - buffer.writeln(' lineEnd++;'); - buffer.writeln(' }'); - buffer.writeln(' if (lineEnd >= data.length - 1) break;'); - buffer.writeln(); - buffer.writeln(' // 解析块大小'); - buffer.writeln( - ' final sizeHex = String.fromCharCodes(data.sublist(offset, lineEnd));'); - buffer.writeln( - ' final chunkSize = int.tryParse(sizeHex, radix: 16) ?? 0;'); - buffer.writeln(' if (chunkSize == 0) break; // 最后一个块'); - buffer.writeln(); - buffer.writeln(' // 跳过 \\r\\n'); - buffer.writeln(' offset = lineEnd + 2;'); - buffer.writeln(); - buffer.writeln(' // 读取块数据'); - buffer.writeln(' if (offset + chunkSize <= data.length) {'); - buffer.writeln( - ' result.addAll(data.sublist(offset, offset + chunkSize));'); - buffer.writeln(' offset += chunkSize + 2; // 跳过块数据后的 \\r\\n'); - buffer.writeln(' } else {'); - buffer.writeln(' break;'); - buffer.writeln(' }'); - buffer.writeln(' }'); - buffer.writeln(' return result;'); - buffer.writeln(' }'); - buffer.writeln(); - - buffer.writeln(' /// 检测字符编码'); - buffer.writeln(' static String? detectEncoding(List bytes) {'); - buffer.writeln(' // 检测 BOM'); - buffer.writeln(' if (bytes.length >= 3) {'); - buffer.writeln( - ' if (bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF) {'); - buffer.writeln(' return \'utf-8\';'); - buffer.writeln(' }'); - buffer.writeln(' }'); - buffer.writeln(' if (bytes.length >= 2) {'); - buffer.writeln(' if (bytes[0] == 0xFF && bytes[1] == 0xFE) {'); - buffer.writeln(' return \'utf-16le\';'); - buffer.writeln(' }'); - buffer.writeln(' if (bytes[0] == 0xFE && bytes[1] == 0xFF) {'); - buffer.writeln(' return \'utf-16be\';'); - buffer.writeln(' }'); - buffer.writeln(' }'); - buffer.writeln(' // 默认假设为 UTF-8'); - buffer.writeln(' return \'utf-8\';'); - buffer.writeln(' }'); - buffer.writeln(); - - buffer.writeln(' /// 验证编码是否有效'); - buffer.writeln( - ' static bool isValidEncoding(List bytes, String encodingName) {'); - buffer.writeln(' try {'); - buffer.writeln(' final encoding = getEncoding(encodingName);'); - buffer.writeln(' encoding.decode(bytes);'); - buffer.writeln(' return true;'); - buffer.writeln(' } catch (e) {'); - buffer.writeln(' return false;'); - buffer.writeln(' }'); - buffer.writeln(' }'); - buffer.writeln('}'); + buffer + ..writeln(' /// 验证编码是否有效') + ..writeln( + ' static bool isValidEncoding(List bytes, String encodingName) {', + ) + ..writeln(' try {') + ..writeln(' final encoding = getEncoding(encodingName);') + ..writeln(' encoding.decode(bytes);') + ..writeln(' return true;') + ..writeln(' } catch (e) {') + ..writeln(' return false;') + ..writeln(' }') + ..writeln(' }') + ..writeln('}'); buffer.writeln(); return buffer.toString(); diff --git a/lib/parsers/swagger_data_parser.dart b/lib/parsers/swagger_data_parser.dart index effff7d..c2a27b6 100644 --- a/lib/parsers/swagger_data_parser.dart +++ b/lib/parsers/swagger_data_parser.dart @@ -2,27 +2,28 @@ import 'dart:convert'; import 'dart:io'; import 'package:http/http.dart' as http; +import 'package:logging/logging.dart'; -import '../core/config.dart'; -import '../core/exceptions.dart'; -import '../core/models.dart'; -import '../utils/cache_manager.dart'; -import '../utils/performance_monitor.dart'; -import '../utils/reference_resolver.dart'; -import '../utils/string_utils.dart'; +import 'package:swagger_generator_flutter/core/config.dart'; +import 'package:swagger_generator_flutter/core/exceptions.dart'; +import 'package:swagger_generator_flutter/core/models.dart'; +import 'package:swagger_generator_flutter/utils/cache_manager.dart'; +import 'package:swagger_generator_flutter/utils/performance_monitor.dart'; +import 'package:swagger_generator_flutter/utils/reference_resolver.dart'; +import 'package:swagger_generator_flutter/utils/string_utils.dart'; /// Swagger数据解析器 /// 负责解析Swagger JSON文档并提取相关信息 class SwaggerDataParser { - final CacheManager _cacheManager; - final PerformanceMonitor _performanceMonitor; - - // 缓存解析结果 - final Map _cachedDocuments = {}; - SwaggerDataParser() : _cacheManager = CacheManager(), _performanceMonitor = PerformanceMonitor(); + final CacheManager _cacheManager; + final PerformanceMonitor _performanceMonitor; + static final Logger _logger = Logger('SwaggerDataParser'); + + // 缓存解析结果 + final Map _cachedDocuments = {}; /// 获取并解析Swagger JSON文档 /// [url] 可选参数,如果不传则使用配置中的第一个 URL @@ -31,7 +32,7 @@ class SwaggerDataParser { // 如果有缓存,直接返回缓存结果 if (_cachedDocuments.containsKey(swaggerUrl)) { - print('📦 使用缓存的文档: $swaggerUrl'); + _logger.info('📦 使用缓存的文档: $swaggerUrl'); return _cachedDocuments[swaggerUrl]!; } @@ -47,8 +48,8 @@ class SwaggerDataParser { // 处理本地文件 final filePath = swaggerUrl.replaceFirst('file://', ''); final file = File(filePath); - if (await file.exists()) { - final content = await file.readAsString(); + if (file.existsSync()) { + final content = file.readAsStringSync(); jsonData = json.decode(content) as Map; } else { throw SwaggerParseException( @@ -78,9 +79,9 @@ class SwaggerDataParser { final document = await parseSwaggerDocument(jsonData); _cachedDocuments[swaggerUrl] = document; - print('✅ Swagger文档解析完成'); + _logger.info('✅ Swagger文档解析完成'); return document; - } catch (e) { + } on Object catch (e) { if (e is SwaggerParseException) { rethrow; } @@ -173,8 +174,8 @@ class SwaggerDataParser { if (servers.isEmpty) { servers.add(const ApiServer(url: '/')); } - } catch (e) { - print('⚠️ 解析servers配置时发生错误: $e'); + } on Object catch (e) { + _logger.warning('⚠️ 解析servers配置时发生错误: $e'); // 提供默认服务器配置 servers.add(const ApiServer(url: '/')); } @@ -226,8 +227,8 @@ class SwaggerDataParser { callbacks: components.callbacks, ); } - } catch (e) { - print('⚠️ 解析components配置时发生错误: $e'); + } on Object catch (e) { + _logger.warning('⚠️ 解析components配置时发生错误: $e'); } return const ApiComponents(); @@ -250,8 +251,8 @@ class SwaggerDataParser { } } } - } catch (e) { - print('⚠️ 解析tags信息时发生错误: $e'); + } on Object catch (e) { + _logger.warning('⚠️ 解析tags信息时发生错误: $e'); } return tagsInfo; @@ -274,11 +275,11 @@ class SwaggerDataParser { final method = HttpMethod.fromString(methodKey); final apiPath = ApiPath.fromJson( pathKey, - methodKey, // 传入字符串而不是HttpMethod对象 + method, methodValue, ); - final key = - '${method.value.toUpperCase()}_${pathKey.replaceAll('/', '_')}'; + final key = '${method.value.toUpperCase()}_' + '${pathKey.replaceAll('/', '_')}'; paths[key] = apiPath; } }); @@ -340,8 +341,8 @@ class SwaggerDataParser { } // 引用类型 ($ref) - if (propData['\$ref'] != null) { - final ref = propData['\$ref'] as String; + if (propData[r'$ref'] != null) { + final ref = propData[r'$ref'] as String; // 从 #/components/schemas/ModelName 或 #/definitions/ModelName 中提取类型名 final parts = ref.split('/'); if (parts.isNotEmpty) { @@ -540,7 +541,7 @@ class SwaggerDataParser { ModelUsageType usageType, ) { // 处理 $ref - final ref = schema['\$ref'] as String?; + final ref = schema[r'$ref'] as String?; if (ref != null) { // 从 #/components/schemas/ModelName 提取 ModelName final schemaName = ref.split('/').last; diff --git a/lib/swagger_cli_new.dart b/lib/swagger_cli_new.dart index f5c5cd7..e5e5188 100644 --- a/lib/swagger_cli_new.dart +++ b/lib/swagger_cli_new.dart @@ -1,19 +1,20 @@ import 'dart:io'; -import 'commands/base_command.dart'; -import 'commands/generate_command.dart'; -import 'core/config.dart'; - -import 'utils/string_utils.dart'; +import 'package:logging/logging.dart'; +import 'package:swagger_generator_flutter/commands/base_command.dart'; +import 'package:swagger_generator_flutter/commands/generate_command.dart'; +import 'package:swagger_generator_flutter/core/config.dart'; +import 'package:swagger_generator_flutter/utils/string_utils.dart'; /// Swagger CLI 应用程序 /// 使用命令模式架构的新版本CLI工具 class SwaggerCLI { - final Map _commands = {}; SwaggerCLI() { _registerCommands(); } + static final Logger _logger = Logger('SwaggerCLI'); + final Map _commands = {}; /// 注册所有命令 void _registerCommands() { @@ -56,8 +57,9 @@ class SwaggerCLI { // 查找并执行命令 final command = _commands[commandName]; if (command == null) { - print('❌ 未知命令: $commandName'); - print(''); + _logger + ..severe('❌ 未知命令: $commandName') + ..info(''); _showAvailableCommands(); return 1; } @@ -75,33 +77,37 @@ class SwaggerCLI { // 显示执行时间 if (exitCode == 0) { - print(''); - print('⏱️ 执行时间: ${StringUtils.formatDuration(stopwatch.elapsed)}'); + _logger + ..info('') + ..info('⏱️ 执行时间: ${StringUtils.formatDuration(stopwatch.elapsed)}'); } return exitCode; } catch (error, stackTrace) { - print('❌ 应用程序错误: $error'); - print('堆栈跟踪: $stackTrace'); + _logger + ..severe('❌ 应用程序错误: $error') + ..severe('堆栈跟踪: $stackTrace'); return 1; } } /// 显示应用程序横幅 void _showBanner() { - print(''); - print('🚀 Swagger API 代码生成器 v2.0'); - print('====================================='); - print('强大的 Swagger API 代码生成工具'); - print(''); + _logger + ..info('') + ..info('🚀 Swagger API 代码生成器 v2.0') + ..info('=====================================') + ..info('强大的 Swagger API 代码生成工具') + ..info(''); } /// 显示帮助信息 void _showHelp() { - print('用法: dart swagger_cli_new.dart <命令> [选项]'); - print(''); - print('全新的命令式架构,提供更好的可扩展性和用户体验。'); - print(''); + _logger + ..info('用法: dart swagger_cli_new.dart <命令> [选项]') + ..info('') + ..info('全新的命令式架构,提供更好的可扩展性和用户体验。') + ..info(''); _showAvailableCommands(); _showGlobalOptions(); _showExamples(); @@ -110,68 +116,74 @@ class SwaggerCLI { /// 显示可用命令 void _showAvailableCommands() { - print('📋 可用命令:'); - print(''); + _logger + ..info('📋 可用命令:') + ..info(''); for (final command in _commands.values) { - print(' ${command.name.padRight(12)} ${command.description}'); + _logger.info(' ${command.name.padRight(12)} ${command.description}'); } - print(' help 显示帮助信息'); - print(' version 显示版本信息'); - print(''); + _logger + ..info(' help 显示帮助信息') + ..info(' version 显示版本信息') + ..info(''); } /// 显示全局选项 void _showGlobalOptions() { - print('🔧 全局选项:'); - print(' -h, --help 显示帮助信息'); - print(' --version 显示版本信息'); - print(''); + _logger + ..info('🔧 全局选项:') + ..info(' -h, --help 显示帮助信息') + ..info(' --version 显示版本信息') + ..info(''); } /// 显示使用示例 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(''); + _logger + ..info('💡 使用示例:') + ..info('') + ..info(' # 生成所有文件') + ..info(' dart swagger_cli_new.dart generate --all') + ..info('') + ..info(' # 只生成模型文件(简洁版本)') + ..info(' dart swagger_cli_new.dart generate --models --simple') + ..info('') + ..info(' # 生成到指定目录并启用性能监控') + ..info( + ' dart swagger_cli_new.dart generate --all --output-dir lib/generated --performance', + ) + ..info('') + ..info(' # 查看具体命令的帮助') + ..info(' dart swagger_cli_new.dart generate --help') + ..info(''); } /// 显示联系信息 void _showContact() { - print('🌐 更多信息:'); - print(' API文档: ${SwaggerConfig.swaggerJsonUrls.first}'); - print(' 基础URL: ${SwaggerConfig.baseUrl}'); - print(''); + _logger + ..info('🌐 更多信息:') + ..info(' API文档: ${SwaggerConfig.swaggerJsonUrls.first}') + ..info(' 基础URL: ${SwaggerConfig.baseUrl}') + ..info(''); } /// 显示版本信息 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(''); + _logger + ..info('Swagger CLI v2.0.0') + ..info('构建于: ${DateTime.now().toIso8601String()}') + ..info('Dart SDK: ${Platform.version}') + ..info('') + ..info('功能特性:') + ..info('- 🏗️ 模块化命令架构') + ..info('- 🚀 性能监控和优化') + ..info('- 🔍 智能类型验证') + ..info('- 📋 详细的错误报告') + ..info('- 💾 智能缓存机制') + ..info('- 📚 丰富的文档生成') + ..info(''); } /// 格式化持续时间 @@ -186,6 +198,16 @@ class SwaggerCLI { /// CLI应用程序入口点 Future main(List arguments) async { + // 配置日志 + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen((record) { + if (record.level >= Level.SEVERE) { + stderr.writeln(record.message); + } else { + stdout.writeln(record.message); + } + }); + final cli = SwaggerCLI(); final exitCode = await cli.run(arguments); exit(exitCode); diff --git a/lib/swagger_generator_flutter.dart b/lib/swagger_generator_flutter.dart index 16c8480..8d8faf8 100644 --- a/lib/swagger_generator_flutter.dart +++ b/lib/swagger_generator_flutter.dart @@ -1,7 +1,7 @@ /// Swagger Generator Flutter /// /// 一个强大的 Flutter OpenAPI 3.0 代码生成器,专门为 Dio + Retrofit 架构优化。 -library swagger_generator_flutter; +library; export 'core/error_reporter.dart'; // 核心模型 diff --git a/lib/utils/cache_manager.dart b/lib/utils/cache_manager.dart index df6b593..d5be82b 100644 --- a/lib/utils/cache_manager.dart +++ b/lib/utils/cache_manager.dart @@ -1,18 +1,17 @@ import 'dart:collection'; -import 'string_utils.dart'; +import 'package:swagger_generator_flutter/utils/string_utils.dart'; /// 缓存管理器 /// 提供简单的内存缓存功能 class CacheManager { + CacheManager(); static const int _maxMemoryItems = 100; // 内存缓存 final Map _memoryCache = {}; final Queue _accessOrder = Queue(); - CacheManager(); - /// 从缓存获取数据 T? get(String key) { final entry = _memoryCache[key]; @@ -64,15 +63,16 @@ class CacheManager { return CacheStats( memoryItems: _memoryCache.length, diskItems: 0, - hitRate: 0.0, + hitRate: 0, totalSize: 0, ); } /// 更新访问顺序 void _updateAccessOrder(String key) { - _accessOrder.removeWhere((k) => k == key); - _accessOrder.addLast(key); + _accessOrder + ..removeWhere((k) => k == key) + ..addLast(key); } /// 驱逐最旧的项 @@ -86,30 +86,28 @@ class CacheManager { /// 缓存条目 class _CacheEntry { - final dynamic value; - final DateTime expiresAt; - _CacheEntry({ required this.value, required this.expiresAt, }); + final dynamic value; + final DateTime 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, }); + final int memoryItems; + final int diskItems; + final double hitRate; + final int totalSize; @override String toString() { diff --git a/lib/utils/file_utils.dart b/lib/utils/file_utils.dart index c34afef..c72cd65 100644 --- a/lib/utils/file_utils.dart +++ b/lib/utils/file_utils.dart @@ -1,9 +1,12 @@ import 'dart:io'; +import 'package:logging/logging.dart'; import 'package:path/path.dart' as path; /// 文件工具类 /// 提供文件操作、目录管理和代码格式化功能 class FileUtils { + static final Logger _logger = Logger('FileUtils'); + /// 解析路径(支持相对路径和绝对路径) /// 如果是相对路径,相对于项目根目录(配置文件所在目录) static String resolvePath(String filePath) { @@ -27,7 +30,7 @@ class FileUtils { /// 查找配置文件 static String? _findConfigFile() { var currentDir = Directory.current; - final maxDepth = 10; + const maxDepth = 10; var depth = 0; while (depth < maxDepth) { @@ -52,7 +55,7 @@ class FileUtils { static Future ensureDirectoryExists(String dirPath) async { final resolvedPath = resolvePath(dirPath); final directory = Directory(resolvedPath); - if (!await directory.exists()) { + if (!directory.existsSync()) { await directory.create(recursive: true); } return directory; @@ -66,13 +69,15 @@ class FileUtils { final directory = file.parent; // 确保目录存在 - if (!await directory.exists()) { + if (!directory.existsSync()) { await directory.create(recursive: true); } // 写入文件 await file.writeAsString(content); - } catch (e) { + } on FileSystemException { + rethrow; + } on Object { throw FileSystemException('写入文件失败: $filePath', filePath); } } @@ -81,30 +86,32 @@ class FileUtils { static Future safeReadFile(String filePath) async { try { final file = File(filePath); - if (!await file.exists()) { + if (!file.existsSync()) { throw FileSystemException('文件不存在: $filePath', filePath); } return await file.readAsString(); - } catch (e) { + } on FileSystemException { + rethrow; + } on Object { throw FileSystemException('读取文件失败: $filePath', filePath); } } /// 检查文件是否存在 static Future fileExists(String filePath) async { - return await File(filePath).exists(); + return File(filePath).existsSync(); } /// 检查目录是否存在 static Future directoryExists(String dirPath) async { - return await Directory(dirPath).exists(); + return Directory(dirPath).existsSync(); } /// 删除文件(如果存在) static Future deleteFileIfExists(String filePath) async { final file = File(filePath); - if (await file.exists()) { + if (file.existsSync()) { await file.delete(); } } @@ -112,56 +119,66 @@ class FileUtils { /// 删除目录(如果存在) static Future deleteDirectoryIfExists(String dirPath) async { final directory = Directory(dirPath); - if (await directory.exists()) { + if (directory.existsSync()) { await directory.delete(recursive: true); } } /// 复制文件 static Future copyFile( - String sourcePath, String destinationPath) async { + String sourcePath, + String destinationPath, + ) async { try { final sourceFile = File(sourcePath); final destinationFile = File(destinationPath); - if (!await sourceFile.exists()) { + if (!sourceFile.existsSync()) { throw FileSystemException('源文件不存在: $sourcePath', sourcePath); } // 确保目标目录存在 final destinationDir = destinationFile.parent; - if (!await destinationDir.exists()) { + if (!destinationDir.existsSync()) { await destinationDir.create(recursive: true); } await sourceFile.copy(destinationPath); - } catch (e) { - throw FileSystemException('复制文件失败: $sourcePath -> $destinationPath', - sourcePath, e is OSError ? e : null); + } on Object catch (e) { + throw FileSystemException( + '复制文件失败: $sourcePath -> $destinationPath', + sourcePath, + e is OSError ? e : null, + ); } } /// 移动文件 static Future moveFile( - String sourcePath, String destinationPath) async { + String sourcePath, + String destinationPath, + ) async { try { final sourceFile = File(sourcePath); final destinationFile = File(destinationPath); - if (!await sourceFile.exists()) { + if (!sourceFile.existsSync()) { throw FileSystemException('源文件不存在: $sourcePath', sourcePath); } // 确保目标目录存在 final destinationDir = destinationFile.parent; - if (!await destinationDir.exists()) { + if (!destinationDir.existsSync()) { await destinationDir.create(recursive: true); } await sourceFile.rename(destinationPath); - } catch (e) { - throw FileSystemException('移动文件失败: $sourcePath -> $destinationPath', - sourcePath, e is OSError ? e : null); + } on Object catch (e) { + throw FileSystemException( + '移动文件失败: $sourcePath -> $destinationPath', + sourcePath, + e is OSError ? e : null, + ); } } @@ -169,11 +186,11 @@ class FileUtils { static Future getFileSize(String filePath) async { try { final file = File(filePath); - if (!await file.exists()) { + if (!file.existsSync()) { return 0; } return await file.length(); - } catch (e) { + } on Object { return 0; } } @@ -186,21 +203,23 @@ class FileUtils { return 0; } - int totalSize = 0; - await for (final entity in directory.list(recursive: true)) { + var totalSize = 0; + for (final entity in directory.listSync(recursive: true)) { if (entity is File) { - totalSize += await entity.length(); + totalSize += entity.lengthSync(); } } return totalSize; - } catch (e) { + } on Object { return 0; } } /// 列出目录中的文件 - static Future> listFiles(String dirPath, - {String? extension}) async { + static Future> listFiles( + String dirPath, { + String? extension, + }) async { try { final directory = Directory(dirPath); if (!await directory.exists()) { @@ -208,7 +227,7 @@ class FileUtils { } final files = []; - await for (final entity in directory.list()) { + for (final entity in directory.listSync()) { if (entity is File) { if (extension == null || entity.path.endsWith(extension)) { files.add(entity.path); @@ -216,7 +235,7 @@ class FileUtils { } } return files; - } catch (e) { + } on Object { return []; } } @@ -230,13 +249,13 @@ class FileUtils { } final directories = []; - await for (final entity in directory.list()) { + for (final entity in directory.listSync()) { if (entity is Directory) { directories.add(entity.path); } } return directories; - } catch (e) { + } on Object { return []; } } @@ -245,34 +264,42 @@ class FileUtils { static Future createBackup(String filePath) async { try { final file = File(filePath); - if (!await file.exists()) { + if (!file.existsSync()) { throw FileSystemException('文件不存在: $filePath', filePath); } final timestamp = DateTime.now().toIso8601String().replaceAll(':', '-'); - final backupPath = '${filePath}.backup.$timestamp'; + final backupPath = '$filePath.backup.$timestamp'; await file.copy(backupPath); return backupPath; - } catch (e) { + } on Object catch (e) { throw FileSystemException( - '创建备份失败: $filePath', filePath, e is OSError ? e : null); + '创建备份失败: $filePath', + filePath, + e is OSError ? e : null, + ); } } /// 恢复备份文件 static Future restoreBackup( - String backupPath, String originalPath) async { + String backupPath, + String originalPath, + ) async { try { final backupFile = File(backupPath); - if (!await backupFile.exists()) { + if (!backupFile.existsSync()) { throw FileSystemException('备份文件不存在: $backupPath', backupPath); } await backupFile.copy(originalPath); - } catch (e) { - throw FileSystemException('恢复备份失败: $backupPath -> $originalPath', - backupPath, e is OSError ? e : null); + } on Object catch (e) { + throw FileSystemException( + '恢复备份失败: $backupPath -> $originalPath', + backupPath, + e is OSError ? e : null, + ); } } @@ -327,20 +354,22 @@ class FileUtils { return fileName .replaceAll(RegExp(r'[<>:"/\\|?*]'), '_') .replaceAll(RegExp(r'\s+'), '_') - .replaceAll(RegExp(r'_{2,}'), '_') + .replaceAll(RegExp('_{2,}'), '_') .replaceAll(RegExp(r'^_|_$'), ''); } /// 生成唯一文件名 static Future generateUniqueFileName( - String basePath, String fileName) async { + String basePath, + String fileName, + ) async { final extension = getFileExtension(fileName); final nameWithoutExt = getFileNameWithoutExtension(fileName); - String uniqueName = fileName; - int counter = 1; + var uniqueName = fileName; + var counter = 1; - while (await File(path.join(basePath, uniqueName)).exists()) { + while (File(path.join(basePath, uniqueName)).existsSync()) { uniqueName = '${nameWithoutExt}_$counter$extension'; counter++; } @@ -356,8 +385,8 @@ class FileUtils { for (final filePath in filePaths) { try { await operation(filePath); - } catch (e) { - print('批量操作失败: $filePath - $e'); + } on Object catch (e) { + _logger.warning('批量操作失败: $filePath - $e'); } } } @@ -377,7 +406,7 @@ class FileUtils { final regex = RegExp(pattern); final foundFiles = []; - await for (final entity in directory.list(recursive: recursive)) { + for (final entity in directory.listSync(recursive: recursive)) { if (entity is File) { final fileName = getFileName(entity.path); if (regex.hasMatch(fileName)) { @@ -387,7 +416,7 @@ class FileUtils { } return foundFiles; - } catch (e) { + } on Object { return []; } } @@ -396,13 +425,13 @@ class FileUtils { static Future getFileModifiedTime(String filePath) async { try { final file = File(filePath); - if (!await file.exists()) { + if (!file.existsSync()) { return null; } - final stat = await file.stat(); + final stat = file.statSync(); return stat.modified; - } catch (e) { + } on Object { return null; } } @@ -423,13 +452,13 @@ class FileUtils { static Future calculateFileHash(String filePath) async { try { final file = File(filePath); - if (!await file.exists()) { + if (!file.existsSync()) { return null; } final bytes = await file.readAsBytes(); return bytes.hashCode.toString(); - } catch (e) { + } on Object { return null; } } @@ -470,8 +499,8 @@ class FileUtils { } } } - } catch (e) { - print('清理临时文件失败: $e'); + } on Object catch (e) { + _logger.warning('清理临时文件失败: $e'); } } @@ -490,11 +519,11 @@ class FileUtils { static Future> listDirectory(String path) async { try { final directory = Directory(path); - if (!await directory.exists()) { + if (!directory.existsSync()) { return []; } return await directory.list().toList(); - } catch (e) { + } on Object { return []; } } diff --git a/lib/utils/performance_monitor.dart b/lib/utils/performance_monitor.dart index 449a00d..2f21941 100644 --- a/lib/utils/performance_monitor.dart +++ b/lib/utils/performance_monitor.dart @@ -6,23 +6,24 @@ import 'package:logging/logging.dart'; /// 性能监控器 /// 用于监控和记录应用程序性能指标 class PerformanceMonitor { + PerformanceMonitor({bool enabled = true}) : _enabled = enabled { + if (_enabled) { + _logger.info('性能监控器已启用'); + } + } 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 { + String operationName, + Future Function() operation, + ) async { if (!_enabled) { - return await operation(); + return operation(); } final stopwatch = Stopwatch()..start(); @@ -34,7 +35,7 @@ class PerformanceMonitor { _recordMeasurement(operationName, stopwatch.elapsed); return result; - } catch (e) { + } on Object catch (e) { stopwatch.stop(); _recordMeasurement(operationName, stopwatch.elapsed, error: e); rethrow; @@ -56,7 +57,7 @@ class PerformanceMonitor { _recordMeasurement(operationName, stopwatch.elapsed); return result; - } catch (e) { + } on Object catch (e) { stopwatch.stop(); _recordMeasurement(operationName, stopwatch.elapsed, error: e); rethrow; @@ -69,8 +70,11 @@ class PerformanceMonitor { } /// 记录测量结果 - void _recordMeasurement(String operationName, Duration duration, - {dynamic error}) { + void _recordMeasurement( + String operationName, + Duration duration, { + dynamic error, + }) { if (!_enabled) return; // 记录到测量历史中 @@ -124,18 +128,21 @@ class PerformanceMonitor { } /// 获取慢操作(执行时间超过阈值) - List getSlowOperations( - [Duration threshold = const Duration(milliseconds: 500)]) { + 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.add( + SlowOperation( + name: metric.operationName, + maxTime: metric.maxTime, + avgTime: metric.averageTime, + count: metric.count, + ), + ); } } @@ -157,16 +164,18 @@ class PerformanceMonitor { '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(), - })), + '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); @@ -181,53 +190,56 @@ class PerformanceMonitor { /// 打印性能摘要 void printSummary() { if (!_enabled) { - print('性能监控器已禁用'); + _logger.info('性能监控器已禁用'); return; } if (_metrics.isEmpty) { - print('没有性能数据'); + _logger.info('没有性能数据'); return; } - print('\n🔍 性能监控摘要:'); - print('=' * 50); + final buffer = StringBuffer() + ..writeln('\n🔍 性能监控摘要:') + ..writeln('=' * 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'); + buffer + ..writeln('${metric.operationName}:') + ..writeln(' 执行次数: ${metric.count}') + ..writeln(' 总时间: ${metric.totalTime.inMilliseconds}ms') + ..writeln(' 平均时间: ${metric.averageTime.inMilliseconds}ms') + ..writeln(' 最小时间: ${metric.minTime.inMilliseconds}ms') + ..writeln(' 最大时间: ${metric.maxTime.inMilliseconds}ms'); if (metric.errorCount > 0) { - print(' 错误次数: ${metric.errorCount}'); + buffer.writeln(' 错误次数: ${metric.errorCount}'); } - print(''); + buffer.writeln(); } final slowOps = getSlowOperations(); if (slowOps.isNotEmpty) { - print('🐌 慢操作 (>500ms):'); + buffer.writeln('🐌 慢操作 (>500ms):'); for (final op in slowOps.take(5)) { - print(' ${op.name}: ${op.maxTime.inMilliseconds}ms (最大)'); + buffer.writeln(' ${op.name}: ${op.maxTime.inMilliseconds}ms (最大)'); } } + + _logger.info(buffer.toString()); } } /// 性能计时器 class PerformanceTimer { + PerformanceTimer(this.operationName, this.monitor) + : _stopwatch = Stopwatch()..start(); 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; @@ -242,6 +254,7 @@ class PerformanceTimer { /// 性能指标 class PerformanceMetric { + PerformanceMetric(this.operationName); final String operationName; int count = 0; Duration totalTime = Duration.zero; @@ -250,8 +263,6 @@ class PerformanceMetric { int errorCount = 0; DateTime? lastExecuted; - PerformanceMetric(this.operationName); - /// 添加测量结果 void addMeasurement(Duration duration, {dynamic error}) { count++; @@ -279,24 +290,23 @@ class PerformanceMetric { /// 成功率 double get successRate { - if (count == 0) return 0.0; + if (count == 0) return 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, }); + final int totalOperations; + final Duration totalTime; + final Map metrics; + final DateTime generatedAt; /// 获取最慢的操作 PerformanceMetric? get slowestOperation { @@ -321,15 +331,14 @@ class PerformanceReport { /// 慢操作信息 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, }); + final String name; + final Duration maxTime; + final Duration avgTime; + final int count; } diff --git a/lib/utils/reference_resolver.dart b/lib/utils/reference_resolver.dart index d035c2b..6b5e77d 100644 --- a/lib/utils/reference_resolver.dart +++ b/lib/utils/reference_resolver.dart @@ -1,12 +1,17 @@ /// 引用解析器 /// 处理复杂嵌套类型和循环引用检测 -library reference_resolver; +library; -import '../core/models.dart'; +import 'package:logging/logging.dart'; +import 'package:swagger_generator_flutter/core/models.dart'; /// 引用解析器 /// 负责处理 OpenAPI 文档中的复杂引用关系 class ReferenceResolver { + ReferenceResolver({this.maxDepth = 50}); + + static final Logger _logger = Logger('ReferenceResolver'); + /// 已解析的模型缓存 final Map _resolvedModels = {}; @@ -22,8 +27,6 @@ class ReferenceResolver { /// 当前解析深度 int _currentDepth = 0; - ReferenceResolver({this.maxDepth = 50}); - /// 解析所有模型,处理复杂引用关系 Map resolveModels(Map componentsJson) { final schemasJson = @@ -40,8 +43,8 @@ class ReferenceResolver { if (model != null) { resolvedModels[schemaName] = model; } - } catch (e) { - print('⚠️ 解析模型 $schemaName 时发生错误: $e'); + } on Object catch (e) { + _logger.warning('⚠️ 解析模型 $schemaName 时发生错误: $e'); // 创建一个基本的模型作为后备 resolvedModels[schemaName] = ApiModel( name: schemaName, @@ -73,20 +76,21 @@ class ReferenceResolver { // 检查循环引用 if (_resolutionPath.contains(modelName)) { - print('🔄 检测到循环引用: ${_resolutionPath.join(' -> ')} -> $modelName'); + _logger + .warning('🔄 检测到循环引用: ${_resolutionPath.join(' -> ')} -> $modelName'); return _createCircularReferenceModel(modelName); } // 检查解析深度 if (_currentDepth >= maxDepth) { - print('⚠️ 达到最大解析深度 $maxDepth,停止解析 $modelName'); + _logger.warning('⚠️ 达到最大解析深度 $maxDepth,停止解析 $modelName'); return _createDepthLimitModel(modelName); } // 获取原始数据 final schemaData = _rawSchemas[modelName]; if (schemaData == null) { - print('⚠️ 未找到模型定义: $modelName'); + _logger.warning('⚠️ 未找到模型定义: $modelName'); return null; } @@ -125,7 +129,7 @@ class ReferenceResolver { /// 解析枚举模型 ApiModel _parseEnumModel(String name, Map json) { - final enumValues = List.from(json['enum'] ?? []); + final enumValues = List.from((json['enum'] as List?) ?? []); final enumType = PropertyType.fromString(json['type'] as String? ?? 'string'); @@ -169,10 +173,11 @@ class ReferenceResolver { if (json['properties'] != null) { final directProperties = _parseProperties( json['properties'] as Map, - List.from(json['required'] ?? []), + List.from((json['required'] as List?) ?? []), ); mergedProperties.addAll(directProperties); - mergedRequired.addAll(List.from(json['required'] ?? [])); + mergedRequired + .addAll(List.from((json['required'] as List?) ?? [])); } return ApiModel( @@ -192,10 +197,10 @@ class ReferenceResolver { ApiModel _parseObjectModel(String name, Map json) { final properties = _parseProperties( json['properties'] as Map? ?? {}, - List.from(json['required'] ?? []), + List.from((json['required'] as List?) ?? []), ); - final required = List.from(json['required'] ?? []); + final required = List.from((json['required'] as List?) ?? []); return ApiModel( name: name, @@ -245,8 +250,8 @@ class ReferenceResolver { final property = _parsePropertyWithContext(propName, propData, requiredFields); properties[propName] = property; - } catch (e) { - print('⚠️ 解析属性 $propName 时发生错误: $e'); + } on Object catch (e) { + _logger.warning('⚠️ 解析属性 $propName 时发生错误: $e'); // 创建一个基本属性作为后备 properties[propName] = ApiProperty( name: propName, diff --git a/lib/utils/string_utils.dart b/lib/utils/string_utils.dart index ee8e318..3f862aa 100644 --- a/lib/utils/string_utils.dart +++ b/lib/utils/string_utils.dart @@ -15,12 +15,13 @@ /// ``` /// /// This utility provides string conversion helpers for code generation, such as -/// converting snake_case to camelCase, generating Dart class names, and cleaning descriptions. +/// converting snake_case to camelCase, generating Dart class names, and +/// cleaning descriptions. /// library; -import '../core/config_loader.dart'; -import '../core/models.dart'; +import 'package:swagger_generator_flutter/core/config_loader.dart'; +import 'package:swagger_generator_flutter/core/models.dart'; class StringUtils { /// 转换为camelCase @@ -41,8 +42,8 @@ class StringUtils { 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++) { + var result = parts.first.toLowerCase(); + for (var i = 1; i < parts.length; i++) { final part = parts[i]; if (part.isNotEmpty) { result += part[0].toUpperCase() + part.substring(1).toLowerCase(); @@ -59,7 +60,7 @@ class StringUtils { // 如果输入包含下划线,先按下划线分割并转换每个部分 if (input.contains('_')) { final parts = input.split('_'); - String result = ''; + var result = ''; for (final part in parts) { if (part.isNotEmpty) { // 保持每个部分的原始大小写,只确保首字母大写 @@ -94,7 +95,7 @@ class StringUtils { // 处理PascalCase和camelCase final result = - input.replaceAllMapped(RegExp(r'([A-Z]+)([A-Z][a-z])'), (match) { + input.replaceAllMapped(RegExp('([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]}'; @@ -130,13 +131,13 @@ class StringUtils { return propName[0].toLowerCase() + propName.substring(1); } // 处理特殊字符和数字开头的情况 - String result = propName; + var result = propName; // 如果以数字开头,添加前缀 - if (RegExp(r'^[0-9]').hasMatch(result)) { + if (RegExp('^[0-9]').hasMatch(result)) { result = 'n$result'; } // 替换特殊字符为下划线 - result = result.replaceAll(RegExp(r'[^a-zA-Z0-9_]'), '_'); + result = result.replaceAll(RegExp('[^a-zA-Z0-9_]'), '_'); // 转换为camelCase result = toCamelCase(result); // 确保不为空 @@ -151,7 +152,7 @@ class StringUtils { if (description.isEmpty) return description; // 移除多余的空白字符和换行符 - String cleaned = description + var cleaned = description .replaceAll(RegExp(r'\s+'), ' ') .replaceAll(RegExp(r'[\r\n]+'), ' ') .trim(); @@ -178,7 +179,7 @@ class StringUtils { } // 移除路径中的版本前缀 - String cleanPath = path.replaceFirst('/api/v1', ''); + var cleanPath = path.replaceFirst('/api/v1', ''); // 移除开头的斜杠 if (cleanPath.startsWith('/')) { @@ -201,14 +202,14 @@ class StringUtils { /// 生成Dart类名 static String generateClassName(String name) { // 确保类名以大写字母开头 - final cleanName = name.replaceAll(RegExp(r'[^a-zA-Z0-9_]'), '_'); + final cleanName = name.replaceAll(RegExp('[^a-zA-Z0-9_]'), '_'); return toPascalCase(cleanName); } /// 生成常量名称 (UPPER_SNAKE_CASE) static String generateConstantName(String name) { // 清理特殊字符 - final cleanName = name.replaceAll(RegExp(r'[^a-zA-Z0-9_]'), '_'); + final cleanName = name.replaceAll(RegExp('[^a-zA-Z0-9_]'), '_'); // 转换为 snake_case 然后转为大写 return toSnakeCase(cleanName).toUpperCase(); } @@ -232,7 +233,7 @@ class StringUtils { static String generateEnumValueName(dynamic value, int index) { if (value is String) { // 尝试从字符串生成合法的枚举名 - final cleanValue = value.replaceAll(RegExp(r'[^a-zA-Z0-9_]'), ''); + final cleanValue = value.replaceAll(RegExp('[^a-zA-Z0-9_]'), ''); if (cleanValue.isNotEmpty && isValidDartIdentifier(cleanValue)) { return toCamelCase(cleanValue); } @@ -286,11 +287,11 @@ class StringUtils { /// 转义字符串中的特殊字符 String escapeString(String input) { return input - .replaceAll('\\', '\\\\') - .replaceAll('"', '\\"') - .replaceAll('\n', '\\n') - .replaceAll('\r', '\\r') - .replaceAll('\t', '\\t'); + .replaceAll(r'\', r'\\') + .replaceAll('"', r'\"') + .replaceAll('\n', r'\n') + .replaceAll('\r', r'\r') + .replaceAll('\t', r'\t'); } /// 缩进文本 @@ -379,14 +380,16 @@ class StringUtils { final author = ConfigLoader.getAuthor(); final copyright = ConfigLoader.getCopyright(); - return '''// $description + return ''' +// $description // 由 $generatorName by $author 生成 // $copyright '''; } /// 应用文件头模板 - /// 支持变量: {fileName}, {fileType}, {swaggerUrl}, {generatorName}, {author}, {copyright} + /// 支持变量: {fileName}, {fileType}, {swaggerUrl}, {generatorName}, {author}, + /// {copyright} static String _applyFileHeaderTemplate( String template, { required String description, diff --git a/lib/utils/type_validator.dart b/lib/utils/type_validator.dart index 4fe181b..339ac1d 100644 --- a/lib/utils/type_validator.dart +++ b/lib/utils/type_validator.dart @@ -1,4 +1,4 @@ -import '../core/models.dart'; +import 'package:swagger_generator_flutter/core/models.dart'; /// 类型验证器 /// 提供严格的类型检查和验证功能 @@ -248,12 +248,10 @@ class TypeValidator { ), ); } - break; case CodeType.documentation: if (!code.contains('#')) { warnings.add('文档代码似乎不包含Markdown标题'); } - break; } // 检查潜在的命名冲突 @@ -387,8 +385,8 @@ class TypeValidator { errors.add( ValidationError( field: 'reference', - message: - '模型 ${model.name} 中的属性 ${property.name} 引用了不存在的类型: ${property.reference}', + message: '模型 ${model.name} 中的属性 ${property.name} ' + '引用了不存在的类型: ${property.reference}', severity: ErrorSeverity.error, ), ); @@ -424,7 +422,7 @@ class TypeValidator { try { DateTime.parse(dateString); return true; - } catch (e) { + } on Object { return false; } } @@ -546,15 +544,14 @@ class TypeValidator { /// 验证结果 class ValidationResult { - final bool isValid; - final List errors; - final List warnings; - const ValidationResult({ required this.isValid, required this.errors, required this.warnings, }); + final bool isValid; + final List errors; + final List warnings; /// 是否有错误 bool get hasErrors => errors.isNotEmpty; @@ -580,7 +577,8 @@ class ValidationResult { buffer.writeln('\n🚨 错误:'); for (final error in errors) { buffer.writeln( - '- [${error.severity.name.toUpperCase()}] ${error.field}: ${error.message}', + '- [${error.severity.name.toUpperCase()}] ' + '${error.field}: ${error.message}', ); } } @@ -598,19 +596,19 @@ class ValidationResult { /// 验证错误 class ValidationError { - final String field; - final String message; - final ErrorSeverity severity; - const ValidationError({ required this.field, required this.message, required this.severity, }); + final String field; + final String message; + final ErrorSeverity severity; @override String toString() { - return 'ValidationError(field: $field, message: $message, severity: $severity)'; + return 'ValidationError(field: $field, message: $message, ' + 'severity: $severity)'; } } diff --git a/lib/validators/enhanced_validator.dart b/lib/validators/enhanced_validator.dart index 7f554d8..a62ce5d 100644 --- a/lib/validators/enhanced_validator.dart +++ b/lib/validators/enhanced_validator.dart @@ -2,18 +2,17 @@ /// 集成详细的错误报告和修复建议 library; -import '../core/error_reporter.dart'; -import '../core/models.dart'; +import 'package:swagger_generator_flutter/core/error_reporter.dart'; +import 'package:swagger_generator_flutter/core/models.dart'; /// 增强的 OpenAPI 验证器 class EnhancedValidator { - final ErrorReporter _errorReporter; - final bool _includeWarnings; - EnhancedValidator({ bool includeWarnings = true, }) : _errorReporter = ErrorReporter(), _includeWarnings = includeWarnings; + final ErrorReporter _errorReporter; + final bool _includeWarnings; /// 获取错误报告器 ErrorReporter get errorReporter => _errorReporter; @@ -54,7 +53,7 @@ class EnhancedValidator { category: ErrorCategory.validation, jsonPath: 'info.title', suggestions: [ - FixSuggestion( + const FixSuggestion( description: 'Add a descriptive title for your API', codeExample: '"title": "My API"', ), @@ -72,7 +71,7 @@ class EnhancedValidator { category: ErrorCategory.validation, jsonPath: 'info.version', suggestions: [ - FixSuggestion( + const FixSuggestion( description: 'Add a version number using semantic versioning', codeExample: '"version": "1.0.0"', documentationUrl: 'https://semver.org/', @@ -92,7 +91,7 @@ class EnhancedValidator { category: ErrorCategory.bestPractice, jsonPath: 'info.description', suggestions: [ - FixSuggestion( + const FixSuggestion( description: 'Add a description explaining what your API does', codeExample: '"description": "This API provides user management functionality"', @@ -112,10 +111,10 @@ class EnhancedValidator { category: ErrorCategory.bestPractice, jsonPath: 'servers', suggestions: [ - FixSuggestion( + const FixSuggestion( description: 'Add at least one server configuration', - codeExample: - '"servers": [{"url": "https://api.example.com", "description": "Production server"}]', + codeExample: '"servers": [{"url": "https://api.example.com", ' + '"description": "Production server"}]', ), ], ); @@ -133,19 +132,17 @@ class EnhancedValidator { category: ErrorCategory.validation, jsonPath: 'paths', suggestions: [ - FixSuggestion( + const FixSuggestion( description: 'Add at least one API endpoint', - codeExample: - '"/users": { "get": { "responses": { "200": { "description": "Success" } } } }', + codeExample: '"/users": { "get": { "responses": { "200": ' + '{ "description": "Success" } } } }', ), ], ); return; } - document.paths.forEach((pathPattern, apiPath) { - _validatePath(pathPattern, apiPath); - }); + document.paths.forEach(_validatePath); } /// 验证单个路径 @@ -181,7 +178,7 @@ class EnhancedValidator { category: ErrorCategory.validation, jsonPath: '$pathKey.responses', suggestions: [ - FixSuggestion( + const FixSuggestion( description: 'Add at least a default response', codeExample: '"responses": { "200": { "description": "Success" } }', ), @@ -202,8 +199,8 @@ class EnhancedValidator { suggestions: [ FixSuggestion( description: 'Add a unique operationId', - codeExample: - '"operationId": "${_generateOperationId(pathPattern, apiPath.method)}"', + codeExample: '"operationId": ' + '"${_generateOperationId(pathPattern, apiPath.method)}"', ), ], ); @@ -220,7 +217,7 @@ class EnhancedValidator { category: ErrorCategory.bestPractice, jsonPath: '$pathKey.summary', suggestions: [ - FixSuggestion( + const FixSuggestion( description: 'Add a brief summary', codeExample: '"summary": "Get all users"', ), @@ -237,7 +234,10 @@ class EnhancedValidator { /// 验证参数 void _validateParameters( - List parameters, String pathKey, String pathPattern) { + List parameters, + String pathKey, + String pathPattern, + ) { // 提取路径参数 final pathParams = _extractPathParameters(pathPattern); final declaredPathParams = parameters @@ -259,8 +259,8 @@ class EnhancedValidator { suggestions: [ FixSuggestion( description: 'Add parameter declaration', - codeExample: - '{"name": "$param", "in": "path", "required": true, "schema": {"type": "string"}}', + codeExample: '{"name": "$param", "in": "path", "required": true, ' + '"schema": {"type": "string"}}', ), ], ); @@ -268,7 +268,7 @@ class EnhancedValidator { } // 验证每个参数 - for (int i = 0; i < parameters.length; i++) { + for (var i = 0; i < parameters.length; i++) { final param = parameters[i]; final paramPath = '$pathKey.parameters[$i]'; @@ -282,7 +282,7 @@ class EnhancedValidator { category: ErrorCategory.validation, jsonPath: '$paramPath.name', suggestions: [ - FixSuggestion( + const FixSuggestion( description: 'Add a name for the parameter', codeExample: '"name": "userId"', ), @@ -300,7 +300,7 @@ class EnhancedValidator { category: ErrorCategory.validation, jsonPath: '$paramPath.required', suggestions: [ - FixSuggestion( + const FixSuggestion( description: 'Set required: true for path parameters', codeExample: '"required": true', ), @@ -312,8 +312,8 @@ class EnhancedValidator { /// 验证响应 void _validateResponses(Map responses, String pathKey) { - bool hasSuccessResponse = false; - bool hasErrorResponse = false; + var hasSuccessResponse = false; + var hasErrorResponse = false; responses.forEach((code, response) { final responsePath = '$pathKey.responses["$code"]'; @@ -339,7 +339,7 @@ class EnhancedValidator { category: ErrorCategory.bestPractice, jsonPath: '$responsePath.description', suggestions: [ - FixSuggestion( + const FixSuggestion( description: 'Add a description for the response', codeExample: '"description": "Successful operation"', ), @@ -359,7 +359,7 @@ class EnhancedValidator { category: ErrorCategory.bestPractice, jsonPath: '$pathKey.responses', suggestions: [ - FixSuggestion( + const FixSuggestion( description: 'Add a success response', codeExample: '"200": { "description": "Success" }', ), @@ -378,10 +378,10 @@ class EnhancedValidator { category: ErrorCategory.bestPractice, jsonPath: '$pathKey.responses', suggestions: [ - FixSuggestion( + const FixSuggestion( description: 'Add common error responses', - codeExample: - '"400": { "description": "Bad Request" }, "404": { "description": "Not Found" }', + codeExample: '"400": { "description": "Bad Request" }, ' + '"404": { "description": "Not Found" }', ), ], ); @@ -391,14 +391,10 @@ class EnhancedValidator { /// 验证组件 void _validateComponents(SwaggerDocument document) { // 验证 schemas - document.components.schemas.forEach((name, model) { - _validateSchema(name, model); - }); + document.components.schemas.forEach(_validateSchema); // 验证安全方案 - document.components.securitySchemes.forEach((name, scheme) { - _validateSecurityScheme(name, scheme); - }); + document.components.securitySchemes.forEach(_validateSecurityScheme); } /// 验证 Schema @@ -415,7 +411,7 @@ class EnhancedValidator { category: ErrorCategory.validation, jsonPath: schemaPath, suggestions: [ - FixSuggestion( + const FixSuggestion( description: 'Ensure schema has a valid name', codeExample: 'Schema name should match the key in components.schemas', @@ -435,10 +431,11 @@ class EnhancedValidator { category: ErrorCategory.performance, jsonPath: schemaPath, suggestions: [ - FixSuggestion( + const FixSuggestion( description: 'Consider using composition with allOf', codeExample: - '"allOf": [{ "\$ref": "#/components/schemas/BaseModel" }, { "type": "object", "properties": {...} }]', + r'"allOf": [{ "$ref": "#/components/schemas/BaseModel" }, ' + '{ "type": "object", "properties": {...} }]', ), ], ); @@ -461,14 +458,13 @@ class EnhancedValidator { category: ErrorCategory.security, jsonPath: '$schemePath.name', suggestions: [ - FixSuggestion( + const FixSuggestion( description: 'Add name field for API key parameter', codeExample: '"name": "X-API-Key"', ), ], ); } - break; case SecuritySchemeType.http: if (scheme.scheme == null || scheme.scheme!.isEmpty) { @@ -481,14 +477,13 @@ class EnhancedValidator { category: ErrorCategory.security, jsonPath: '$schemePath.scheme', suggestions: [ - FixSuggestion( + const FixSuggestion( description: 'Add scheme field', codeExample: '"scheme": "bearer"', ), ], ); } - break; case SecuritySchemeType.oauth2: if (scheme.flows == null) { @@ -500,15 +495,14 @@ class EnhancedValidator { category: ErrorCategory.security, jsonPath: '$schemePath.flows', suggestions: [ - FixSuggestion( + const FixSuggestion( description: 'Add flows configuration', - codeExample: - '"flows": { "authorizationCode": { "authorizationUrl": "...", "tokenUrl": "..." } }', + codeExample: '"flows": { "authorizationCode": ' + '{ "authorizationUrl": "...", "tokenUrl": "..." } }', ), ], ); } - break; case SecuritySchemeType.openIdConnect: if (scheme.openIdConnectUrl == null || @@ -521,15 +515,14 @@ class EnhancedValidator { category: ErrorCategory.security, jsonPath: '$schemePath.openIdConnectUrl', suggestions: [ - FixSuggestion( + const FixSuggestion( description: 'Add OpenID Connect URL', - codeExample: - '"openIdConnectUrl": "https://example.com/.well-known/openid_configuration"', + codeExample: '"openIdConnectUrl": ' + '"https://example.com/.well-known/openid_configuration"', ), ], ); } - break; } } @@ -552,7 +545,7 @@ class EnhancedValidator { category: ErrorCategory.bestPractice, jsonPath: 'paths', suggestions: [ - FixSuggestion( + const FixSuggestion( description: 'Add tags to operations', codeExample: '"tags": ["users"]', ), @@ -581,7 +574,7 @@ class EnhancedValidator { pathParts.removeRange(0, 2); } - final nameParts = pathParts.map((part) => _toPascalCase(part)).join(''); + final nameParts = pathParts.map(_toPascalCase).join(); return '$methodPrefix$nameParts'; } @@ -589,9 +582,11 @@ class EnhancedValidator { String _toPascalCase(String input) { return input .split('_') - .map((word) => word.isEmpty - ? '' - : word[0].toUpperCase() + word.substring(1).toLowerCase()) - .join(''); + .map( + (word) => word.isEmpty + ? '' + : word[0].toUpperCase() + word.substring(1).toLowerCase(), + ) + .join(); } } diff --git a/lib/validators/schema_validator.dart b/lib/validators/schema_validator.dart index a4e74e3..b2b464e 100644 --- a/lib/validators/schema_validator.dart +++ b/lib/validators/schema_validator.dart @@ -2,14 +2,10 @@ /// 验证 OpenAPI 3.0 文档的完整性和正确性 library; -import '../core/models.dart'; +import 'package:swagger_generator_flutter/core/models.dart'; /// Schema 验证结果 class ValidationResult { - final bool isValid; - final List errors; - final List warnings; - const ValidationResult({ required this.isValid, this.errors = const [], @@ -17,8 +13,9 @@ class ValidationResult { }); /// 创建成功的验证结果 - factory ValidationResult.success( - {List warnings = const []}) { + factory ValidationResult.success({ + List warnings = const [], + }) { return ValidationResult( isValid: true, warnings: warnings, @@ -26,14 +23,19 @@ class ValidationResult { } /// 创建失败的验证结果 - factory ValidationResult.failure(List errors, - {List warnings = const []}) { + factory ValidationResult.failure( + List errors, { + List warnings = const [], + }) { return ValidationResult( isValid: false, errors: errors, warnings: warnings, ); } + final bool isValid; + final List errors; + final List warnings; /// 是否有警告 bool get hasWarnings => warnings.isNotEmpty; @@ -44,22 +46,20 @@ class ValidationResult { /// 验证错误 class ValidationError { - final String path; - final String message; - final ValidationErrorType type; - final String? suggestion; - const ValidationError({ required this.path, required this.message, required this.type, this.suggestion, }); + final String path; + final String message; + final ValidationErrorType type; + final String? suggestion; @override String toString() { - final buffer = StringBuffer(); - buffer.write('[$type] $path: $message'); + final buffer = StringBuffer()..write('[$type] $path: $message'); if (suggestion != null) { buffer.write(' (建议: $suggestion)'); } @@ -69,20 +69,18 @@ class ValidationError { /// 验证警告 class ValidationWarning { - final String path; - final String message; - final String? suggestion; - const ValidationWarning({ required this.path, required this.message, this.suggestion, }); + final String path; + final String message; + final String? suggestion; @override String toString() { - final buffer = StringBuffer(); - buffer.write('[WARNING] $path: $message'); + final buffer = StringBuffer()..write('[WARNING] $path: $message'); if (suggestion != null) { buffer.write(' (建议: $suggestion)'); } @@ -136,70 +134,84 @@ class SchemaValidator { /// 验证基本信息 void _validateInfo(SwaggerDocument document) { if (document.title.isEmpty) { - _errors.add(const ValidationError( - path: 'info.title', - message: 'API 标题不能为空', - type: ValidationErrorType.required, - suggestion: '请提供有意义的 API 标题', - )); + _errors.add( + const ValidationError( + path: 'info.title', + message: 'API 标题不能为空', + type: ValidationErrorType.required, + suggestion: '请提供有意义的 API 标题', + ), + ); } if (document.version.isEmpty) { - _errors.add(const ValidationError( - path: 'info.version', - message: 'API 版本不能为空', - type: ValidationErrorType.required, - suggestion: '请使用语义化版本号,如 "1.0.0"', - )); + _errors.add( + const ValidationError( + path: 'info.version', + message: 'API 版本不能为空', + type: ValidationErrorType.required, + suggestion: '请使用语义化版本号,如 "1.0.0"', + ), + ); } if (document.description.isEmpty) { - _warnings.add(const ValidationWarning( - path: 'info.description', - message: 'API 描述为空', - suggestion: '建议添加 API 的详细描述', - )); + _warnings.add( + const ValidationWarning( + path: 'info.description', + message: 'API 描述为空', + suggestion: '建议添加 API 的详细描述', + ), + ); } } /// 验证服务器配置 void _validateServers(List servers) { if (servers.isEmpty) { - _warnings.add(const ValidationWarning( - path: 'servers', - message: '未定义服务器配置', - suggestion: '建议添加至少一个服务器配置', - )); + _warnings.add( + const ValidationWarning( + path: 'servers', + message: '未定义服务器配置', + suggestion: '建议添加至少一个服务器配置', + ), + ); return; } - for (int i = 0; i < servers.length; i++) { + for (var i = 0; i < servers.length; i++) { final server = servers[i]; final path = 'servers[$i]'; if (server.url.isEmpty) { - _errors.add(ValidationError( - path: '$path.url', - message: '服务器 URL 不能为空', - type: ValidationErrorType.required, - )); + _errors.add( + ValidationError( + path: '$path.url', + message: '服务器 URL 不能为空', + type: ValidationErrorType.required, + ), + ); } else if (!_isValidUrl(server.url)) { - _errors.add(ValidationError( - path: '$path.url', - message: '服务器 URL 格式无效: ${server.url}', - type: ValidationErrorType.format, - suggestion: '请使用有效的 URL 格式,如 "https://api.example.com"', - )); + _errors.add( + ValidationError( + path: '$path.url', + message: '服务器 URL 格式无效: ${server.url}', + type: ValidationErrorType.format, + suggestion: '请使用有效的 URL 格式,如 "https://api.example.com"', + ), + ); } // 验证服务器变量 server.variables.forEach((name, variable) { if (variable.defaultValue.isEmpty) { - _errors.add(ValidationError( - path: '$path.variables.$name.default', - message: '服务器变量必须有默认值', - type: ValidationErrorType.required, - )); + _errors.add( + ValidationError( + path: '$path.variables.$name.default', + message: '服务器变量必须有默认值', + type: ValidationErrorType.required, + ), + ); } }); } @@ -208,11 +220,13 @@ class SchemaValidator { /// 验证路径 void _validatePaths(Map paths) { if (paths.isEmpty) { - _errors.add(const ValidationError( - path: 'paths', - message: 'API 文档必须包含至少一个路径', - type: ValidationErrorType.required, - )); + _errors.add( + const ValidationError( + path: 'paths', + message: 'API 文档必须包含至少一个路径', + type: ValidationErrorType.required, + ), + ); return; } @@ -226,24 +240,28 @@ class SchemaValidator { void _validatePath(ApiPath path, String pathKey) { // 验证操作 ID if (path.operationId.isEmpty) { - _warnings.add(ValidationWarning( - path: '$pathKey.operationId', - message: '缺少操作 ID', - suggestion: '建议为每个操作添加唯一的 operationId', - )); + _warnings.add( + ValidationWarning( + path: '$pathKey.operationId', + message: '缺少操作 ID', + suggestion: '建议为每个操作添加唯一的 operationId', + ), + ); } // 验证摘要和描述 if (path.summary.isEmpty) { - _warnings.add(ValidationWarning( - path: '$pathKey.summary', - message: '缺少操作摘要', - suggestion: '建议添加简短的操作描述', - )); + _warnings.add( + ValidationWarning( + path: '$pathKey.summary', + message: '缺少操作摘要', + suggestion: '建议添加简短的操作描述', + ), + ); } // 验证参数 - for (int i = 0; i < path.parameters.length; i++) { + for (var i = 0; i < path.parameters.length; i++) { _validateParameter(path.parameters[i], '$pathKey.parameters[$i]'); } @@ -254,11 +272,13 @@ class SchemaValidator { // 验证响应 if (path.responses.isEmpty) { - _errors.add(ValidationError( - path: '$pathKey.responses', - message: '操作必须定义至少一个响应', - type: ValidationErrorType.required, - )); + _errors.add( + ValidationError( + path: '$pathKey.responses', + message: '操作必须定义至少一个响应', + type: ValidationErrorType.required, + ), + ); } else { path.responses.forEach((code, response) { _validateResponse(response, '$pathKey.responses["$code"]'); @@ -266,7 +286,7 @@ class SchemaValidator { } // 验证安全要求 - for (int i = 0; i < path.security.length; i++) { + for (var i = 0; i < path.security.length; i++) { _validateSecurityRequirement(path.security[i], '$pathKey.security[$i]'); } } @@ -274,40 +294,48 @@ class SchemaValidator { /// 验证参数 void _validateParameter(ApiParameter parameter, String path) { if (parameter.name.isEmpty) { - _errors.add(ValidationError( - path: '$path.name', - message: '参数名称不能为空', - type: ValidationErrorType.required, - )); + _errors.add( + ValidationError( + path: '$path.name', + message: '参数名称不能为空', + type: ValidationErrorType.required, + ), + ); } // 验证路径参数必须是必需的 if (parameter.location == ParameterLocation.path && !parameter.required) { - _errors.add(ValidationError( - path: '$path.required', - message: '路径参数必须是必需的', - type: ValidationErrorType.constraint, - )); + _errors.add( + ValidationError( + path: '$path.required', + message: '路径参数必须是必需的', + type: ValidationErrorType.constraint, + ), + ); } // 验证参数类型 if (parameter.type == PropertyType.unknown) { - _warnings.add(ValidationWarning( - path: '$path.type', - message: '参数类型未知', - suggestion: '建议明确指定参数类型', - )); + _warnings.add( + ValidationWarning( + path: '$path.type', + message: '参数类型未知', + suggestion: '建议明确指定参数类型', + ), + ); } } /// 验证请求体 void _validateRequestBody(ApiRequestBody requestBody, String path) { if (requestBody.content.isEmpty) { - _errors.add(ValidationError( - path: '$path.content', - message: '请求体必须定义至少一种内容类型', - type: ValidationErrorType.required, - )); + _errors.add( + ValidationError( + path: '$path.content', + message: '请求体必须定义至少一种内容类型', + type: ValidationErrorType.required, + ), + ); } requestBody.content.forEach((mediaType, content) { @@ -318,11 +346,13 @@ class SchemaValidator { /// 验证响应 void _validateResponse(ApiResponse response, String path) { if (response.description.isEmpty) { - _warnings.add(ValidationWarning( - path: '$path.description', - message: '响应缺少描述', - suggestion: '建议为响应添加描述', - )); + _warnings.add( + ValidationWarning( + path: '$path.description', + message: '响应缺少描述', + suggestion: '建议为响应添加描述', + ), + ); } response.content.forEach((mediaType, content) { @@ -332,24 +362,31 @@ class SchemaValidator { /// 验证媒体类型 void _validateMediaType( - ApiMediaType mediaType, String path, String contentType) { + ApiMediaType mediaType, + String path, + String contentType, + ) { // 验证 schema if (mediaType.schema == null) { - _warnings.add(ValidationWarning( - path: '$path.schema', - message: '媒体类型缺少 schema 定义', - suggestion: '建议为媒体类型添加 schema', - )); + _warnings.add( + ValidationWarning( + path: '$path.schema', + message: '媒体类型缺少 schema 定义', + suggestion: '建议为媒体类型添加 schema', + ), + ); } // 验证编码(仅适用于 multipart 和 form data) if (contentType.startsWith('multipart/') || contentType.contains('form')) { if (mediaType.encoding.isEmpty) { - _warnings.add(ValidationWarning( - path: '$path.encoding', - message: '表单数据建议定义编码信息', - suggestion: '为文件上传字段添加 contentType 等编码信息', - )); + _warnings.add( + ValidationWarning( + path: '$path.encoding', + message: '表单数据建议定义编码信息', + suggestion: '为文件上传字段添加 contentType 等编码信息', + ), + ); } } } @@ -372,11 +409,13 @@ class SchemaValidator { /// 验证模型 void _validateModel(ApiModel model, String path) { if (model.name.isEmpty) { - _errors.add(ValidationError( - path: '$path.name', - message: '模型名称不能为空', - type: ValidationErrorType.required, - )); + _errors.add( + ValidationError( + path: '$path.name', + message: '模型名称不能为空', + type: ValidationErrorType.required, + ), + ); } // 验证属性 @@ -387,11 +426,13 @@ class SchemaValidator { // 验证必需字段 for (final requiredField in model.required) { if (!model.properties.containsKey(requiredField)) { - _errors.add(ValidationError( - path: '$path.required', - message: '必需字段 "$requiredField" 在属性中未定义', - type: ValidationErrorType.reference, - )); + _errors.add( + ValidationError( + path: '$path.required', + message: '必需字段 "$requiredField" 在属性中未定义', + type: ValidationErrorType.reference, + ), + ); } } } @@ -399,42 +440,52 @@ class SchemaValidator { /// 验证属性 void _validateProperty(ApiProperty property, String path) { if (property.name.isEmpty) { - _errors.add(ValidationError( - path: '$path.name', - message: '属性名称不能为空', - type: ValidationErrorType.required, - )); + _errors.add( + ValidationError( + path: '$path.name', + message: '属性名称不能为空', + type: ValidationErrorType.required, + ), + ); } if (property.type == PropertyType.unknown) { - _warnings.add(ValidationWarning( - path: '$path.type', - message: '属性类型未知', - suggestion: '建议明确指定属性类型', - )); + _warnings.add( + ValidationWarning( + path: '$path.type', + message: '属性类型未知', + suggestion: '建议明确指定属性类型', + ), + ); } } /// 验证安全方案 - void _validateSecurity(List security, - Map schemes) { - for (int i = 0; i < security.length; i++) { + void _validateSecurity( + List security, + Map schemes, + ) { + for (var i = 0; i < security.length; i++) { _validateSecurityRequirement(security[i], 'security[$i]'); } } /// 验证安全要求 void _validateSecurityRequirement( - ApiSecurityRequirement requirement, String path) { + ApiSecurityRequirement requirement, + String path, + ) { for (final schemeName in requirement.schemeNames) { // 这里应该验证安全方案是否在 components.securitySchemes 中定义 // 但由于当前模型结构限制,我们只能添加警告 if (schemeName.isEmpty) { - _warnings.add(ValidationWarning( - path: path, - message: '安全方案名称为空', - suggestion: '请确保安全方案名称有效', - )); + _warnings.add( + ValidationWarning( + path: path, + message: '安全方案名称为空', + suggestion: '请确保安全方案名称有效', + ), + ); } } } @@ -444,41 +495,45 @@ class SchemaValidator { switch (scheme.type) { case SecuritySchemeType.apiKey: if (scheme.name == null || scheme.name!.isEmpty) { - _errors.add(ValidationError( - path: '$path.name', - message: 'API Key 安全方案必须指定参数名称', - type: ValidationErrorType.required, - )); + _errors.add( + ValidationError( + path: '$path.name', + message: 'API Key 安全方案必须指定参数名称', + type: ValidationErrorType.required, + ), + ); } - break; case SecuritySchemeType.http: if (scheme.scheme == null || scheme.scheme!.isEmpty) { - _errors.add(ValidationError( - path: '$path.scheme', - message: 'HTTP 安全方案必须指定认证方案', - type: ValidationErrorType.required, - )); + _errors.add( + ValidationError( + path: '$path.scheme', + message: 'HTTP 安全方案必须指定认证方案', + type: ValidationErrorType.required, + ), + ); } - break; case SecuritySchemeType.oauth2: if (scheme.flows == null) { - _errors.add(ValidationError( - path: '$path.flows', - message: 'OAuth2 安全方案必须定义流程', - type: ValidationErrorType.required, - )); + _errors.add( + ValidationError( + path: '$path.flows', + message: 'OAuth2 安全方案必须定义流程', + type: ValidationErrorType.required, + ), + ); } - break; case SecuritySchemeType.openIdConnect: if (scheme.openIdConnectUrl == null || scheme.openIdConnectUrl!.isEmpty) { - _errors.add(ValidationError( - path: '$path.openIdConnectUrl', - message: 'OpenID Connect 安全方案必须指定 URL', - type: ValidationErrorType.required, - )); + _errors.add( + ValidationError( + path: '$path.openIdConnectUrl', + message: 'OpenID Connect 安全方案必须指定 URL', + type: ValidationErrorType.required, + ), + ); } - break; } } @@ -487,7 +542,7 @@ class SchemaValidator { try { final uri = Uri.parse(url); return uri.hasScheme && (uri.scheme == 'http' || uri.scheme == 'https'); - } catch (e) { + } on Object { return false; } } @@ -507,11 +562,13 @@ class SchemaValidator { void _validateOpenApiVersion(SwaggerDocument document) { // SwaggerDocument 没有直接的 openApiVersion 属性 // 这里我们假设它是 OpenAPI 3.0 兼容的 - _warnings.add(const ValidationWarning( - path: 'openapi', - message: '无法验证 OpenAPI 版本', - suggestion: '确保使用 OpenAPI 3.0.x 或 3.1.x 版本', - )); + _warnings.add( + const ValidationWarning( + path: 'openapi', + message: '无法验证 OpenAPI 版本', + suggestion: '确保使用 OpenAPI 3.0.x 或 3.1.x 版本', + ), + ); } /// 验证路径结构 @@ -519,15 +576,17 @@ class SchemaValidator { final pathPatterns = document.paths.keys.toList(); // 检查路径冲突 - for (int i = 0; i < pathPatterns.length; i++) { - for (int j = i + 1; j < pathPatterns.length; j++) { + for (var i = 0; i < pathPatterns.length; i++) { + for (var j = i + 1; j < pathPatterns.length; j++) { if (_pathsConflict(pathPatterns[i], pathPatterns[j])) { - _errors.add(ValidationError( - path: 'paths', - message: '路径冲突: "${pathPatterns[i]}" 与 "${pathPatterns[j]}"', - type: ValidationErrorType.constraint, - suggestion: '确保路径模式不会产生歧义', - )); + _errors.add( + ValidationError( + path: 'paths', + message: '路径冲突: "${pathPatterns[i]}" 与 "${pathPatterns[j]}"', + type: ValidationErrorType.constraint, + suggestion: '确保路径模式不会产生歧义', + ), + ); } } } @@ -544,23 +603,27 @@ class SchemaValidator { // 检查路径中的参数是否都有声明 for (final param in pathParams) { if (!declaredParams.contains(param)) { - _errors.add(ValidationError( - path: 'paths["$pathPattern"][${path.method.value}].parameters', - message: '路径参数 "$param" 未在参数列表中声明', - type: ValidationErrorType.reference, - suggestion: '添加路径参数的声明', - )); + _errors.add( + ValidationError( + path: 'paths["$pathPattern"][${path.method.value}].parameters', + message: '路径参数 "$param" 未在参数列表中声明', + type: ValidationErrorType.reference, + suggestion: '添加路径参数的声明', + ), + ); } } // 检查声明的路径参数是否都在路径中使用 for (final param in declaredParams) { if (!pathParams.contains(param)) { - _warnings.add(ValidationWarning( - path: 'paths["$pathPattern"][${path.method.value}].parameters', - message: '声明的路径参数 "$param" 未在路径中使用', - suggestion: '移除未使用的参数声明或修正路径', - )); + _warnings.add( + ValidationWarning( + path: 'paths["$pathPattern"][${path.method.value}].parameters', + message: '声明的路径参数 "$param" 未在路径中使用', + suggestion: '移除未使用的参数声明或修正路径', + ), + ); } } }); @@ -580,34 +643,40 @@ class SchemaValidator { // 检查未定义的引用 for (final ref in schemaRefs) { if (!schemas.contains(ref)) { - _errors.add(ValidationError( - path: 'components.schemas', - message: '引用的 schema "$ref" 未定义', - type: ValidationErrorType.reference, - suggestion: '定义缺失的 schema 或修正引用', - )); + _errors.add( + ValidationError( + path: 'components.schemas', + message: '引用的 schema "$ref" 未定义', + type: ValidationErrorType.reference, + suggestion: '定义缺失的 schema 或修正引用', + ), + ); } } for (final ref in securityRefs) { if (!securitySchemes.contains(ref)) { - _errors.add(ValidationError( - path: 'components.securitySchemes', - message: '引用的安全方案 "$ref" 未定义', - type: ValidationErrorType.reference, - suggestion: '定义缺失的安全方案或修正引用', - )); + _errors.add( + ValidationError( + path: 'components.securitySchemes', + message: '引用的安全方案 "$ref" 未定义', + type: ValidationErrorType.reference, + suggestion: '定义缺失的安全方案或修正引用', + ), + ); } } // 检查未使用的组件 for (final schema in schemas) { if (!schemaRefs.contains(schema)) { - _warnings.add(ValidationWarning( - path: 'components.schemas["$schema"]', - message: 'Schema "$schema" 已定义但未被使用', - suggestion: '移除未使用的 schema 或添加引用', - )); + _warnings.add( + ValidationWarning( + path: 'components.schemas["$schema"]', + message: 'Schema "$schema" 已定义但未被使用', + suggestion: '移除未使用的 schema 或添加引用', + ), + ); } } } @@ -617,32 +686,37 @@ class SchemaValidator { final definedSchemes = document.components.securitySchemes.keys.toSet(); // 检查全局安全要求 - for (int i = 0; i < document.security.length; i++) { + for (var i = 0; i < document.security.length; i++) { final requirement = document.security[i]; for (final schemeName in requirement.schemeNames) { if (!definedSchemes.contains(schemeName)) { - _errors.add(ValidationError( - path: 'security[$i]', - message: '引用的安全方案 "$schemeName" 未定义', - type: ValidationErrorType.reference, - suggestion: '在 components.securitySchemes 中定义该安全方案', - )); + _errors.add( + ValidationError( + path: 'security[$i]', + message: '引用的安全方案 "$schemeName" 未定义', + type: ValidationErrorType.reference, + suggestion: '在 components.securitySchemes 中定义该安全方案', + ), + ); } } } // 检查操作级别的安全要求 document.paths.forEach((pathPattern, path) { - for (int i = 0; i < path.security.length; i++) { + for (var i = 0; i < path.security.length; i++) { final requirement = path.security[i]; for (final schemeName in requirement.schemeNames) { if (!definedSchemes.contains(schemeName)) { - _errors.add(ValidationError( - path: 'paths["$pathPattern"][${path.method.value}].security[$i]', - message: '引用的安全方案 "$schemeName" 未定义', - type: ValidationErrorType.reference, - suggestion: '在 components.securitySchemes 中定义该安全方案', - )); + _errors.add( + ValidationError( + path: + 'paths["$pathPattern"][${path.method.value}].security[$i]', + message: '引用的安全方案 "$schemeName" 未定义', + type: ValidationErrorType.reference, + suggestion: '在 components.securitySchemes 中定义该安全方案', + ), + ); } } } @@ -655,16 +729,22 @@ class SchemaValidator { // 验证请求体示例 if (path.requestBody != null) { path.requestBody!.content.forEach((mediaType, content) { - _validateMediaTypeExamples(content, - '$pathPattern[${path.method.value}].requestBody.content["$mediaType"]'); + _validateMediaTypeExamples( + content, + '$pathPattern[${path.method.value}]' + '.requestBody.content["$mediaType"]', + ); }); } // 验证响应示例 path.responses.forEach((code, response) { response.content.forEach((mediaType, content) { - _validateMediaTypeExamples(content, - '$pathPattern[${path.method.value}].responses["$code"].content["$mediaType"]'); + _validateMediaTypeExamples( + content, + '$pathPattern[${path.method.value}]' + '.responses["$code"].content["$mediaType"]', + ); }); }); }); @@ -674,16 +754,18 @@ class SchemaValidator { void _validateMediaTypeExamples(ApiMediaType mediaType, String path) { // 检查 example 和 examples 不能同时存在 if (mediaType.example != null && mediaType.examples.isNotEmpty) { - _warnings.add(ValidationWarning( - path: path, - message: 'example 和 examples 不应同时存在', - suggestion: '使用 examples 对象来提供多个示例', - )); + _warnings.add( + ValidationWarning( + path: path, + message: 'example 和 examples 不应同时存在', + suggestion: '使用 examples 对象来提供多个示例', + ), + ); } // 验证示例格式 if (mediaType.example != null && mediaType.schema != null) { - // TODO: 根据 schema 验证 example 的格式 + // TODO(max): 根据 schema 验证 example 的格式 } } @@ -697,11 +779,13 @@ class SchemaValidator { }); if (!hasSuccessResponse) { - _warnings.add(ValidationWarning( - path: 'paths["$pathPattern"][${path.method.value}].responses', - message: '缺少成功响应 (2xx)', - suggestion: '添加至少一个成功响应', - )); + _warnings.add( + ValidationWarning( + path: 'paths["$pathPattern"][${path.method.value}].responses', + message: '缺少成功响应 (2xx)', + suggestion: '添加至少一个成功响应', + ), + ); } // 检查错误响应 @@ -711,11 +795,13 @@ class SchemaValidator { }); if (!hasErrorResponse) { - _warnings.add(ValidationWarning( - path: 'paths["$pathPattern"][${path.method.value}].responses', - message: '建议添加错误响应 (4xx/5xx)', - suggestion: '添加常见的错误响应,如 400、401、404、500', - )); + _warnings.add( + ValidationWarning( + path: 'paths["$pathPattern"][${path.method.value}].responses', + message: '建议添加错误响应 (4xx/5xx)', + suggestion: '添加常见的错误响应,如 400、401、404、500', + ), + ); } }); } @@ -730,12 +816,14 @@ class SchemaValidator { parameterNames.putIfAbsent(pathPattern, () => {}); if (parameterNames[pathPattern]!.contains(key)) { - _errors.add(ValidationError( - path: 'paths["$pathPattern"][${path.method.value}].parameters', - message: '重复的参数: ${param.name} (${param.location.name})', - type: ValidationErrorType.constraint, - suggestion: '确保参数名称在同一位置类型中唯一', - )); + _errors.add( + ValidationError( + path: 'paths["$pathPattern"][${path.method.value}].parameters', + message: '重复的参数: ${param.name} (${param.location.name})', + type: ValidationErrorType.constraint, + suggestion: '确保参数名称在同一位置类型中唯一', + ), + ); } else { parameterNames[pathPattern]!.add(key); } @@ -762,13 +850,16 @@ class SchemaValidator { } /// 收集所有引用 - void _collectReferences(SwaggerDocument document, Set schemaRefs, - Set securityRefs) { + void _collectReferences( + SwaggerDocument document, + Set schemaRefs, + Set securityRefs, + ) { // 从路径中收集引用 document.paths.forEach((pathPattern, path) { // 从参数中收集引用 for (final _ in path.parameters) { - // TODO: 收集参数 schema 引用 + // TODO(max): 收集参数 schema 引用 } // 从请求体中收集引用 @@ -799,18 +890,20 @@ class SchemaValidator { // 从组件中收集引用 document.components.schemas.forEach((name, model) { for (final _ in model.properties.values) { - // TODO: 收集属性 schema 引用 + // TODO(max): 收集属性 schema 引用 } }); } /// 收集 Schema 引用 void _collectSchemaReferences( - Map? schema, Set refs) { + Map? schema, + Set refs, + ) { if (schema == null) return; // 检查 $ref - final ref = schema['\$ref'] as String?; + final ref = schema[r'$ref'] as String?; if (ref != null && ref.startsWith('#/components/schemas/')) { final refName = ref.substring('#/components/schemas/'.length); refs.add(refName); diff --git a/pubspec.lock b/pubspec.lock index ca8e16d..895e28e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + sha256: f0bb5d1648339c8308cc0b9838d8456b3cfe5c91f9dc1a735b4d003269e5da9a url: "https://pub.flutter-io.cn" source: hosted - version: "67.0.0" + version: "88.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + sha256: "0b7b9c329d2879f8f05d6c05b32ee9ec025f39b077864bdb5ac9a7b63418a98f" url: "https://pub.flutter-io.cn" source: hosted - version: "6.4.1" + version: "8.1.1" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.3" args: dependency: transitive description: @@ -45,50 +53,34 @@ packages: dependency: transitive description: name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + sha256: c1668065e9ba04752570ad7e038288559d1e2ca5c6d0131c0f5f55e39e777413 url: "https://pub.flutter-io.cn" source: hosted - version: "2.4.1" + version: "4.0.3" build_config: dependency: transitive description: name: build_config - sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187" url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.2" + version: "1.2.0" build_daemon: dependency: transitive description: name: build_daemon - sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 url: "https://pub.flutter-io.cn" source: hosted - version: "4.0.4" - build_resolvers: - dependency: transitive - description: - name: build_resolvers - sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.4.2" + version: "4.1.1" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: "110c56ef29b5eb367b4d17fc79375fa8c18a6cd7acd92c05bb3986c17a079057" url: "https://pub.flutter-io.cn" source: hosted - version: "2.4.13" - build_runner_core: - dependency: transitive - description: - name: build_runner_core - sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 - url: "https://pub.flutter-io.cn" - source: hosted - version: "7.3.2" + version: "2.10.4" built_collection: dependency: transitive description: @@ -101,10 +93,10 @@ packages: dependency: transitive description: name: built_value - sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27" + sha256: "426cf75afdb23aa74bd4e471704de3f9393f3c7b04c1e2d9c6f1073ae0b8b139" url: "https://pub.flutter-io.cn" source: hosted - version: "8.10.1" + version: "8.12.1" checked_yaml: dependency: transitive description: @@ -125,10 +117,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243" url: "https://pub.flutter-io.cn" source: hosted - version: "4.10.1" + version: "4.11.0" collection: dependency: transitive description: @@ -157,26 +149,26 @@ packages: dependency: transitive description: name: crypto - sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.6" + version: "3.0.7" dart_style: dependency: transitive description: name: dart_style - sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + sha256: c87dfe3d56f183ffe9106a18aebc6db431fc7c98c31a54b952a77f3d54a85697 url: "https://pub.flutter-io.cn" source: hosted - version: "2.3.6" + version: "3.1.2" dio: dependency: "direct main" description: name: dio - sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" + sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 url: "https://pub.flutter-io.cn" source: hosted - version: "5.8.0+1" + version: "5.9.0" dio_web_adapter: dependency: transitive description: @@ -201,6 +193,30 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.0.0" + freezed: + dependency: "direct dev" + description: + name: freezed + sha256: "13065f10e135263a4f5a4391b79a8efc5fb8106f8dd555a9e49b750b45393d77" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.3" + freezed_annotation: + dependency: "direct main" + description: + name: freezed_annotation + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.0" frontend_server_client: dependency: transitive description: @@ -225,14 +241,22 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.3.2" + hotreloader: + dependency: transitive + description: + name: hotreloader + sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.3.0" http: dependency: "direct main" description: name: http - sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" url: "https://pub.flutter-io.cn" source: hosted - version: "1.4.0" + version: "1.6.0" http_multi_server: dependency: transitive description: @@ -257,14 +281,6 @@ packages: 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: @@ -277,10 +293,26 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + sha256: c5b2ee75210a0f263c6c7b9eeea80553dbae96ea1bf57f02484e806a3ffdffa3 url: "https://pub.flutter-io.cn" source: hosted - version: "6.8.0" + version: "6.11.2" + lean_builder: + dependency: transitive + description: + name: lean_builder + sha256: ef5cd5f907157eb7aa87d1704504b5a6386d2cbff88a3c2b3344477bab323ee9 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.2" + lints: + dependency: transitive + description: + name: lints + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.0.0" logging: dependency: "direct main" description: @@ -293,18 +325,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.flutter-io.cn" source: hosted - version: "0.12.17" + version: "0.12.18" meta: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.flutter-io.cn" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -341,18 +373,18 @@ packages: dependency: transitive description: name: pool - sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" url: "https://pub.flutter-io.cn" source: hosted - version: "1.5.1" + version: "1.5.2" protobuf: dependency: transitive description: name: protobuf - sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + sha256: "2fcc8a202ca7ec17dab7c97d6b6d91cf03aa07fe6f65f8afbb6dfa52cc5bd902" url: "https://pub.flutter-io.cn" source: hosted - version: "3.1.0" + version: "5.1.0" pub_semver: dependency: transitive description: @@ -373,18 +405,18 @@ packages: dependency: "direct main" description: name: retrofit - sha256: "84d70114a5b6bae5f4c1302335f9cb610ebeb1b02023d5e7e87697aaff52926a" + sha256: "84063c18a00d55af41d6b8401edf8473e8c215bd7068ef7ec5e34c60657ffdbe" url: "https://pub.flutter-io.cn" source: hosted - version: "4.6.0" + version: "4.9.1" retrofit_generator: dependency: "direct dev" description: name: retrofit_generator - sha256: "8dfc406cdfa171f33cbd21bf5bd8b6763548cc217de19cdeaa07a76727fac4ca" + sha256: "7ec323f3329ad2ca0bcdc96fe02ec7f2486ecfac6cd2d035b03c398ef6f42308" url: "https://pub.flutter-io.cn" source: hosted - version: "8.2.1" + version: "10.2.0" shelf: dependency: transitive description: @@ -413,26 +445,26 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.1" + version: "3.0.0" source_gen: dependency: transitive description: name: source_gen - sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + sha256: "07b277b67e0096c45196cbddddf2d8c6ffc49342e88bf31d460ce04605ddac75" url: "https://pub.flutter-io.cn" source: hosted - version: "1.5.0" + version: "4.1.1" source_helper: dependency: transitive description: name: source_helper - sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" + sha256: "6a3c6cc82073a8797f8c4dc4572146114a39652851c157db37e964d9c7038723" url: "https://pub.flutter-io.cn" source: hosted - version: "1.3.5" + version: "1.3.8" source_map_stack_trace: dependency: transitive description: @@ -501,42 +533,26 @@ packages: dependency: "direct dev" description: name: test - sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" + sha256: "77cc98ea27006c84e71a7356cf3daf9ddbde2d91d84f77dbfe64cf0e4d9611ae" url: "https://pub.flutter-io.cn" source: hosted - version: "1.26.2" + version: "1.28.0" test_api: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8" url: "https://pub.flutter-io.cn" source: hosted - version: "0.7.6" + version: "0.7.8" test_core: dependency: transitive description: name: test_core - sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" + sha256: f1072617a6657e5fc09662e721307f7fb009b4ed89b19f47175d11d5254a62d4 url: "https://pub.flutter-io.cn" source: hosted - version: "0.6.11" - timing: - dependency: transitive - description: - name: timing - sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.0.2" - tuple: - dependency: transitive - description: - name: tuple - sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.0.2" + version: "0.6.14" typed_data: dependency: transitive description: @@ -545,22 +561,30 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" + very_good_analysis: + dependency: "direct dev" + description: + name: very_good_analysis + sha256: "96245839dbcc45dfab1af5fa551603b5c7a282028a64746c19c547d21a7f1e3a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "10.0.0" vm_service: dependency: transitive description: name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" url: "https://pub.flutter-io.cn" source: hosted - version: "15.0.0" + version: "15.0.2" watcher: dependency: transitive description: name: watcher - sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" + sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a" url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.2" + version: "1.1.4" web: dependency: transitive description: @@ -593,6 +617,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.2.1" + xxh3: + dependency: transitive + description: + name: xxh3 + sha256: "399a0438f5d426785723c99da6b16e136f4953fb1e9db0bf270bd41dd4619916" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0" yaml: dependency: "direct main" description: @@ -602,4 +634,4 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.8.0 <4.0.0" + dart: ">=3.9.0 <4.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 6721a57..dfe7699 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,27 +15,27 @@ executables: swagger_generator_flutter: swagger_generator_flutter dependencies: - # Flutter SDK(可选,仅当需要 Flutter 特性时) - # 注释掉以支持纯 Dart 项目 - # flutter: - # sdk: flutter - - # 核心依赖 - path: ^1.8.0 - logging: ^1.1.0 - yaml: ^3.1.0 - + # HTTP 客户端 + dio: ^5.9.0 + # JSON 序列化 + freezed_annotation: ^3.1.0 # HTTP 和 API 相关(仅用于类型引用,不是运行时依赖) http: ^1.1.0 - dio: ^5.0.0 - retrofit: ^4.0.0 - json_annotation: ^4.8.1 + json_annotation: ^4.9.0 + # 核心依赖 + logging: ^1.3.0 + path: ^1.9.1 + # API 客户端 + retrofit: ^4.9.1 + yaml: ^3.1.3 dev_dependencies: + # 代码生成工具(仅用于测试/示例) + build_runner: ^2.10.4 + flutter_lints: 6.0.0 + freezed: ^3.2.3 + json_serializable: ^6.11.2 + retrofit_generator: ^10.2.0 # 测试框架 test: ^1.24.0 - - # 代码生成工具(仅用于测试/示例) - build_runner: ^2.4.7 - json_serializable: ^6.7.1 - retrofit_generator: ^8.0.0 \ No newline at end of file + very_good_analysis: ^10.0.0 diff --git a/tests/comprehensive_generator_test.dart b/tests/comprehensive_generator_test.dart index aa5aa94..eb68363 100644 --- a/tests/comprehensive_generator_test.dart +++ b/tests/comprehensive_generator_test.dart @@ -18,15 +18,14 @@ void main() { ), ], components: ApiComponents( - schemas: {}, securitySchemes: { - 'bearerAuth': const ApiSecurityScheme( + 'bearerAuth': ApiSecurityScheme( type: SecuritySchemeType.http, description: 'Bearer token', scheme: 'bearer', bearerFormat: 'JWT', ), - 'apiKey': const ApiSecurityScheme( + 'apiKey': ApiSecurityScheme( type: SecuritySchemeType.apiKey, description: 'API Key', name: 'X-API-Key', @@ -35,7 +34,7 @@ void main() { }, ), paths: { - '/users': const ApiPath( + '/users': ApiPath( path: '/users', method: HttpMethod.get, summary: 'Get all users', @@ -59,25 +58,25 @@ void main() { ), ], responses: { - '200': const ApiResponse( + '200': ApiResponse( code: '200', description: 'Successful response', content: { - 'application/json': const ApiMediaType( + 'application/json': ApiMediaType( schema: { 'type': 'array', 'items': { - '\$ref': '#/components/schemas/User', + r'$ref': '#/components/schemas/User', }, }, ), }, ), - '400': const ApiResponse( + '400': ApiResponse( code: '400', description: 'Bad request', ), - '401': const ApiResponse( + '401': ApiResponse( code: '401', description: 'Unauthorized', ), @@ -88,7 +87,7 @@ void main() { ), ], ), - '/users/{id}': const ApiPath( + '/users/{id}': ApiPath( path: '/users/{id}', method: HttpMethod.get, summary: 'Get user by ID', @@ -105,24 +104,24 @@ void main() { ), ], responses: { - '200': const ApiResponse( + '200': ApiResponse( code: '200', description: 'User found', content: { - 'application/json': const ApiMediaType( + 'application/json': ApiMediaType( schema: { - '\$ref': '#/components/schemas/User', + r'$ref': '#/components/schemas/User', }, ), }, ), - '404': const ApiResponse( + '404': ApiResponse( code: '404', description: 'User not found', ), }, ), - '/users/create': const ApiPath( + '/users/create': ApiPath( path: '/users/create', method: HttpMethod.post, summary: 'Create user', @@ -134,32 +133,32 @@ void main() { description: 'User data', required: true, content: { - 'application/json': const ApiMediaType( + 'application/json': ApiMediaType( schema: { - '\$ref': '#/components/schemas/CreateUserRequest', + r'$ref': '#/components/schemas/CreateUserRequest', }, ), }, ), responses: { - '201': const ApiResponse( + '201': ApiResponse( code: '201', description: 'User created', content: { - 'application/json': const ApiMediaType( + 'application/json': ApiMediaType( schema: { - '\$ref': '#/components/schemas/User', + r'$ref': '#/components/schemas/User', }, ), }, ), - '400': const ApiResponse( + '400': ApiResponse( code: '400', description: 'Invalid input', ), }, ), - '/files/upload': const ApiPath( + '/files/upload': ApiPath( path: '/files/upload', method: HttpMethod.post, summary: 'Upload file', @@ -171,7 +170,7 @@ void main() { description: 'File to upload', required: true, content: { - 'multipart/form-data': const ApiMediaType( + 'multipart/form-data': ApiMediaType( schema: { 'type': 'object', 'properties': { @@ -188,13 +187,13 @@ void main() { }, ), responses: { - '200': const ApiResponse( + '200': ApiResponse( code: '200', description: 'File uploaded successfully', content: { - 'application/json': const ApiMediaType( + 'application/json': ApiMediaType( schema: { - '\$ref': '#/components/schemas/FileUploadResult', + r'$ref': '#/components/schemas/FileUploadResult', }, ), }, @@ -203,29 +202,29 @@ void main() { ), }, models: { - 'User': const ApiModel( + 'User': ApiModel( name: 'User', description: 'User model', properties: { - 'id': const ApiProperty( + 'id': ApiProperty( name: 'id', type: PropertyType.integer, description: 'User ID', required: true, ), - 'name': const ApiProperty( + 'name': ApiProperty( name: 'name', type: PropertyType.string, description: 'User name', required: true, ), - 'email': const ApiProperty( + 'email': ApiProperty( name: 'email', type: PropertyType.string, description: 'User email', required: true, ), - 'createdAt': const ApiProperty( + 'createdAt': ApiProperty( name: 'createdAt', type: PropertyType.string, description: 'Creation timestamp', @@ -234,17 +233,17 @@ void main() { }, required: ['id', 'name', 'email'], ), - 'CreateUserRequest': const ApiModel( + 'CreateUserRequest': ApiModel( name: 'CreateUserRequest', description: 'Request model for creating a user', properties: { - 'name': const ApiProperty( + 'name': ApiProperty( name: 'name', type: PropertyType.string, description: 'User name', required: true, ), - 'email': const ApiProperty( + 'email': ApiProperty( name: 'email', type: PropertyType.string, description: 'User email', @@ -253,23 +252,23 @@ void main() { }, required: ['name', 'email'], ), - 'FileUploadResult': const ApiModel( + 'FileUploadResult': ApiModel( name: 'FileUploadResult', description: 'Result of file upload', properties: { - 'url': const ApiProperty( + 'url': ApiProperty( name: 'url', type: PropertyType.string, description: 'File URL', required: true, ), - 'filename': const ApiProperty( + 'filename': ApiProperty( name: 'filename', type: PropertyType.string, description: 'Original filename', required: true, ), - 'size': const ApiProperty( + 'size': ApiProperty( name: 'size', type: PropertyType.integer, description: 'File size in bytes', @@ -301,87 +300,87 @@ void main() { expect(result, contains('abstract class TestApiService')); expect(result, contains('@RestApi()')); expect(result, contains('factory TestApiService(Dio dio')); - expect(result, contains('@GET(\'/users\')')); - expect(result, contains('@POST(\'/users\')')); - expect(result, contains('@Path(\'id\')')); - expect(result, contains('@Query(\'page\')')); + expect(result, contains("@GET('/users')")); + expect(result, contains("@POST('/users')")); + expect(result, contains("@Path('id')")); + expect(result, contains("@Query('page')")); expect(result, contains('@Body()')); }); test('generates split APIs by tags', () { final generator = RetrofitApiGenerator( className: 'ApiService', - splitByTags: true, ); final result = generator.generateFromDocument(testDocument); expect(result, isNotEmpty); - expect(result, contains('UsersApi')); - expect(result, contains('FilesApi')); + // The main file should contain the aggregator class expect(result, contains('class ApiService')); - expect(result, contains('late final UsersApi users')); - expect(result, contains('late final FilesApi files')); + // It should have getters for the individual API services + expect(result, contains('UsersApi get users => _usersApi;')); + expect(result, contains('FilesApi get files => _filesApi;')); + // It should import the tag-based API files + expect(result, contains("import 'users_api.dart';")); + expect(result, contains("import 'files_api.dart';")); }); test('handles file upload endpoints', () { - final generator = RetrofitApiGenerator(); + final generator = RetrofitApiGenerator(splitByTags: false); final result = generator.generateFromDocument(testDocument); - expect(result, contains('@POST(\'/files/upload\')')); + expect(result, contains("@POST('/files/upload')")); expect(result, contains('@MultiPart()')); expect(result, contains('MultipartFile')); }); test('generates proper parameter annotations', () { - final generator = RetrofitApiGenerator(); + final generator = RetrofitApiGenerator(splitByTags: false); final result = generator.generateFromDocument(testDocument); // Path parameters - expect(result, contains('@Path(\'id\') int id')); + expect(result, contains("@Path('id') int id")); // Query parameters - expect(result, contains('@Query(\'page\') int? page')); - expect(result, contains('@Query(\'limit\') int? limit')); + expect(result, contains("@Query('page') int? page")); + expect(result, contains("@Query('limit') int? limit")); // Body parameters - expect(result, contains('@Body() CreateUserRequest body')); + expect(result, contains('@Body() CreateUserRequest request')); }); test('generates security annotations', () { - final generator = RetrofitApiGenerator(); + final generator = RetrofitApiGenerator(splitByTags: false); final result = generator.generateFromDocument(testDocument); // Should include headers for authentication expect( - result, anyOf([contains('Authorization'), contains('X-API-Key')])); + result, anyOf([contains('Authorization'), contains('X-API-Key')]),); }); }); group('Code Quality', () { test('generated code is valid Dart syntax', () { - final generator = RetrofitApiGenerator(); + final generator = RetrofitApiGenerator(splitByTags: false); final result = generator.generateFromDocument(testDocument); // Basic syntax checks expect(result, isNot(contains(';;'))); // No double semicolons expect(result, isNot(contains(',,'))); // No double commas - expect(result, - isNot(contains(' '))); // No double spaces (basic formatting) // Check for proper imports - expect(result, contains('import \'package:dio/dio.dart\';')); - expect(result, contains('import \'package:retrofit/retrofit.dart\';')); + expect(result, contains("import 'package:dio/dio.dart';")); + expect(result, contains("import 'package:retrofit/retrofit.dart';")); // Check for proper class structure final classMatches = RegExp(r'class \w+').allMatches(result); final abstractClassMatches = RegExp(r'abstract class \w+').allMatches(result); expect( - classMatches.length + abstractClassMatches.length, greaterThan(0)); + classMatches.length + abstractClassMatches.length, greaterThan(0),); }); test('handles special characters in names', () { @@ -389,10 +388,8 @@ void main() { title: 'API with Special-Characters_and.dots', version: '1.0.0', description: 'Test API', - servers: [], - components: ApiComponents(schemas: {}, securitySchemes: {}), paths: { - '/special-endpoint_with.dots': const ApiPath( + '/special-endpoint_with.dots': ApiPath( path: '/special-endpoint_with.dots', method: HttpMethod.get, summary: 'Special endpoint', @@ -401,7 +398,7 @@ void main() { tags: ['special-tag_with.dots'], parameters: [], responses: { - '200': const ApiResponse( + '200': ApiResponse( code: '200', description: 'Success', ), @@ -410,10 +407,9 @@ void main() { }, models: {}, controllers: {}, - security: [], ); - final generator = RetrofitApiGenerator(); + final generator = RetrofitApiGenerator(splitByTags: false); final result = generator.generateFromDocument(specialDocument); expect(result, isNotEmpty); @@ -422,15 +418,15 @@ void main() { }); test('handles nullable and required fields correctly', () { - final generator = RetrofitApiGenerator(); + final generator = RetrofitApiGenerator(splitByTags: false); final result = generator.generateFromDocument(testDocument); // Required path parameters should not be nullable - expect(result, contains('@Path(\'id\') int id')); + expect(result, contains("@Path('id') int id")); // Optional query parameters should be nullable - expect(result, contains('@Query(\'page\') int? page')); - expect(result, contains('@Query(\'limit\') int? limit')); + expect(result, contains("@Query('page') int? page")); + expect(result, contains("@Query('limit') int? limit")); }); }); @@ -440,19 +436,16 @@ void main() { title: 'Empty API', version: '1.0.0', description: 'Empty test API', - servers: [], - components: ApiComponents(schemas: {}, securitySchemes: {}), paths: {}, models: {}, controllers: {}, - security: [], ); final generator = RetrofitApiGenerator(); final result = generator.generateFromDocument(emptyDocument); expect(result, isNotEmpty); - expect(result, contains('Empty API')); + // Should still generate basic structure even with no paths }); @@ -461,10 +454,8 @@ void main() { title: 'Test API', version: '1.0.0', description: 'Test', - servers: [], - components: ApiComponents(schemas: {}, securitySchemes: {}), paths: { - '/test': const ApiPath( + '/test': ApiPath( path: '/test', method: HttpMethod.get, summary: 'Test endpoint', @@ -473,7 +464,7 @@ void main() { tags: [], parameters: [], responses: { - '200': const ApiResponse( + '200': ApiResponse( code: '200', description: 'Success', ), @@ -482,13 +473,12 @@ void main() { }, models: {}, controllers: {}, - security: [], ); - final generator = RetrofitApiGenerator(); + final generator = RetrofitApiGenerator(splitByTags: false); expect( () => generator.generateFromDocument(documentWithoutOperationIds), - returnsNormally); + returnsNormally,); }); }); }); diff --git a/tests/comprehensive_parser_test.dart b/tests/comprehensive_parser_test.dart index d1fe4d5..6d888eb 100644 --- a/tests/comprehensive_parser_test.dart +++ b/tests/comprehensive_parser_test.dart @@ -5,7 +5,7 @@ void main() { group('Comprehensive Parser Tests', () { group('OpenAPI 3.0 Core Features', () { test('parses basic OpenAPI 3.0 document', () { - final json = { + final json = { 'openapi': '3.0.3', 'info': { 'title': 'Test API', @@ -31,7 +31,7 @@ void main() { 'schema': { 'type': 'array', 'items': { - '\$ref': '#/components/schemas/User', + r'$ref': '#/components/schemas/User', }, }, }, @@ -73,7 +73,7 @@ void main() { }); test('parses servers with variables', () { - final json = { + final json = { 'openapi': '3.0.3', 'info': {'title': 'Test', 'version': '1.0.0'}, 'servers': [ @@ -100,7 +100,7 @@ void main() { final server = document.servers.first; expect( - server.url, equals('https://{environment}.example.com/{basePath}')); + server.url, equals('https://{environment}.example.com/{basePath}'),); expect(server.variables, hasLength(2)); expect(server.variables.containsKey('environment'), isTrue); expect(server.variables['environment']!.defaultValue, equals('api')); @@ -108,7 +108,7 @@ void main() { }); test('parses complex request body', () { - final json = { + final json = { 'openapi': '3.0.3', 'info': {'title': 'Test', 'version': '1.0.0'}, 'paths': { @@ -121,7 +121,7 @@ void main() { 'content': { 'application/json': { 'schema': { - '\$ref': '#/components/schemas/User', + r'$ref': '#/components/schemas/User', }, 'examples': { 'user1': { @@ -135,7 +135,7 @@ void main() { }, 'application/xml': { 'schema': { - '\$ref': '#/components/schemas/User', + r'$ref': '#/components/schemas/User', }, }, }, @@ -166,9 +166,9 @@ void main() { expect(path.requestBody!.required, isTrue); expect(path.requestBody!.content, hasLength(2)); expect( - path.requestBody!.content.containsKey('application/json'), isTrue); + path.requestBody!.content.containsKey('application/json'), isTrue,); expect( - path.requestBody!.content.containsKey('application/xml'), isTrue); + path.requestBody!.content.containsKey('application/xml'), isTrue,); final jsonContent = path.requestBody!.content['application/json']!; expect(jsonContent.examples, hasLength(1)); @@ -176,7 +176,7 @@ void main() { }); test('parses complex responses with headers and links', () { - final json = { + final json = { 'openapi': '3.0.3', 'info': {'title': 'Test', 'version': '1.0.0'}, 'paths': { @@ -203,7 +203,7 @@ void main() { 'content': { 'application/json': { 'schema': { - '\$ref': '#/components/schemas/User', + r'$ref': '#/components/schemas/User', }, }, }, @@ -211,7 +211,7 @@ void main() { 'getUserPosts': { 'operationId': 'getUserPosts', 'parameters': { - 'userId': '\$response.body#/id', + 'userId': r'$response.body#/id', }, }, }, @@ -253,7 +253,7 @@ void main() { }); test('parses security schemes and requirements', () { - final json = { + final json = { 'openapi': '3.0.3', 'info': {'title': 'Test', 'version': '1.0.0'}, 'security': [ @@ -266,7 +266,7 @@ void main() { 'summary': 'Protected endpoint', 'security': [ { - 'bearerAuth': ['read:users'] + 'bearerAuth': ['read:users'], }, ], 'responses': { @@ -333,7 +333,7 @@ void main() { group('Schema Validation', () { test('parses allOf composition', () { - final json = { + final json = { 'openapi': '3.0.3', 'info': {'title': 'Test', 'version': '1.0.0'}, 'paths': {}, @@ -348,7 +348,7 @@ void main() { }, 'Dog': { 'allOf': [ - {'\$ref': '#/components/schemas/Pet'}, + {r'$ref': '#/components/schemas/Pet'}, { 'type': 'object', 'properties': { @@ -373,7 +373,7 @@ void main() { }); test('parses oneOf and anyOf', () { - final json = { + final json = { 'openapi': '3.0.3', 'info': {'title': 'Test', 'version': '1.0.0'}, 'paths': {}, @@ -403,7 +403,7 @@ void main() { }); test('parses discriminator', () { - final json = { + final json = { 'openapi': '3.0.3', 'info': {'title': 'Test', 'version': '1.0.0'}, 'paths': {}, @@ -426,7 +426,7 @@ void main() { }, 'Dog': { 'allOf': [ - {'\$ref': '#/components/schemas/Pet'}, + {r'$ref': '#/components/schemas/Pet'}, { 'type': 'object', 'properties': { @@ -437,7 +437,7 @@ void main() { }, 'Cat': { 'allOf': [ - {'\$ref': '#/components/schemas/Pet'}, + {r'$ref': '#/components/schemas/Pet'}, { 'type': 'object', 'properties': { @@ -461,18 +461,18 @@ void main() { group('Error Handling', () { test('handles missing required fields gracefully', () { - final json = { + final json = { 'openapi': '3.0.3', // Missing info object 'paths': {}, }; expect(() => SwaggerDocument.fromJson(json), - throwsA(isA())); + throwsA(isA()),); }); test('handles invalid OpenAPI version', () { - final json = { + final json = { 'openapi': '2.0', // Invalid version 'info': {'title': 'Test', 'version': '1.0.0'}, 'paths': {}, @@ -483,21 +483,21 @@ void main() { }); test('handles malformed paths', () { - final json = { + final json = { 'openapi': '3.0.3', 'info': {'title': 'Test', 'version': '1.0.0'}, 'paths': { '/valid': { 'get': { 'responses': { - '200': {'description': 'OK'} + '200': {'description': 'OK'}, }, }, }, '/invalid': { 'invalidMethod': { 'responses': { - '200': {'description': 'OK'} + '200': {'description': 'OK'}, }, }, }, @@ -510,7 +510,7 @@ void main() { }); test('handles circular references', () { - final json = { + final json = { 'openapi': '3.0.3', 'info': {'title': 'Test', 'version': '1.0.0'}, 'paths': {}, @@ -522,7 +522,7 @@ void main() { 'value': {'type': 'string'}, 'children': { 'type': 'array', - 'items': {'\$ref': '#/components/schemas/Node'}, + 'items': {r'$ref': '#/components/schemas/Node'}, }, }, }, @@ -539,7 +539,7 @@ void main() { group('Edge Cases', () { test('handles empty document', () { - final json = { + final json = { 'openapi': '3.0.3', 'info': {'title': 'Empty API', 'version': '1.0.0'}, 'paths': {}, @@ -556,12 +556,12 @@ void main() { final schemas = {}; // Create a large number of paths and schemas - for (int i = 0; i < 1000; i++) { + for (var i = 0; i < 1000; i++) { paths['/resource$i'] = { 'get': { 'summary': 'Get resource $i', 'responses': { - '200': {'description': 'Success'} + '200': {'description': 'Success'}, }, }, }; @@ -575,7 +575,7 @@ void main() { }; } - final json = { + final json = { 'openapi': '3.0.3', 'info': {'title': 'Large API', 'version': '1.0.0'}, 'paths': paths, @@ -589,11 +589,11 @@ void main() { expect(document.paths.length, greaterThan(500)); expect(document.models.length, greaterThan(500)); expect(stopwatch.elapsedMilliseconds, - lessThan(10000)); // Should complete within 10 seconds + lessThan(10000),); // Should complete within 10 seconds }); test('handles unicode and special characters', () { - final json = { + final json = { 'openapi': '3.0.3', 'info': { 'title': 'API with 中文 and émojis 🚀', @@ -605,7 +605,7 @@ void main() { 'get': { 'summary': 'Test with unicode path', 'responses': { - '200': {'description': 'Success'} + '200': {'description': 'Success'}, }, }, }, diff --git a/tests/encoding_test.dart b/tests/encoding_test.dart index 104fc13..e2ab04b 100644 --- a/tests/encoding_test.dart +++ b/tests/encoding_test.dart @@ -13,7 +13,7 @@ void main() { expect( encoded.length, greaterThan( - testString.length)); // UTF-8 uses multiple bytes for non-ASCII + testString.length,),); // UTF-8 uses multiple bytes for non-ASCII }); test('handles ASCII encoding', () { @@ -23,7 +23,7 @@ void main() { expect(decoded, testString); expect( - encoded.length, testString.length); // ASCII is 1 byte per character + encoded.length, testString.length,); // ASCII is 1 byte per character }); test('handles Latin1 encoding', () { @@ -46,7 +46,7 @@ void main() { }); test('handles URL encoding and decoding', () { - const testString = 'Hello World & Special Characters!@#\$%^&*()'; + const testString = r'Hello World & Special Characters!@#$%^&*()'; final encoded = Uri.encodeComponent(testString); final decoded = Uri.decodeComponent(encoded); @@ -60,7 +60,7 @@ void main() { final testBytes = utf8Bom + utf8.encode('Hello'); // 检测 BOM - final bool hasUtf8Bom = testBytes.length >= 3 && + final hasUtf8Bom = testBytes.length >= 3 && testBytes[0] == 0xEF && testBytes[1] == 0xBB && testBytes[2] == 0xBF; @@ -71,7 +71,7 @@ void main() { test('detects BOM for UTF-16LE', () { final utf16leBom = [0xFF, 0xFE]; - final bool hasUtf16LeBom = utf16leBom.length >= 2 && + final hasUtf16LeBom = utf16leBom.length >= 2 && utf16leBom[0] == 0xFF && utf16leBom[1] == 0xFE; @@ -81,7 +81,7 @@ void main() { test('detects BOM for UTF-16BE', () { final utf16beBom = [0xFE, 0xFF]; - final bool hasUtf16BeBom = utf16beBom.length >= 2 && + final hasUtf16BeBom = utf16beBom.length >= 2 && utf16beBom[0] == 0xFE && utf16beBom[1] == 0xFF; @@ -167,7 +167,7 @@ void main() { final encodedPairs = []; formData.forEach((key, value) { final encodedKey = Uri.encodeComponent(key); - final encodedValue = Uri.encodeComponent(value.toString()); + final encodedValue = Uri.encodeComponent(value); encodedPairs.add('$encodedKey=$encodedValue'); }); diff --git a/tests/enhanced_validator_test.dart b/tests/enhanced_validator_test.dart index 1fdd1cb..5fd3d4e 100644 --- a/tests/enhanced_validator_test.dart +++ b/tests/enhanced_validator_test.dart @@ -9,7 +9,7 @@ void main() { setUp(() { validator = EnhancedValidator( - includeWarnings: true, + ); }); @@ -24,12 +24,8 @@ void main() { description: 'Production server', ), ], - components: ApiComponents( - schemas: {}, - securitySchemes: {}, - ), paths: { - '/users': const ApiPath( + '/users': ApiPath( path: '/users', method: HttpMethod.get, summary: 'Get users', @@ -38,11 +34,11 @@ void main() { tags: ['users'], parameters: [], responses: { - '200': const ApiResponse( + '200': ApiResponse( code: '200', description: 'Success', content: { - 'application/json': const ApiMediaType( + 'application/json': ApiMediaType( schema: {'type': 'array'}, ), }, @@ -52,7 +48,6 @@ void main() { }, models: {}, controllers: {}, - security: [], ); final isValid = validator.validateDocument(document); @@ -65,12 +60,9 @@ void main() { title: '', // Missing title version: '', // Missing version description: '', - servers: [], - components: ApiComponents(schemas: {}, securitySchemes: {}), paths: {}, // Empty paths models: {}, controllers: {}, - security: [], ); final isValid = validator.validateDocument(document); @@ -88,10 +80,8 @@ void main() { title: 'Test API', version: '1.0.0', description: 'Test', - servers: [], - components: ApiComponents(schemas: {}, securitySchemes: {}), paths: { - '/users/{id}': const ApiPath( + '/users/{id}': ApiPath( path: '/users/{id}', method: HttpMethod.get, summary: 'Get user', @@ -102,7 +92,7 @@ void main() { // Missing path parameter declaration for 'id' ], responses: { - '200': const ApiResponse( + '200': ApiResponse( code: '200', description: 'Success', ), @@ -111,7 +101,6 @@ void main() { }, models: {}, controllers: {}, - security: [], ); final isValid = validator.validateDocument(document); @@ -126,10 +115,8 @@ void main() { title: 'Test API', version: '1.0.0', description: 'Test', - servers: [], - components: ApiComponents(schemas: {}, securitySchemes: {}), paths: { - '/users/{id}': const ApiPath( + '/users/{id}': ApiPath( path: '/users/{id}', method: HttpMethod.get, summary: 'Get user', @@ -146,7 +133,7 @@ void main() { ), ], responses: { - '200': const ApiResponse( + '200': ApiResponse( code: '200', description: 'Success', ), @@ -155,7 +142,6 @@ void main() { }, models: {}, controllers: {}, - security: [], ); final isValid = validator.validateDocument(document); @@ -170,17 +156,15 @@ void main() { title: 'Test API', version: '1.0.0', description: 'Test', - servers: [], components: ApiComponents( - schemas: {}, securitySchemes: { - 'apiKey': const ApiSecurityScheme( + 'apiKey': ApiSecurityScheme( type: SecuritySchemeType.apiKey, description: 'API Key', name: '', // Missing name location: ApiKeyLocation.header, ), - 'bearer': const ApiSecurityScheme( + 'bearer': ApiSecurityScheme( type: SecuritySchemeType.http, description: 'Bearer token', scheme: '', // Missing scheme @@ -188,7 +172,7 @@ void main() { }, ), paths: { - '/test': const ApiPath( + '/test': ApiPath( path: '/test', method: HttpMethod.get, summary: 'Test', @@ -197,7 +181,7 @@ void main() { tags: [], parameters: [], responses: { - '200': const ApiResponse( + '200': ApiResponse( code: '200', description: 'Success', ), @@ -206,7 +190,6 @@ void main() { }, models: {}, controllers: {}, - security: [], ); final isValid = validator.validateDocument(document); @@ -222,10 +205,8 @@ void main() { title: 'Test API', version: '1.0.0', description: '', // Missing description - servers: [], // Missing servers - components: ApiComponents(schemas: {}, securitySchemes: {}), paths: { - '/test': const ApiPath( + '/test': ApiPath( path: '/test', method: HttpMethod.get, summary: '', // Missing summary @@ -234,7 +215,7 @@ void main() { tags: [], parameters: [], responses: { - '200': const ApiResponse( + '200': ApiResponse( code: '200', description: '', // Missing response description ), @@ -243,7 +224,6 @@ void main() { }, models: {}, controllers: {}, - security: [], ); final isValid = validator.validateDocument(document); @@ -263,10 +243,8 @@ void main() { title: 'Test API', version: '1.0.0', description: 'Test', - servers: [], - components: ApiComponents(schemas: {}, securitySchemes: {}), paths: { - '/test': const ApiPath( + '/test': ApiPath( path: '/test', method: HttpMethod.get, summary: 'Test', @@ -279,7 +257,6 @@ void main() { }, models: {}, controllers: {}, - security: [], ); final isValid = validator.validateDocument(document); @@ -295,9 +272,8 @@ void main() { version: '1.0.0', description: 'Test API', servers: [ApiServer(url: 'https://api.example.com')], - components: ApiComponents(schemas: {}, securitySchemes: {}), paths: { - '/test': const ApiPath( + '/test': ApiPath( path: '/test', method: HttpMethod.get, summary: 'Test', @@ -306,7 +282,7 @@ void main() { tags: [], // No tags parameters: [], responses: { - '200': const ApiResponse( + '200': ApiResponse( code: '200', description: 'Success', ), @@ -316,7 +292,6 @@ void main() { }, models: {}, controllers: {}, - security: [], ); final isValid = validator.validateDocument(document); @@ -331,7 +306,7 @@ void main() { test('validates large schemas', () { // Create a model with many properties final properties = {}; - for (int i = 0; i < 25; i++) { + for (var i = 0; i < 25; i++) { properties['property$i'] = ApiProperty( name: 'property$i', type: PropertyType.string, @@ -366,7 +341,7 @@ void main() { tags: ['test'], parameters: [], responses: { - '200': const ApiResponse( + '200': ApiResponse( code: '200', description: 'Success', ), @@ -391,12 +366,9 @@ void main() { title: '', version: '', description: '', - servers: [], - components: ApiComponents(schemas: {}, securitySchemes: {}), paths: {}, models: {}, controllers: {}, - security: [], ); validator.validateDocument(document); @@ -414,12 +386,9 @@ void main() { title: '', version: '', description: '', - servers: [], - components: ApiComponents(schemas: {}, securitySchemes: {}), paths: {}, models: {}, controllers: {}, - security: [], ); validator.validateDocument(document); @@ -440,10 +409,8 @@ void main() { title: 'Test API', version: '1.0.0', description: '', // Missing description - servers: [], // Missing servers - components: ApiComponents(schemas: {}, securitySchemes: {}), paths: { - '/test': const ApiPath( + '/test': ApiPath( path: '/test', method: HttpMethod.get, summary: 'Test', @@ -452,7 +419,7 @@ void main() { tags: ['test'], parameters: [], responses: { - '200': const ApiResponse( + '200': ApiResponse( code: '200', description: 'Success', ), @@ -461,7 +428,6 @@ void main() { }, models: {}, controllers: {}, - security: [], ); final isValid = strictValidator.validateDocument(document); diff --git a/tests/integration_test.dart b/tests/integration_test.dart index 9fea53c..9a8ad3c 100644 --- a/tests/integration_test.dart +++ b/tests/integration_test.dart @@ -58,7 +58,7 @@ void main() { 'data': { 'type': 'array', 'items': { - '\$ref': '#/components/schemas/User', + r'$ref': '#/components/schemas/User', }, }, 'total': {'type': 'integer'}, @@ -83,7 +83,7 @@ void main() { 'content': { 'application/json': { 'schema': { - '\$ref': '#/components/schemas/CreateUserRequest', + r'$ref': '#/components/schemas/CreateUserRequest', }, }, }, @@ -94,7 +94,7 @@ void main() { 'content': { 'application/json': { 'schema': { - '\$ref': '#/components/schemas/User', + r'$ref': '#/components/schemas/User', }, }, }, @@ -125,7 +125,7 @@ void main() { 'content': { 'application/json': { 'schema': { - '\$ref': '#/components/schemas/User', + r'$ref': '#/components/schemas/User', }, }, }, @@ -167,7 +167,7 @@ void main() { 'content': { 'application/json': { 'schema': { - '\$ref': '#/components/schemas/FileUploadResult', + r'$ref': '#/components/schemas/FileUploadResult', }, }, }, @@ -275,7 +275,7 @@ void main() { // 3. 验证文档 final validator = EnhancedValidator( - includeWarnings: true, + ); final isValid = validator.validateDocument(document); @@ -285,17 +285,16 @@ void main() { final criticalErrors = errors .where((e) => e.severity == ErrorSeverity.error || - e.severity == ErrorSeverity.critical) + e.severity == ErrorSeverity.critical,) .toList(); expect(criticalErrors, isEmpty, reason: - 'Document should not have critical errors: ${criticalErrors.map((e) => e.title).join(", ")}'); + 'Document should not have critical errors: ${criticalErrors.map((e) => e.title).join(", ")}',); // 4. 生成 Retrofit API 代码 final retrofitGenerator = RetrofitApiGenerator( className: 'IntegrationTestApi', - splitByTags: true, ); final retrofitCode = retrofitGenerator.generateFromDocument(document); @@ -303,27 +302,27 @@ void main() { // 验证生成的代码 expect(retrofitCode, isNotEmpty); expect(retrofitCode, contains('IntegrationTestApi')); - expect(retrofitCode, contains('@GET(\'/users\')')); - expect(retrofitCode, contains('@POST(\'/users\')')); - expect(retrofitCode, contains('@GET(\'/users/{id}\')')); - expect(retrofitCode, contains('@POST(\'/files/upload\')')); - expect(retrofitCode, contains('@Path(\'id\')')); - expect(retrofitCode, contains('@Query(\'page\')')); + expect(retrofitCode, contains("@GET('/users')")); + expect(retrofitCode, contains("@POST('/users')")); + expect(retrofitCode, contains("@GET('/users/{id}')")); + expect(retrofitCode, contains("@POST('/files/upload')")); + expect(retrofitCode, contains("@Path('id')")); + expect(retrofitCode, contains("@Query('page')")); expect(retrofitCode, contains('@MultiPart()')); // 5. 性能验证 print('Integration Test Performance Summary:'); print(' Parse Time: ${parseStats.totalTime.inMilliseconds}ms'); print( - ' Document Size: ${(jsonString.length / 1024).toStringAsFixed(2)}KB'); + ' Document Size: ${(jsonString.length / 1024).toStringAsFixed(2)}KB',); print(' Paths Parsed: ${parseStats.pathCount}'); print(' Schemas Parsed: ${parseStats.schemaCount}'); print( - ' Retrofit Code Size: ${(retrofitCode.length / 1024).toStringAsFixed(2)}KB'); + ' Retrofit Code Size: ${(retrofitCode.length / 1024).toStringAsFixed(2)}KB',); // 验证性能指标 expect( - parseStats.totalTime.inMilliseconds, lessThan(2000)); // 解析应在2秒内完成 + parseStats.totalTime.inMilliseconds, lessThan(2000),); // 解析应在2秒内完成 expect(retrofitCode.length, greaterThan(1000)); // 应生成足够的代码 }); @@ -331,20 +330,19 @@ void main() { final file = File('swagger.json'); if (!file.existsSync()) { print( - 'swagger.json not found, skipping real project integration test'); + 'swagger.json not found, skipping real project integration test',); return; } final jsonString = await file.readAsString(); print( - 'Real project swagger.json size: ${(jsonString.length / 1024).toStringAsFixed(2)}KB'); + 'Real project swagger.json size: ${(jsonString.length / 1024).toStringAsFixed(2)}KB',); // 解析 final parser = PerformanceParser( config: const ParseConfig( enablePerformanceStats: true, enableParallelParsing: false, // 禁用并行解析 - maxConcurrency: 4, ), ); @@ -378,7 +376,6 @@ void main() { // 生成代码(使用 RetrofitApiGenerator) final generator = RetrofitApiGenerator( className: 'OAMobileApiService', - splitByTags: true, ); final genStopwatch = Stopwatch()..start(); @@ -391,7 +388,7 @@ void main() { print('Code generation results:'); print(' Generation Time: ${genStopwatch.elapsedMilliseconds}ms'); print( - ' Generated Code Size: ${(generatedCode.length / 1024).toStringAsFixed(2)}KB'); + ' Generated Code Size: ${(generatedCode.length / 1024).toStringAsFixed(2)}KB',); print(' Generated Lines: ${generatedCode.split('\n').length}'); // 性能要求 @@ -409,7 +406,7 @@ void main() { final parser = PerformanceParser(); expect(() => parser.parseDocument(malformedJson), - throwsA(isA())); + throwsA(isA()),); }); test('handles invalid OpenAPI document', () async { @@ -426,7 +423,7 @@ void main() { final parser = PerformanceParser(); expect(() => parser.parseDocument(jsonString), - throwsA(isA())); + throwsA(isA()),); }); test('validation catches common errors', () async { @@ -471,7 +468,7 @@ void main() { final paths = {}; final schemas = {}; - for (int i = 0; i < 200; i++) { + for (var i = 0; i < 200; i++) { paths['/resource$i'] = { 'get': { 'summary': 'Get resource $i', @@ -483,7 +480,7 @@ void main() { 'content': { 'application/json': { 'schema': { - '\$ref': '#/components/schemas/Resource$i', + r'$ref': '#/components/schemas/Resource$i', }, }, }, @@ -517,14 +514,13 @@ void main() { final jsonString = jsonEncode(largeDoc); print( - 'Large document size: ${(jsonString.length / 1024).toStringAsFixed(2)}KB'); + 'Large document size: ${(jsonString.length / 1024).toStringAsFixed(2)}KB',); // 测试解析性能 final parser = PerformanceParser( config: const ParseConfig( enablePerformanceStats: true, enableParallelParsing: false, // 禁用并行解析 - maxConcurrency: 4, ), ); @@ -538,7 +534,7 @@ void main() { // 测试生成性能(使用 RetrofitApiGenerator) final generator = RetrofitApiGenerator( - splitByTags: true, + ); final genStopwatch = Stopwatch()..start(); @@ -554,7 +550,7 @@ void main() { print(' Paths: ${document.paths.length}'); print(' Models: ${document.models.length}'); print( - ' Generated Code: ${(generatedCode.length / 1024).toStringAsFixed(2)}KB'); + ' Generated Code: ${(generatedCode.length / 1024).toStringAsFixed(2)}KB',); }); }); }); diff --git a/tests/media_type_test.dart b/tests/media_type_test.dart index 3fbdc1a..1dd2222 100644 --- a/tests/media_type_test.dart +++ b/tests/media_type_test.dart @@ -8,12 +8,12 @@ void main() { expect(MediaType.xml.value, 'application/xml'); expect(MediaType.multipartFormData.value, 'multipart/form-data'); expect( - MediaType.formUrlEncoded.value, 'application/x-www-form-urlencoded'); + MediaType.formUrlEncoded.value, 'application/x-www-form-urlencoded',); expect(MediaType.textPlain.value, 'text/plain'); expect(MediaType.textHtml.value, 'text/html'); expect(MediaType.textCsv.value, 'text/csv'); expect( - MediaType.applicationOctetStream.value, 'application/octet-stream'); + MediaType.applicationOctetStream.value, 'application/octet-stream',); expect(MediaType.applicationPdf.value, 'application/pdf'); expect(MediaType.imagePng.value, 'image/png'); expect(MediaType.imageJpeg.value, 'image/jpeg'); @@ -28,22 +28,22 @@ void main() { expect(MediaTypeExtension.fromString('application/xml'), MediaType.xml); expect(MediaTypeExtension.fromString('text/xml'), MediaType.xml); expect(MediaTypeExtension.fromString('multipart/form-data'), - MediaType.multipartFormData); + MediaType.multipartFormData,); expect(MediaTypeExtension.fromString('application/x-www-form-urlencoded'), - MediaType.formUrlEncoded); + MediaType.formUrlEncoded,); expect(MediaTypeExtension.fromString('text/plain'), MediaType.textPlain); expect(MediaTypeExtension.fromString('text/html'), MediaType.textHtml); expect(MediaTypeExtension.fromString('text/csv'), MediaType.textCsv); expect(MediaTypeExtension.fromString('application/octet-stream'), - MediaType.applicationOctetStream); + MediaType.applicationOctetStream,); expect(MediaTypeExtension.fromString('application/pdf'), - MediaType.applicationPdf); + MediaType.applicationPdf,); expect(MediaTypeExtension.fromString('image/png'), MediaType.imagePng); expect(MediaTypeExtension.fromString('image/jpeg'), MediaType.imageJpeg); expect(MediaTypeExtension.fromString('image/jpg'), MediaType.imageJpeg); expect(MediaTypeExtension.fromString('image/gif'), MediaType.imageGif); expect( - MediaTypeExtension.fromString('image/svg+xml'), MediaType.imageSvg); + MediaTypeExtension.fromString('image/svg+xml'), MediaType.imageSvg,); expect(MediaTypeExtension.fromString('audio/mpeg'), MediaType.audioMp3); expect(MediaTypeExtension.fromString('audio/mp3'), MediaType.audioMp3); expect(MediaTypeExtension.fromString('video/mp4'), MediaType.videoMp4); diff --git a/tests/models_test.dart b/tests/models_test.dart index 64bb797..574a41d 100644 --- a/tests/models_test.dart +++ b/tests/models_test.dart @@ -13,7 +13,6 @@ void main() { tags: ['User'], parameters: [], responses: {}, - requestBody: null, ); expect(path.path, '/api/users'); @@ -195,7 +194,7 @@ void main() { 'links': { 'GetUserByName': { 'operationId': 'getUserByName', - 'parameters': {'username': '\$response.body#/username'}, + 'parameters': {'username': r'$response.body#/username'}, }, }, }; @@ -248,7 +247,7 @@ void main() { expect(requestBody.content.length, 1); expect( - requestBody.content['application/json']?.schema?['type'], 'object'); + requestBody.content['application/json']?.schema?['type'], 'object',); expect(requestBody.supportedMediaTypes, contains('application/json')); expect(requestBody.supportsMediaType('application/json'), true); }); @@ -433,7 +432,7 @@ void main() { final json = { 'type': 'object', 'description': 'User object', - '\$ref': '#/components/schemas/User', + r'$ref': '#/components/schemas/User', }; final property = ApiProperty.fromJson('user', json, []); @@ -448,7 +447,7 @@ void main() { 'description': 'User list', 'items': { 'type': 'object', - '\$ref': '#/components/schemas/User', + r'$ref': '#/components/schemas/User', }, }; @@ -633,7 +632,7 @@ void main() { expect(document.components.schemas['User']?.name, 'User'); expect(document.components.responses.length, 1); expect(document.components.responses['NotFound']?.description, - 'Resource not found'); + 'Resource not found',); }); test('creates SwaggerDocument with composition schemas', () { @@ -654,7 +653,7 @@ void main() { }, 'Dog': { 'allOf': [ - {'\$ref': '#/components/schemas/Pet'}, + {r'$ref': '#/components/schemas/Pet'}, { 'type': 'object', 'properties': { @@ -665,7 +664,7 @@ void main() { }, 'Animal': { 'oneOf': [ - {'\$ref': '#/components/schemas/Pet'}, + {r'$ref': '#/components/schemas/Pet'}, { 'type': 'object', 'properties': { @@ -743,7 +742,6 @@ void main() { tags: ['User'], parameters: [], responses: {}, - requestBody: null, ), const ApiPath( path: '/api/users/{id}', @@ -754,7 +752,6 @@ void main() { tags: ['User'], parameters: [], responses: {}, - requestBody: null, ), ]; @@ -780,7 +777,6 @@ void main() { tags: ['User'], parameters: [], responses: {}, - requestBody: null, ), ]; @@ -812,7 +808,7 @@ void main() { '200': { 'description': 'Success', 'schema': {'type': 'object'}, - } + }, }, 'requestBody': { 'description': 'User data', @@ -826,7 +822,7 @@ void main() { 'deprecated': true, }; - final path = ApiPath.fromJson('/api/users/{id}', 'PUT', json); + final path = ApiPath.fromJson('/api/users/{id}', HttpMethod.put, json); expect(path.path, '/api/users/{id}'); expect(path.method, HttpMethod.put); @@ -843,7 +839,7 @@ void main() { test('creates ApiPath from JSON with minimal fields', () { final json = {}; - final path = ApiPath.fromJson('/api/users', 'GET', json); + final path = ApiPath.fromJson('/api/users', HttpMethod.get, json); expect(path.path, '/api/users'); expect(path.method, HttpMethod.get); @@ -952,7 +948,7 @@ void main() { 'description': 'User list', 'items': { 'type': 'object', - '\$ref': '#/components/schemas/User', + r'$ref': '#/components/schemas/User', }, }; @@ -1057,7 +1053,7 @@ void main() { expect(ParameterLocation.fromString('unknown'), ParameterLocation.query); expect(ParameterLocation.fromString(''), ParameterLocation.query); expect(ParameterLocation.fromString('CUSTOM_LOCATION'), - ParameterLocation.query); + ParameterLocation.query,); }); test('HttpMethod fromString handles unknown methods', () { @@ -1121,7 +1117,7 @@ void main() { test('creates ApiSchema from JSON with allOf', () { final json = { 'allOf': [ - {'\$ref': '#/components/schemas/Pet'}, + {r'$ref': '#/components/schemas/Pet'}, { 'type': 'object', 'properties': { @@ -1146,8 +1142,8 @@ void main() { test('creates ApiSchema from JSON with oneOf', () { final json = { 'oneOf': [ - {'\$ref': '#/components/schemas/Cat'}, - {'\$ref': '#/components/schemas/Dog'}, + {r'$ref': '#/components/schemas/Cat'}, + {r'$ref': '#/components/schemas/Dog'}, ], }; @@ -1165,7 +1161,7 @@ void main() { 'type': 'string', 'minLength': 1, 'maxLength': 100, - 'pattern': '^[a-zA-Z]+\$', + 'pattern': r'^[a-zA-Z]+$', 'example': 'example', 'nullable': true, }; @@ -1175,7 +1171,7 @@ void main() { expect(schema.type, 'string'); expect(schema.minLength, 1); expect(schema.maxLength, 100); - expect(schema.pattern, '^[a-zA-Z]+\$'); + expect(schema.pattern, r'^[a-zA-Z]+$'); expect(schema.example, 'example'); expect(schema.nullable, true); }); @@ -1183,8 +1179,8 @@ void main() { test('creates ApiSchema from JSON with discriminator', () { final json = { 'oneOf': [ - {'\$ref': '#/components/schemas/Cat'}, - {'\$ref': '#/components/schemas/Dog'}, + {r'$ref': '#/components/schemas/Cat'}, + {r'$ref': '#/components/schemas/Dog'}, ], 'discriminator': { 'propertyName': 'petType', @@ -1204,9 +1200,9 @@ void main() { expect(schema.discriminator?.hasMapping, true); expect(schema.discriminator?.mapping.length, 2); expect(schema.discriminator?.getSchemaForValue('cat'), - '#/components/schemas/Cat'); + '#/components/schemas/Cat',); expect(schema.discriminator?.getSchemaForValue('dog'), - '#/components/schemas/Dog'); + '#/components/schemas/Dog',); }); }); @@ -1234,9 +1230,9 @@ void main() { expect(discriminator.mapping.length, 2); expect(discriminator.hasMapping, true); expect( - discriminator.getSchemaForValue('cat'), '#/components/schemas/Cat'); + discriminator.getSchemaForValue('cat'), '#/components/schemas/Cat',); expect( - discriminator.getSchemaForValue('dog'), '#/components/schemas/Dog'); + discriminator.getSchemaForValue('dog'), '#/components/schemas/Dog',); expect(discriminator.getSchemaForValue('bird'), isNull); }); @@ -1255,9 +1251,9 @@ void main() { expect(discriminator.mapping.length, 2); expect(discriminator.hasMapping, true); expect( - discriminator.getSchemaForValue('user'), '#/components/schemas/User'); + discriminator.getSchemaForValue('user'), '#/components/schemas/User',); expect(discriminator.getSchemaForValue('admin'), - '#/components/schemas/Admin'); + '#/components/schemas/Admin',); }); test('creates ApiDiscriminator from JSON with minimal fields', () { @@ -1313,15 +1309,15 @@ void main() { final addressProperty = property.nestedProperties['address']!; expect(addressProperty.nestedProperties.length, 3); expect(addressProperty.nestedProperties['coordinates']?.type, - PropertyType.object); + PropertyType.object,); final coordinatesProperty = addressProperty.nestedProperties['coordinates']!; expect(coordinatesProperty.nestedProperties.length, 2); expect(coordinatesProperty.nestedProperties['lat']?.type, - PropertyType.number); + PropertyType.number,); expect(coordinatesProperty.nestedProperties['lng']?.type, - PropertyType.number); + PropertyType.number,); }); test('creates ApiProperty with array of nested objects', () { @@ -1438,7 +1434,7 @@ void main() { }, 'additionalProperties': { 'type': 'string', - 'pattern': '^[a-zA-Z]+\$', + 'pattern': r'^[a-zA-Z]+$', }, }; @@ -1448,7 +1444,7 @@ void main() { expect(schema.allowsAdditionalProperties, true); expect(schema.additionalPropertiesSchema, isNotNull); expect(schema.additionalPropertiesSchema?.type, 'string'); - expect(schema.additionalPropertiesSchema?.pattern, '^[a-zA-Z]+\$'); + expect(schema.additionalPropertiesSchema?.pattern, r'^[a-zA-Z]+$'); }); test('creates ApiSchema with patternProperties', () { @@ -1472,14 +1468,14 @@ void main() { final json = { 'type': 'object', 'propertyNames': { - 'pattern': '^[A-Za-z_][A-Za-z0-9_]*\$', + 'pattern': r'^[A-Za-z_][A-Za-z0-9_]*$', }, }; final schema = ApiSchema.fromJson(json); expect(schema.hasPropertyNames, true); - expect(schema.propertyNames?.pattern, '^[A-Za-z_][A-Za-z0-9_]*\$'); + expect(schema.propertyNames?.pattern, r'^[A-Za-z_][A-Za-z0-9_]*$'); }); test('creates ApiSchema with dependencies', () { @@ -1567,7 +1563,7 @@ void main() { '^prefix_': {'type': 'integer'}, }, 'propertyNames': { - 'pattern': '^[a-z]+\$', + 'pattern': r'^[a-z]+$', }, 'dependencies': { 'name': ['id'], diff --git a/tests/reference_resolver_test.dart b/tests/reference_resolver_test.dart index 2d02098..0b599f7 100644 --- a/tests/reference_resolver_test.dart +++ b/tests/reference_resolver_test.dart @@ -66,7 +66,7 @@ void main() { }, 'User': { 'allOf': [ - {'\$ref': '#/components/schemas/BaseEntity'}, + {r'$ref': '#/components/schemas/BaseEntity'}, { 'type': 'object', 'properties': { @@ -89,7 +89,7 @@ void main() { // 检查合并的属性 expect( - models['User']!.properties.length, 4); // id, createdAt, name, email + models['User']!.properties.length, 4,); // id, createdAt, name, email expect(models['User']!.properties['id'], isNotNull); expect(models['User']!.properties['name'], isNotNull); expect(models['User']!.properties['email'], isNotNull); @@ -103,10 +103,10 @@ void main() { 'type': 'object', 'properties': { 'id': {'type': 'integer'}, - 'parent': {'\$ref': '#/components/schemas/Node'}, + 'parent': {r'$ref': '#/components/schemas/Node'}, 'children': { 'type': 'array', - 'items': {'\$ref': '#/components/schemas/Node'}, + 'items': {r'$ref': '#/components/schemas/Node'}, }, }, 'required': ['id'], @@ -152,7 +152,7 @@ void main() { 'name': {'type': 'string'}, 'addresses': { 'type': 'array', - 'items': {'\$ref': '#/components/schemas/Address'}, + 'items': {r'$ref': '#/components/schemas/Address'}, }, }, 'required': ['id', 'name'], @@ -211,8 +211,8 @@ void main() { 'schemas': { 'Pet': { 'oneOf': [ - {'\$ref': '#/components/schemas/Cat'}, - {'\$ref': '#/components/schemas/Dog'}, + {r'$ref': '#/components/schemas/Cat'}, + {r'$ref': '#/components/schemas/Dog'}, ], 'discriminator': { 'propertyName': 'petType', diff --git a/tests/schema_validator_test.dart b/tests/schema_validator_test.dart deleted file mode 100644 index 8b13789..0000000 --- a/tests/schema_validator_test.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/security_test.dart b/tests/security_test.dart index ddf4ffc..76ef1a4 100644 --- a/tests/security_test.dart +++ b/tests/security_test.dart @@ -79,10 +79,10 @@ void main() { expect(flows.hasAnyFlow, true); expect(flows.availableFlows.length, 3); expect(flows.availableFlows.contains(OAuth2FlowType.authorizationCode), - true); + true,); expect(flows.availableFlows.contains(OAuth2FlowType.implicit), true); expect(flows.availableFlows.contains(OAuth2FlowType.clientCredentials), - true); + true,); expect(flows.availableFlows.contains(OAuth2FlowType.password), false); expect(flows.authorizationCode, isNotNull); @@ -206,7 +206,7 @@ void main() { expect(scheme.type, SecuritySchemeType.openIdConnect); expect(scheme.description, 'OpenID Connect authentication'); expect(scheme.openIdConnectUrl, - 'https://example.com/.well-known/openid_configuration'); + 'https://example.com/.well-known/openid_configuration',); expect(scheme.isOpenIdConnect, true); }); }); @@ -251,15 +251,15 @@ void main() { test('converts string to security scheme type', () { expect(SecuritySchemeTypeExtension.fromString('apiKey'), - SecuritySchemeType.apiKey); + SecuritySchemeType.apiKey,); expect(SecuritySchemeTypeExtension.fromString('http'), - SecuritySchemeType.http); + SecuritySchemeType.http,); expect(SecuritySchemeTypeExtension.fromString('oauth2'), - SecuritySchemeType.oauth2); + SecuritySchemeType.oauth2,); expect(SecuritySchemeTypeExtension.fromString('openIdConnect'), - SecuritySchemeType.openIdConnect); + SecuritySchemeType.openIdConnect,); expect(SecuritySchemeTypeExtension.fromString('unknown'), - SecuritySchemeType.apiKey); + SecuritySchemeType.apiKey,); }); test('converts API key location to string', () { @@ -271,11 +271,11 @@ void main() { test('converts string to API key location', () { expect(ApiKeyLocationExtension.fromString('query'), ApiKeyLocation.query); expect( - ApiKeyLocationExtension.fromString('header'), ApiKeyLocation.header); + ApiKeyLocationExtension.fromString('header'), ApiKeyLocation.header,); expect( - ApiKeyLocationExtension.fromString('cookie'), ApiKeyLocation.cookie); + ApiKeyLocationExtension.fromString('cookie'), ApiKeyLocation.cookie,); expect( - ApiKeyLocationExtension.fromString('unknown'), ApiKeyLocation.header); + ApiKeyLocationExtension.fromString('unknown'), ApiKeyLocation.header,); }); test('converts OAuth2 flow type to string', () { @@ -287,15 +287,15 @@ void main() { test('converts string to OAuth2 flow type', () { expect(OAuth2FlowTypeExtension.fromString('authorizationCode'), - OAuth2FlowType.authorizationCode); + OAuth2FlowType.authorizationCode,); expect(OAuth2FlowTypeExtension.fromString('implicit'), - OAuth2FlowType.implicit); + OAuth2FlowType.implicit,); expect(OAuth2FlowTypeExtension.fromString('password'), - OAuth2FlowType.password); + OAuth2FlowType.password,); expect(OAuth2FlowTypeExtension.fromString('clientCredentials'), - OAuth2FlowType.clientCredentials); + OAuth2FlowType.clientCredentials,); expect(OAuth2FlowTypeExtension.fromString('unknown'), - OAuth2FlowType.authorizationCode); + OAuth2FlowType.authorizationCode,); }); }); diff --git a/tests/simple_generator_test.dart b/tests/simple_generator_test.dart index 05100aa..ea50f53 100644 --- a/tests/simple_generator_test.dart +++ b/tests/simple_generator_test.dart @@ -1,4 +1,5 @@ import 'package:swagger_generator_flutter/core/models.dart'; +import 'package:swagger_generator_flutter/generators/model_code_generator.dart'; import 'package:swagger_generator_flutter/generators/retrofit_api_generator.dart'; import 'package:test/test.dart'; @@ -7,7 +8,7 @@ void main() { late SwaggerDocument simpleDocument; setUp(() { - simpleDocument = SwaggerDocument( + simpleDocument = const SwaggerDocument( title: 'Simple Test API', version: '1.0.0', description: 'A simple test API', @@ -17,10 +18,6 @@ void main() { description: 'Test server', ), ], - components: ApiComponents( - schemas: {}, - securitySchemes: {}, - ), paths: { '/users': ApiPath( path: '/users', @@ -93,7 +90,6 @@ void main() { ), }, controllers: {}, - security: [], ); }); @@ -122,7 +118,7 @@ void main() { }); test('generates path annotations', () { - final generator = RetrofitApiGenerator(); + final generator = RetrofitApiGenerator(splitByTags: false); final result = generator.generateFromDocument(simpleDocument); @@ -132,7 +128,7 @@ void main() { }); test('generates parameter annotations', () { - final generator = RetrofitApiGenerator(); + final generator = RetrofitApiGenerator(splitByTags: false); final result = generator.generateFromDocument(simpleDocument); @@ -142,7 +138,7 @@ void main() { test('handles split by tags', () { final generator = RetrofitApiGenerator( - splitByTags: true, + ); final result = generator.generateFromDocument(simpleDocument); @@ -154,7 +150,7 @@ void main() { group('Code Quality Tests', () { test('generated code has proper structure', () { - final generator = RetrofitApiGenerator(); + final generator = RetrofitApiGenerator(splitByTags: false); final result = generator.generateFromDocument(simpleDocument); // Check for basic Dart syntax @@ -168,32 +164,26 @@ void main() { }); test('handles empty paths gracefully', () { - final emptyDocument = SwaggerDocument( + const emptyDocument = SwaggerDocument( title: 'Empty API', version: '1.0.0', description: 'Empty API', - servers: [], - components: ApiComponents(schemas: {}, securitySchemes: {}), paths: {}, models: {}, controllers: {}, - security: [], ); - final generator = RetrofitApiGenerator(); + final generator = RetrofitApiGenerator(splitByTags: false); final result = generator.generateFromDocument(emptyDocument); expect(result, isNotEmpty); - expect(result, contains('Empty API')); }); test('handles special characters in API names', () { - final specialDocument = SwaggerDocument( + const specialDocument = SwaggerDocument( title: 'API-with_Special.Characters', version: '1.0.0', description: 'Test', - servers: [], - components: ApiComponents(schemas: {}, securitySchemes: {}), paths: { '/special-endpoint': ApiPath( path: '/special-endpoint', @@ -213,21 +203,18 @@ void main() { }, models: {}, controllers: {}, - security: [], ); - final generator = RetrofitApiGenerator(); + final generator = RetrofitApiGenerator(splitByTags: false); expect(() => generator.generateFromDocument(specialDocument), - returnsNormally); + returnsNormally,); }); test('handles nullable parameters correctly', () { - final documentWithOptionalParams = SwaggerDocument( + const documentWithOptionalParams = SwaggerDocument( title: 'Test API', version: '1.0.0', description: 'Test', - servers: [], - components: ApiComponents(schemas: {}, securitySchemes: {}), paths: { '/search': ApiPath( path: '/search', @@ -262,10 +249,9 @@ void main() { }, models: {}, controllers: {}, - security: [], ); - final generator = RetrofitApiGenerator(); + final generator = RetrofitApiGenerator(splitByTags: false); final result = generator.generateFromDocument(documentWithOptionalParams); @@ -281,7 +267,7 @@ void main() { test('handles medium-sized documents efficiently', () { // Create a document with multiple paths final paths = {}; - for (int i = 0; i < 50; i++) { + for (var i = 0; i < 50; i++) { paths['/resource$i'] = ApiPath( path: '/resource$i', method: HttpMethod.get, @@ -291,7 +277,7 @@ void main() { tags: ['resources'], parameters: [], responses: { - '200': ApiResponse( + '200': const ApiResponse( code: '200', description: 'Success', ), @@ -304,14 +290,13 @@ void main() { version: '1.0.0', description: 'Large API', servers: [], - components: ApiComponents(schemas: {}, securitySchemes: {}), paths: paths, models: {}, controllers: {}, security: [], ); - final generator = RetrofitApiGenerator(); + final generator = RetrofitApiGenerator(splitByTags: false); final stopwatch = Stopwatch()..start(); final result = generator.generateFromDocument(largeDocument); @@ -320,9 +305,9 @@ void main() { expect(result, isNotEmpty); expect(stopwatch.elapsedMilliseconds, - lessThan(5000)); // Should complete within 5 seconds + lessThan(5000),); // Should complete within 5 seconds expect(result.length, - greaterThan(1000)); // Should generate substantial code + greaterThan(1000),); // Should generate substantial code }); test('memory usage is reasonable', () { @@ -331,7 +316,22 @@ void main() { // Basic memory usage check - result should not be excessively large expect(result.length, - lessThan(100000)); // Less than 100KB for simple document + lessThan(100000),); // Less than 100KB for simple document + }); + }); + + group('ModelCodeGenerator Tests', () { + test('generates Freezed model structure', () { + final generator = ModelCodeGenerator(simpleDocument); + final result = + generator.generateSingleModelFile(simpleDocument.models['User']!); + + expect(result, contains('@freezed')); + expect(result, contains("part 'user.freezed.dart';")); + expect(result, contains("part 'user.g.dart';")); + expect(result, contains('const factory User({')); + expect(result, + contains('factory User.fromJson(Map json)'),); }); }); }); diff --git a/tests/string_utils_test.dart b/tests/string_utils_test.dart index 18994b0..47af035 100644 --- a/tests/string_utils_test.dart +++ b/tests/string_utils_test.dart @@ -33,17 +33,17 @@ void main() { test('converts PascalCase to camelCase', () { expect(StringUtils.toCamelCase('GetClassesTaskChecklistUsers'), - 'getClassesTaskChecklistUsers'); + 'getClassesTaskChecklistUsers',); expect(StringUtils.toCamelCase('GetUserInfo'), 'getUserInfo'); expect(StringUtils.toCamelCase('CreateTask'), 'createTask'); expect( - StringUtils.toCamelCase('UpdateUserProfile'), 'updateUserProfile'); + StringUtils.toCamelCase('UpdateUserProfile'), 'updateUserProfile',); expect(StringUtils.toCamelCase('DeleteTaskById'), 'deleteTaskById'); }); test('preserves existing camelCase', () { expect(StringUtils.toCamelCase('getClassesTaskChecklistUsers'), - 'getClassesTaskChecklistUsers'); + 'getClassesTaskChecklistUsers',); expect(StringUtils.toCamelCase('getUserInfo'), 'getUserInfo'); expect(StringUtils.toCamelCase('createTask'), 'createTask'); }); @@ -146,7 +146,7 @@ void main() { test('generates valid enum names from strings', () { expect(StringUtils.generateEnumValueName('active', 0), 'active'); expect( - StringUtils.generateEnumValueName('user_status', 1), 'userStatus'); + StringUtils.generateEnumValueName('user_status', 1), 'userStatus',); }); test('handles invalid strings', () { @@ -163,14 +163,14 @@ void main() { group('cleanDescription', () { test('cleans basic descriptions', () { expect(StringUtils.cleanDescription(' test description '), - 'test description'); + 'test description',); expect(StringUtils.cleanDescription('line1\nline2'), 'line1 line2'); }); test('removes special characters', () { - expect(StringUtils.cleanDescription('test@#\$%'), 'test'); + expect(StringUtils.cleanDescription(r'test@#$%'), 'test'); expect(StringUtils.cleanDescription('test[description]'), - 'testdescription'); + 'testdescription',); }); test('truncates long descriptions', () { @@ -211,7 +211,7 @@ void main() { group('formatDuration', () { test('formats duration correctly', () { expect(StringUtils.formatDuration(const Duration(milliseconds: 500)), - '500毫秒'); + '500毫秒',); expect(StringUtils.formatDuration(const Duration(seconds: 1)), '1.00秒'); expect(StringUtils.formatDuration(const Duration(seconds: 2)), '2.00秒'); }); diff --git a/tests/test_function_name.dart b/tests/test_function_name.dart index ce58b3f..fcdc407 100644 --- a/tests/test_function_name.dart +++ b/tests/test_function_name.dart @@ -1,9 +1,9 @@ -import '../lib/utils/string_utils.dart'; +import 'package:swagger_generator_flutter/utils/string_utils.dart'; void main() { print('Testing function name generation:'); print( - 'GetClassesTaskChecklistUsers -> ${StringUtils.toCamelCase('GetClassesTaskChecklistUsers')}'); + 'GetClassesTaskChecklistUsers -> ${StringUtils.toCamelCase('GetClassesTaskChecklistUsers')}',); print('GetUserInfo -> ${StringUtils.toCamelCase('GetUserInfo')}'); print('CreateTask -> ${StringUtils.toCamelCase('CreateTask')}'); print('UpdateUserProfile -> ${StringUtils.toCamelCase('UpdateUserProfile')}'); @@ -11,11 +11,11 @@ void main() { print('\nTesting existing camelCase:'); print( - 'getClassesTaskChecklistUsers -> ${StringUtils.toCamelCase('getClassesTaskChecklistUsers')}'); + 'getClassesTaskChecklistUsers -> ${StringUtils.toCamelCase('getClassesTaskChecklistUsers')}',); print('getUserInfo -> ${StringUtils.toCamelCase('getUserInfo')}'); print('\nTesting snake_case:'); print( - 'get_classes_task_checklist_users -> ${StringUtils.toCamelCase('get_classes_task_checklist_users')}'); + 'get_classes_task_checklist_users -> ${StringUtils.toCamelCase('get_classes_task_checklist_users')}',); print('get_user_info -> ${StringUtils.toCamelCase('get_user_info')}'); } diff --git a/tests/test_property_name.dart b/tests/test_property_name.dart index bff7540..10eed99 100644 --- a/tests/test_property_name.dart +++ b/tests/test_property_name.dart @@ -1,4 +1,4 @@ -import '../lib/utils/string_utils.dart'; +import 'package:swagger_generator_flutter/utils/string_utils.dart'; void main() { print('Testing property name conversion:'); @@ -6,13 +6,13 @@ void main() { print('meetingTitle -> ${StringUtils.toDartPropertyName('meetingTitle')}'); print('taskInfo -> ${StringUtils.toDartPropertyName('taskInfo')}'); print( - 'sunTaskUserResults -> ${StringUtils.toDartPropertyName('sunTaskUserResults')}'); + 'sunTaskUserResults -> ${StringUtils.toDartPropertyName('sunTaskUserResults')}',); print( - 'sunTaskFileResults -> ${StringUtils.toDartPropertyName('sunTaskFileResults')}'); + 'sunTaskFileResults -> ${StringUtils.toDartPropertyName('sunTaskFileResults')}',); print('\nTesting snake_case conversion:'); print( - 'class_cadre_id -> ${StringUtils.toDartPropertyName('class_cadre_id')}'); + 'class_cadre_id -> ${StringUtils.toDartPropertyName('class_cadre_id')}',); print('meeting_title -> ${StringUtils.toDartPropertyName('meeting_title')}'); print('task_info -> ${StringUtils.toDartPropertyName('task_info')}'); @@ -21,10 +21,10 @@ void main() { print('ProblemTitle -> ${StringUtils.toDartPropertyName('ProblemTitle')}'); print('ProblemObj -> ${StringUtils.toDartPropertyName('ProblemObj')}'); print( - 'ProblemPhenomenon -> ${StringUtils.toDartPropertyName('ProblemPhenomenon')}'); + 'ProblemPhenomenon -> ${StringUtils.toDartPropertyName('ProblemPhenomenon')}',); print('ClassesId -> ${StringUtils.toDartPropertyName('ClassesId')}'); print( - 'ProblemTaskType -> ${StringUtils.toDartPropertyName('ProblemTaskType')}'); + 'ProblemTaskType -> ${StringUtils.toDartPropertyName('ProblemTaskType')}',); print('PageSize -> ${StringUtils.toDartPropertyName('PageSize')}'); print('\nTesting parameter name conversion:'); @@ -41,20 +41,20 @@ void main() { print('\nTesting tag names:'); print( - 'Follow Manager -> ${StringUtils.toDartPropertyName('Follow Manager')}'); + 'Follow Manager -> ${StringUtils.toDartPropertyName('Follow Manager')}',); print('Health Check -> ${StringUtils.toDartPropertyName('Health Check')}'); print( - 'Mobile Manager -> ${StringUtils.toDartPropertyName('Mobile Manager')}'); + 'Mobile Manager -> ${StringUtils.toDartPropertyName('Mobile Manager')}',); print('My Info -> ${StringUtils.toDartPropertyName('My Info')}'); print( - 'Task Class Cadre Meeting -> ${StringUtils.toDartPropertyName('Task Class Cadre Meeting')}'); + 'Task Class Cadre Meeting -> ${StringUtils.toDartPropertyName('Task Class Cadre Meeting')}',); print( - 'Task Class Meeting -> ${StringUtils.toDartPropertyName('Task Class Meeting')}'); + 'Task Class Meeting -> ${StringUtils.toDartPropertyName('Task Class Meeting')}',); print( - 'Task Coach Sub -> ${StringUtils.toDartPropertyName('Task Coach Sub')}'); + 'Task Coach Sub -> ${StringUtils.toDartPropertyName('Task Coach Sub')}',); print('Task Cultural -> ${StringUtils.toDartPropertyName('Task Cultural')}'); print( - 'Task Data Collect -> ${StringUtils.toDartPropertyName('Task Data Collect')}'); + 'Task Data Collect -> ${StringUtils.toDartPropertyName('Task Data Collect')}',); print('Task Follow -> ${StringUtils.toDartPropertyName('Task Follow')}'); print('Task Info -> ${StringUtils.toDartPropertyName('Task Info')}'); print('Task Meeting -> ${StringUtils.toDartPropertyName('Task Meeting')}'); @@ -62,15 +62,15 @@ void main() { print('Task Solution -> ${StringUtils.toDartPropertyName('Task Solution')}'); print('Task Spot -> ${StringUtils.toDartPropertyName('Task Spot')}'); print( - 'Task Summarize -> ${StringUtils.toDartPropertyName('Task Summarize')}'); + 'Task Summarize -> ${StringUtils.toDartPropertyName('Task Summarize')}',); print('Task Talk -> ${StringUtils.toDartPropertyName('Task Talk')}'); print( - 'Task Teacher Behavior -> ${StringUtils.toDartPropertyName('Task Teacher Behavior')}'); + 'Task Teacher Behavior -> ${StringUtils.toDartPropertyName('Task Teacher Behavior')}',); print( - 'Task Teacher Talk -> ${StringUtils.toDartPropertyName('Task Teacher Talk')}'); + 'Task Teacher Talk -> ${StringUtils.toDartPropertyName('Task Teacher Talk')}',); print('\nTesting comment cleaning:'); print('部长新增工作任务指标(会删除所有管理的班级任务指标)-删除所有管理的学习官的通用任务指标'); print( - 'Cleaned: ${StringUtils.cleanDescription('部长新增工作任务指标(会删除所有管理的班级任务指标)-删除所有管理的学习官的通用任务指标')}'); + 'Cleaned: ${StringUtils.cleanDescription('部长新增工作任务指标(会删除所有管理的班级任务指标)-删除所有管理的学习官的通用任务指标')}',); }