feat: update

This commit is contained in:
Max 2025-11-21 22:27:01 +08:00
parent 69aad6bda1
commit f2e48277ea
52 changed files with 3752 additions and 3591 deletions

159
DEPENDENCY_UPDATE.md Normal file
View File

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

View File

@ -16,9 +16,9 @@
### 🎯 专为 Flutter 优化
- **Dio + Retrofit 集成**:完美适配主流网络架构
- **现代数据模型 (Freezed)**:生成基于 Freezed 的不可变数据模型,自动获得 `copyWith`、`toString`、`==/hashCode` 等功能。
- **类型安全**:生成强类型的 API 接口和模型
- **JSON 序列化**:自动生成 json_serializable 代码
- **String 默认值**:自动为 String 类型字段添加默认值,提升容错性
- **JSON 序列化**:与 `json_serializable` 无缝集成,自动生成序列化代码
- **文件上传支持**:完整的 multipart/form-data 支持
### 🔧 高级特性
@ -56,6 +56,11 @@
1) 在宿主项目 `pubspec.yaml` 添加依赖
```yaml
dependencies:
# Freezed 模型需要
freezed_annotation: ^2.4.1
json_annotation: ^4.8.1
dev_dependencies:
swagger_generator_flutter:
git:
@ -64,6 +69,12 @@ 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`
@ -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/<version>/` 下,模型分类在 `api_models/`
@ -137,7 +151,7 @@ Future<void> main() async {
- `--api` / `-r`: 只生成 Retrofit 风格 API 接口
- `--models` / `-m`: 只生成数据模型
- `--docs` / `-d`: 只生成 API 文档
- `--simple` / `-s`: 使用简洁版模型生成
- `--split-by-tags` / `-t`: 按 tags 分组生成多个 API 文件(默认启用)
- `--output-dir` / `-o`: 指定输出目录默认generator

View File

@ -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
# 关闭强制文档注释 (很多业务开发觉得这条太累赘)
public_member_api_docs: false
# 根据项目特性考虑启用的规则 (可能需要团队讨论)
# - 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
# 可选:如果你不喜欢强制构造函数必须写在最前面,也可以关掉
# sort_constructors_first: false

View File

@ -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<void> 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<void> 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
```
---

View File

@ -42,7 +42,7 @@ SwaggerDataParser下载/解析+缓存+性能监测)
- **SchemaValidator / EnhancedValidator**: 基础与增强校验器,用于在生成前验证文档一致性
#### 4. 生成器 (Generators)
- **ModelCodeGenerator**: 按 request/result/enums/parameters 分类生成模型,分页模型自动替换为 `BasePageResult<T>`,响应模型补全字符串/列表默认值
- **ModelCodeGenerator**: 生成基于 Freezed 的不可变数据模型,自动获得 `copyWith`、`toString`、`==/hashCode` 等功能,并与 `json_serializable` 无缝集成。
- **RetrofitApiGenerator**: 支持按 tag 拆分、多版本目录与统一 ApiClient自动生成查询参数实体并处理版本化类名
- **DocumentationGenerator**: 输出 Markdown API 文档与统计摘要

View File

@ -22,25 +22,16 @@ flutter pub get
### 基础使用
#### 命令行方式 (推荐新手)
在项目根目录下(`generator_config.yaml` 所在目录)运行以下命令:
```bash
# 克隆或下载项目
git clone <repository-url>
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
```
#### 编程方式 (推荐进阶用户)
@ -90,20 +81,16 @@ 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

View File

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

View File

@ -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"

View File

@ -19,7 +19,8 @@ class BasePageResult<T> extends Object {
factory BasePageResult.fromJson(
Map<String, dynamic> json,
T Function(dynamic json) fromJsonT,
) => _$BasePageResultFromJson(json, fromJsonT);
) =>
_$BasePageResultFromJson(json, fromJsonT);
Map<String, dynamic> toJson(Object Function(T value) toJsonT) =>
_$BasePageResultToJson(this, toJsonT);

View File

@ -43,7 +43,8 @@ class BaseResult<T> extends BaseContainsParametersAbstract {
factory BaseResult.fromJson(
Map<String, dynamic> json,
T Function(dynamic json) fromJsonT,
) => _$BaseResultFromJson(json, fromJsonT);
) =>
_$BaseResultFromJson(json, fromJsonT);
@override
Map<String, dynamic> toJson(Object Function(T value) toJsonT) =>

View File

@ -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"

View File

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

130
fix_cascades.py Normal file
View File

@ -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 <file_or_directory>')
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()

View File

@ -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<String> args) {
final result = ParsedArguments();
final argQueue = List<String>.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)' : ''}';

View File

@ -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 = <String, Map<String, String>>{};
@ -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<bool>('api') ?? false)
: (args.getOption<bool>('all') ?? true),
useSimpleModels: args.getOption<bool>('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<String, Map<String, String>> versionedFiles) {
Map<String, Map<String, String>> 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<void> _generateVersionIndexFile(
String versionDir, List<String> fileNames) async {
String versionDir,
List<String> 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 = <String, ApiController>{};
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<String> usedModelNames) {
// schema
void extractModelsFromSchema(Map<String, dynamic> 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<String, dynamic>);
} else if (type == 'object' && schema.containsKey('properties')) {
final properties = schema['properties'] as Map<String, dynamic>;
for (final propSchema in properties.values) {
extractModelsFromSchema(propSchema);
extractModelsFromSchema(propSchema as Map<String, dynamic>);
}
}
}
@ -1000,7 +993,7 @@ class GenerateCommand extends BaseCommand {
if (schema.containsKey(key)) {
final subSchemas = schema[key] as List<dynamic>;
for (final subSchema in subSchemas) {
extractModelsFromSchema(subSchema);
extractModelsFromSchema(subSchema as Map<String, dynamic>);
}
}
}
@ -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<String>? includedTags;
final List<String>? 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<String>? includedTags;
final List<String>? excludedTags;
}

View File

@ -1,4 +1,4 @@
import 'config_loader.dart';
import 'package:swagger_generator_flutter/core/config_loader.dart';
/// Swagger配置管理
/// Swagger相关的配置项

View File

@ -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) {

View File

@ -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<DetailedError> _errors = [];
final Map<String, int> _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) =>
bool get hasErrorsOrCritical => _errors.any(
(e) =>
e.severity == ErrorSeverity.error ||
e.severity == ErrorSeverity.critical);
e.severity == ErrorSeverity.critical,
);
///
void clear() {
@ -360,7 +361,9 @@ class ErrorReporter {
///
void _generateReportByCategory(
StringBuffer buffer, List<DetailedError> errors) {
StringBuffer buffer,
List<DetailedError> errors,
) {
final errorsByCategory = <ErrorCategory, List<DetailedError>>{};
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,7 +405,8 @@ class ErrorReporter {
'by_severity': getErrorStatistics().map((k, v) => MapEntry(k.name, v)),
},
'errors': _errors
.map((error) => {
.map(
(error) => {
'id': error.id,
'title': error.title,
'description': error.description,
@ -415,15 +419,18 @@ class ErrorReporter {
'snippet': error.location.snippet,
},
'suggestions': error.suggestions
.map((suggestion) => {
.map(
(suggestion) => {
'description': suggestion.description,
'code_example': suggestion.codeExample,
'documentation_url': suggestion.documentationUrl,
})
},
)
.toList(),
'related_errors': error.relatedErrors,
'timestamp': error.timestamp.toIso8601String(),
})
},
)
.toList(),
};

View File

@ -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<FixSuggestion> 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<FixSuggestion> 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<String> validFields) {
String fieldPath,
String fieldName,
List<String> 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"',
),
],
);

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -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<String, dynamic> _cache = {};
ParsePerformanceStats? _lastStats;
PerformanceParser({ParseConfig? config})
: _config = config ?? const ParseConfig();
///
ParsePerformanceStats? get lastStats => _lastStats;
@ -165,7 +163,7 @@ class PerformanceParser {
//
final chunks = <String>[];
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<SwaggerDocument> _parseDocumentParallel(
Map<String, dynamic> json) async {
final futures = <Future>[];
Map<String, dynamic> json,
) async {
final futures = <Future<void>>[];
final results = <String, dynamic>{};
//
if (json.containsKey('paths')) {
futures.add(_parsePathsParallel(json['paths'] as Map<String, dynamic>)
.then((paths) => results['paths'] = paths));
futures.add(
_parsePathsParallel(json['paths'] as Map<String, dynamic>)
.then((paths) => results['paths'] = paths),
);
}
if (json.containsKey('components')) {
futures.add(
_parseComponentsParallel(json['components'] as Map<String, dynamic>)
.then((components) => results['components'] = components));
.then((components) => results['components'] = components),
);
}
if (json.containsKey('servers')) {
futures.add(_parseServersParallel(json['servers'] as List<dynamic>)
.then((servers) => results['servers'] = servers));
futures.add(
_parseServersParallel(json['servers'] as List<dynamic>)
.then((servers) => results['servers'] = servers),
);
}
//
@ -229,14 +233,15 @@ class PerformanceParser {
///
Future<Map<String, ApiPath>> _parsePathsParallel(
Map<String, dynamic> pathsJson) async {
Map<String, dynamic> 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<ApiComponents> _parseComponentsParallel(
Map<String, dynamic> componentsJson) async {
final futures = <Future>[];
Map<String, dynamic> componentsJson,
) async {
final futures = <Future<void>>[];
final results = <String, dynamic>{};
if (componentsJson.containsKey('schemas')) {
futures.add(_parseSchemasParallel(
componentsJson['schemas'] as Map<String, dynamic>)
.then((schemas) => results['schemas'] = schemas));
futures.add(
_parseSchemasParallel(
componentsJson['schemas'] as Map<String, dynamic>,
).then((schemas) => results['schemas'] = schemas),
);
}
if (componentsJson.containsKey('securitySchemes')) {
futures.add(_parseSecuritySchemesParallel(
componentsJson['securitySchemes'] as Map<String, dynamic>)
.then((schemes) => results['securitySchemes'] = schemes));
futures.add(
_parseSecuritySchemesParallel(
componentsJson['securitySchemes'] as Map<String, dynamic>,
).then((schemes) => results['securitySchemes'] = schemes),
);
}
await Future.wait(futures);
@ -276,7 +286,8 @@ class PerformanceParser {
///
Future<List<ApiServer>> _parseServersParallel(
List<dynamic> serversJson) async {
List<dynamic> serversJson,
) async {
if (serversJson.length <= _config.maxConcurrency) {
return serversJson
.map((json) => ApiServer.fromJson(json as Map<String, dynamic>))
@ -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<Map<String, ApiPath>> _parsePathChunk(
Map<String, dynamic> pathChunk) async {
Map<String, dynamic> pathChunk,
) async {
return _parsePathsSequential(pathChunk);
}
@ -318,8 +330,11 @@ class PerformanceParser {
pathData.forEach((method, operationData) {
if (operationData is Map<String, dynamic>) {
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<Map<String, ApiModel>> _parseSchemasParallel(
Map<String, dynamic> schemasJson) async {
Map<String, dynamic> 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<Map<String, ApiSecurityScheme>> _parseSecuritySchemesParallel(
Map<String, dynamic> schemesJson) async {
Map<String, dynamic> schemesJson,
) async {
final schemes = <String, ApiSecurityScheme>{};
schemesJson.forEach((name, schemeData) {
@ -373,13 +390,15 @@ class PerformanceParser {
/// Schema
Future<Map<String, ApiModel>> _parseSchemaChunk(
Map<String, dynamic> schemaChunk) async {
Map<String, dynamic> schemaChunk,
) async {
return _parseSchemasSequential(schemaChunk);
}
/// Schemas
Map<String, ApiModel> _parseSchemasSequential(
Map<String, dynamic> schemasJson) {
Map<String, dynamic> schemasJson,
) {
final schemas = <String, ApiModel>{};
schemasJson.forEach((name, schemaData) {
@ -400,12 +419,14 @@ class PerformanceParser {
void _validateBasicStructure(Map<String, dynamic> 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<String, dynamic>?;
@ -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<Map<String, dynamic>> _chunkMap(
Map<String, dynamic> map, int chunkSize) {
Map<String, dynamic> map,
int chunkSize,
) {
final chunks = <Map<String, dynamic>>[];
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 = <String, dynamic>{};
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<List<dynamic>> _chunkList(List<dynamic> list, int chunkSize) {
final chunks = <List<dynamic>>[];
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));
}

View File

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

View File

@ -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\';',
// This method is deprecated and will not be used.
// The generator now uses generateSeparateModelFiles.
throw UnimplementedError(
'Single file model generation is no longer supported.',
);
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);
}
@override
@ -96,106 +63,17 @@ 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)}',
@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.',
);
}
// 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<String, dynamic> json) =>',
);
buffer.writeln(' _\$${className}FromJson(json);');
buffer.writeln('');
// toJson
buffer.writeln(
' Map<String, dynamic> toJson() => _\$${className}ToJson(this);');
buffer.writeln('');
buffer.writeln('}');
return buffer.toString();
}
///
/// ///
String _getModelSubDirectory(ApiModel model) {
@ -259,33 +137,38 @@ class ModelCodeGenerator extends ModelGenerator {
final buffer = StringBuffer();
//
buffer.writeln(generateFileHeader(
buffer.writeln(
generateFileHeader(
'${model.name} 模型定义',
fileName: fileName ?? StringUtils.generateFileName(model.name),
));
buffer.writeln('');
),
);
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';
@ -413,50 +297,27 @@ class ModelCodeGenerator extends ModelGenerator {
}
// 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('');
});
//
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';
// required
final shouldBeRequired = isNormalString || !property.nullable;
final required = shouldBeRequired ? 'required ' : '';
buffer.writeln(' ${required}this.$dartPropName,');
buffer.writeln(' $required$dartType$nullable $dartPropName,');
});
buffer.writeln(' });');
}
buffer.writeln('');
buffer.writeln(' }) = _$className;');
buffer.writeln();
// fromJson
buffer.writeln(
' factory $className.fromJson(Map<String, dynamic> json) =>',
);
buffer.writeln(' _\$${className}FromJson(json);');
buffer.writeln('');
// toJson
buffer.writeln(
' Map<String, dynamic> 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<ApiModel>.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<String>.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'");
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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<String, SwaggerDocument> _cachedDocuments = {};
SwaggerDataParser()
: _cacheManager = CacheManager(),
_performanceMonitor = PerformanceMonitor();
final CacheManager _cacheManager;
final PerformanceMonitor _performanceMonitor;
static final Logger _logger = Logger('SwaggerDataParser');
//
final Map<String, SwaggerDocument> _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<String, dynamic>;
} 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;

View File

@ -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<String, BaseCommand> _commands = {};
SwaggerCLI() {
_registerCommands();
}
static final Logger _logger = Logger('SwaggerCLI');
final Map<String, BaseCommand> _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(
_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',
);
print('');
print(' # 查看具体命令的帮助');
print(' dart swagger_cli_new.dart generate --help');
print('');
)
..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<void> main(List<String> 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);

View File

@ -1,7 +1,7 @@
/// Swagger Generator Flutter
///
/// Flutter OpenAPI 3.0 Dio + Retrofit
library swagger_generator_flutter;
library;
export 'core/error_reporter.dart';
//

View File

@ -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<String, _CacheEntry> _memoryCache = {};
final Queue<String> _accessOrder = Queue<String>();
CacheManager();
///
T? get<T>(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() {

View File

@ -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<Directory> 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<String> 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<bool> fileExists(String filePath) async {
return await File(filePath).exists();
return File(filePath).existsSync();
}
///
static Future<bool> directoryExists(String dirPath) async {
return await Directory(dirPath).exists();
return Directory(dirPath).existsSync();
}
///
static Future<void> 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<void> deleteDirectoryIfExists(String dirPath) async {
final directory = Directory(dirPath);
if (await directory.exists()) {
if (directory.existsSync()) {
await directory.delete(recursive: true);
}
}
///
static Future<void> 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<void> 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<int> 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<List<String>> listFiles(String dirPath,
{String? extension}) async {
static Future<List<String>> listFiles(
String dirPath, {
String? extension,
}) async {
try {
final directory = Directory(dirPath);
if (!await directory.exists()) {
@ -208,7 +227,7 @@ class FileUtils {
}
final files = <String>[];
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 = <String>[];
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<String> 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<void> 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<String> 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 = <String>[];
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<DateTime?> 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<String?> 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<List<FileSystemEntity>> 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 [];
}
}

View File

@ -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<String, PerformanceMetric> _metrics = {};
final Map<String, List<Duration>> _measurements = {};
final bool _enabled;
PerformanceMonitor({bool enabled = true}) : _enabled = enabled {
if (_enabled) {
_logger.info('性能监控器已启用');
}
}
///
Future<T> measure<T>(
String operationName, Future<T> Function() operation) async {
String operationName,
Future<T> 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<SlowOperation> getSlowOperations(
[Duration threshold = const Duration(milliseconds: 500)]) {
List<SlowOperation> getSlowOperations([
Duration threshold = const Duration(milliseconds: 500),
]) {
final slowOps = <SlowOperation>[];
for (final metric in _metrics.values) {
if (metric.maxTime >= threshold) {
slowOps.add(SlowOperation(
slowOps.add(
SlowOperation(
name: metric.operationName,
maxTime: metric.maxTime,
avgTime: metric.averageTime,
count: metric.count,
));
),
);
}
}
@ -157,7 +164,8 @@ class PerformanceMonitor {
'totalOperations': report.totalOperations,
'totalTime': report.totalTime.inMilliseconds,
},
'metrics': report.metrics.map((key, value) => MapEntry(key, {
'metrics': report.metrics.map(
(key, value) => MapEntry(key, {
'operationName': value.operationName,
'count': value.count,
'totalTime': value.totalTime.inMilliseconds,
@ -166,7 +174,8 @@ class PerformanceMonitor {
'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<String, PerformanceMetric> 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<String, PerformanceMetric> 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;
}

View File

@ -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<String, ApiModel> _resolvedModels = {};
@ -22,8 +27,6 @@ class ReferenceResolver {
///
int _currentDepth = 0;
ReferenceResolver({this.maxDepth = 50});
///
Map<String, ApiModel> resolveModels(Map<String, dynamic> 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<String, dynamic> json) {
final enumValues = List<dynamic>.from(json['enum'] ?? []);
final enumValues = List<dynamic>.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<String, dynamic>,
List<String>.from(json['required'] ?? []),
List<String>.from((json['required'] as List?) ?? []),
);
mergedProperties.addAll(directProperties);
mergedRequired.addAll(List<String>.from(json['required'] ?? []));
mergedRequired
.addAll(List<String>.from((json['required'] as List?) ?? []));
}
return ApiModel(
@ -192,10 +197,10 @@ class ReferenceResolver {
ApiModel _parseObjectModel(String name, Map<String, dynamic> json) {
final properties = _parseProperties(
json['properties'] as Map<String, dynamic>? ?? {},
List<String>.from(json['required'] ?? []),
List<String>.from((json['required'] as List?) ?? []),
);
final required = List<String>.from(json['required'] ?? []);
final required = List<String>.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,

View File

@ -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,

View File

@ -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<ValidationError> errors;
final List<String> warnings;
const ValidationResult({
required this.isValid,
required this.errors,
required this.warnings,
});
final bool isValid;
final List<ValidationError> errors;
final List<String> 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)';
}
}

View File

@ -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<ApiParameter> parameters, String pathKey, String pathPattern) {
List<ApiParameter> 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<String, ApiResponse> 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
.map(
(word) => word.isEmpty
? ''
: word[0].toUpperCase() + word.substring(1).toLowerCase())
.join('');
: word[0].toUpperCase() + word.substring(1).toLowerCase(),
)
.join();
}
}

View File

@ -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<ValidationError> errors;
final List<ValidationWarning> warnings;
const ValidationResult({
required this.isValid,
this.errors = const [],
@ -17,8 +13,9 @@ class ValidationResult {
});
///
factory ValidationResult.success(
{List<ValidationWarning> warnings = const []}) {
factory ValidationResult.success({
List<ValidationWarning> warnings = const [],
}) {
return ValidationResult(
isValid: true,
warnings: warnings,
@ -26,14 +23,19 @@ class ValidationResult {
}
///
factory ValidationResult.failure(List<ValidationError> errors,
{List<ValidationWarning> warnings = const []}) {
factory ValidationResult.failure(
List<ValidationError> errors, {
List<ValidationWarning> warnings = const [],
}) {
return ValidationResult(
isValid: false,
errors: errors,
warnings: warnings,
);
}
final bool isValid;
final List<ValidationError> errors;
final List<ValidationWarning> 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(
_errors.add(
const ValidationError(
path: 'info.title',
message: 'API 标题不能为空',
type: ValidationErrorType.required,
suggestion: '请提供有意义的 API 标题',
));
),
);
}
if (document.version.isEmpty) {
_errors.add(const ValidationError(
_errors.add(
const ValidationError(
path: 'info.version',
message: 'API 版本不能为空',
type: ValidationErrorType.required,
suggestion: '请使用语义化版本号,如 "1.0.0"',
));
),
);
}
if (document.description.isEmpty) {
_warnings.add(const ValidationWarning(
_warnings.add(
const ValidationWarning(
path: 'info.description',
message: 'API 描述为空',
suggestion: '建议添加 API 的详细描述',
));
),
);
}
}
///
void _validateServers(List<ApiServer> servers) {
if (servers.isEmpty) {
_warnings.add(const ValidationWarning(
_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(
_errors.add(
ValidationError(
path: '$path.url',
message: '服务器 URL 不能为空',
type: ValidationErrorType.required,
));
),
);
} else if (!_isValidUrl(server.url)) {
_errors.add(ValidationError(
_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(
_errors.add(
ValidationError(
path: '$path.variables.$name.default',
message: '服务器变量必须有默认值',
type: ValidationErrorType.required,
));
),
);
}
});
}
@ -208,11 +220,13 @@ class SchemaValidator {
///
void _validatePaths(Map<String, ApiPath> paths) {
if (paths.isEmpty) {
_errors.add(const ValidationError(
_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(
_warnings.add(
ValidationWarning(
path: '$pathKey.operationId',
message: '缺少操作 ID',
suggestion: '建议为每个操作添加唯一的 operationId',
));
),
);
}
//
if (path.summary.isEmpty) {
_warnings.add(ValidationWarning(
_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(
_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(
_errors.add(
ValidationError(
path: '$path.name',
message: '参数名称不能为空',
type: ValidationErrorType.required,
));
),
);
}
//
if (parameter.location == ParameterLocation.path && !parameter.required) {
_errors.add(ValidationError(
_errors.add(
ValidationError(
path: '$path.required',
message: '路径参数必须是必需的',
type: ValidationErrorType.constraint,
));
),
);
}
//
if (parameter.type == PropertyType.unknown) {
_warnings.add(ValidationWarning(
_warnings.add(
ValidationWarning(
path: '$path.type',
message: '参数类型未知',
suggestion: '建议明确指定参数类型',
));
),
);
}
}
///
void _validateRequestBody(ApiRequestBody requestBody, String path) {
if (requestBody.content.isEmpty) {
_errors.add(ValidationError(
_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(
_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(
_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(
_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(
_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(
_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(
_errors.add(
ValidationError(
path: '$path.name',
message: '属性名称不能为空',
type: ValidationErrorType.required,
));
),
);
}
if (property.type == PropertyType.unknown) {
_warnings.add(ValidationWarning(
_warnings.add(
ValidationWarning(
path: '$path.type',
message: '属性类型未知',
suggestion: '建议明确指定属性类型',
));
),
);
}
}
///
void _validateSecurity(List<ApiSecurityRequirement> security,
Map<String, ApiSecurityScheme> schemes) {
for (int i = 0; i < security.length; i++) {
void _validateSecurity(
List<ApiSecurityRequirement> security,
Map<String, ApiSecurityScheme> 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(
_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(
_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(
_errors.add(
ValidationError(
path: '$path.scheme',
message: 'HTTP 安全方案必须指定认证方案',
type: ValidationErrorType.required,
));
),
);
}
break;
case SecuritySchemeType.oauth2:
if (scheme.flows == null) {
_errors.add(ValidationError(
_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(
_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(
_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(
_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(
_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(
_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(
_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(
_errors.add(
ValidationError(
path: 'components.securitySchemes',
message: '引用的安全方案 "$ref" 未定义',
type: ValidationErrorType.reference,
suggestion: '定义缺失的安全方案或修正引用',
));
),
);
}
}
// 使
for (final schema in schemas) {
if (!schemaRefs.contains(schema)) {
_warnings.add(ValidationWarning(
_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(
_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]',
_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(
_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(
_warnings.add(
ValidationWarning(
path: 'paths["$pathPattern"][${path.method.value}].responses',
message: '缺少成功响应 (2xx)',
suggestion: '添加至少一个成功响应',
));
),
);
}
//
@ -711,11 +795,13 @@ class SchemaValidator {
});
if (!hasErrorResponse) {
_warnings.add(ValidationWarning(
_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, () => <String>{});
if (parameterNames[pathPattern]!.contains(key)) {
_errors.add(ValidationError(
_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<String> schemaRefs,
Set<String> securityRefs) {
void _collectReferences(
SwaggerDocument document,
Set<String> schemaRefs,
Set<String> 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<String, dynamic>? schema, Set<String> refs) {
Map<String, dynamic>? schema,
Set<String> 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);

View File

@ -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"

View File

@ -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
very_good_analysis: ^10.0.0

View File

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

View File

@ -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 = <String, dynamic>{
'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 = <String, dynamic>{
'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 = <String, dynamic>{
'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 = <String, dynamic>{
'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 = <String, dynamic>{
'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 = <String, dynamic>{
'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 = <String, dynamic>{
'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 = <String, dynamic>{
'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 = <String, dynamic>{
'openapi': '3.0.3',
// Missing info object
'paths': {},
};
expect(() => SwaggerDocument.fromJson(json),
throwsA(isA<FormatException>()));
throwsA(isA<FormatException>()),);
});
test('handles invalid OpenAPI version', () {
final json = {
final json = <String, dynamic>{
'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 = <String, dynamic>{
'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 = <String, dynamic>{
'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 = <String, dynamic>{
'openapi': '3.0.3',
'info': {'title': 'Empty API', 'version': '1.0.0'},
'paths': {},
@ -556,12 +556,12 @@ void main() {
final schemas = <String, dynamic>{};
// 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 = <String, dynamic>{
'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 = <String, dynamic>{
'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'},
},
},
},

View File

@ -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 = <String>[];
formData.forEach((key, value) {
final encodedKey = Uri.encodeComponent(key);
final encodedValue = Uri.encodeComponent(value.toString());
final encodedValue = Uri.encodeComponent(value);
encodedPairs.add('$encodedKey=$encodedValue');
});

View File

@ -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 = <String, ApiProperty>{};
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);

View File

@ -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<FormatException>()));
throwsA(isA<FormatException>()),);
});
test('handles invalid OpenAPI document', () async {
@ -426,7 +423,7 @@ void main() {
final parser = PerformanceParser();
expect(() => parser.parseDocument(jsonString),
throwsA(isA<FormatException>()));
throwsA(isA<FormatException>()),);
});
test('validation catches common errors', () async {
@ -471,7 +468,7 @@ void main() {
final paths = <String, dynamic>{};
final schemas = <String, dynamic>{};
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',);
});
});
});

View File

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

View File

@ -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': <String, dynamic>{
'description': 'Success',
'schema': <String, dynamic>{'type': 'object'},
}
},
},
'requestBody': <String, dynamic>{
'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 = <String, dynamic>{};
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'],

View File

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

View File

@ -1 +0,0 @@

View File

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

View File

@ -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 = <String, ApiPath>{};
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<String, dynamic> json)'),);
});
});
});

View File

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

View File

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

View File

@ -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('部长新增工作任务指标(会删除所有管理的班级任务指标)-删除所有管理的学习官的通用任务指标')}',);
}