feat: 恢复代码

This commit is contained in:
Max 2025-07-24 10:44:25 +08:00
parent 547a6c7f16
commit a12bf7e618
56 changed files with 30486 additions and 1876 deletions

View File

@ -0,0 +1,458 @@
# Augment 代码生成规范
## 基于 OpenAPI 3.0 标准的 Flutter API 代码生成规范
### 📋 **核心原则**
#### 1. **OpenAPI 3.0 标准优先**
- **严格遵循 OpenAPI 3.0 规范**
- **swagger.json 是唯一真实来源**
- **不进行主观推断或猜测**
- **有问题与服务器端沟通完善文档**
#### 2. **类型安全第一**
- **所有类型必须从 schema 定义中提取**
- **禁止硬编码类型映射**
- **使用强类型,避免 dynamic**
- **严格的可空性控制**
#### 3. **一致性保证**
- **统一的命名规范**
- **统一的文件结构**
- **统一的代码风格**
- **统一的注释格式**
---
## 🏗️ **项目结构规范**
### **目录结构**
```
generator/
├── api/ # API 接口文件
│ ├── api_client.dart # 主 API 客户端
│ ├── login_api.dart # 按 tag 分组的 API
│ └── ...
├── api_models/ # 数据模型
│ ├── index.dart # 统一导出文件
│ ├── user_result.dart # 具体模型类
│ └── ...
├── api_paths.dart # API 路径常量
├── build.yaml # 构建配置
└── pubspec.yaml # 依赖配置
```
### **文件命名规范**
- **API 文件**: `{tag_name}_api.dart` (snake_case)
- **模型文件**: `{schema_name}.dart` (snake_case)
- **参数文件**: `{operation_id}_parameters.dart`
- **类名**: `PascalCase`
- **方法名**: `camelCase`
- **常量**: `UPPER_SNAKE_CASE`
---
## 🔧 **代码生成规范**
### **1. API 接口生成**
#### **基本结构**
```dart
// {Tag} API 接口定义
// 基于 Swagger API 文档: {swagger_url}
// 由 xy_swagger_generator by max 生成
// Copyright (C) 2025 YuanXuan. All rights reserved.
import 'package:dio/dio.dart';
import 'package:retrofit/retrofit.dart';
import 'package:learning_officer_oa/common/models/common/base_result.dart';
// 按需导入分页类型
import 'package:learning_officer_oa/common/models/common/base_page_result.dart';
part '{tag_name}_api.g.dart';
/// {Tag} API 接口
/// 负责处理 {Tag} 相关的接口
@RestApi(parser: Parser.JsonSerializable)
abstract class {Tag}Api {
factory {Tag}Api(Dio dio, {String? baseUrl}) = _{Tag}Api;
}
```
#### **方法生成规则**
```dart
/// {summary}
@{HTTP_METHOD}('{path}')
Future<{ReturnType}> {methodName}(
// 路径参数
@Path('{param}') {Type} param,
// 查询参数
@Query('{param}') {Type}? param,
// 请求体(仅当 swagger 中明确定义时)
@Body() {Type} request
);
```
### **2. 返回类型规范**
#### **类型提取优先级**
1. **从 responses.200.content.application/json.schema 提取**
2. **从 responses.200.content.text/plain.schema 提取**
3. **特殊处理**: 健康检查接口返回 `BaseResult<void>`
4. **默认**: `BaseResult<Map<String, dynamic>>`
#### **分页类型判断**
```dart
// 当返回类型包含分页结构时使用 BasePageResult
BasePageResult<{ItemType}>
// 判断依据:
// 1. schema 中包含 pageIndex, pageSize, totalCount 等字段
// 2. 查询参数中包含分页参数 (page, size, limit 等)
```
### **3. 请求体生成规范**
#### **添加 @Body() 的条件**
```dart
// 仅在以下情况添加 @Body() 参数:
// 1. requestBody 在 swagger 中明确定义
// 2. parameters 中存在 in: "body" 的参数
// 3. 其他情况一律不添加
```
#### **请求体类型提取**
```dart
// 优先级:
// 1. requestBody.content.application/json.schema
// 2. requestBody.content.text/plain.schema
// 3. 默认: Map<String, dynamic>
```
---
## 📝 **数据模型生成规范**
### **1. 模型类结构**
#### **基本模板**
```dart
// {schemaName} 模型定义
// 基于 Swagger API 文档: {swagger_url}
// 由 xy_swagger_generator by max 生成
// Copyright (C) 2025 YuanXuan. All rights reserved.
import 'package:json_annotation/json_annotation.dart';
import 'index.dart';
part '{schema_name}.g.dart';
@JsonSerializable(checked: true, includeIfNull: false)
class {SchemaName} {
// 属性定义
const {SchemaName}({
// 构造函数参数
});
factory {SchemaName}.fromJson(Map<String, dynamic> json) =>
_${SchemaName}FromJson(json);
Map<String, dynamic> toJson() => _${SchemaName}ToJson(this);
}
```
### **2. 属性生成规则**
#### **可空性判断**
```dart
// 严格按照 OpenAPI 规范:
// 1. 有 "nullable": true -> 可空类型 (Type?)
// 2. 没有 "nullable": true -> 非空类型 (Type)
// 3. 忽略 required 字段,只看 nullable
final String name; // 非空
final String? nickname; // 可空 (nullable: true)
```
#### **类型映射**
```dart
// OpenAPI -> Dart 类型映射
"string" -> String
"integer" -> int
"number" -> double
"boolean" -> bool
"array" -> List<T>
"object" -> Map<String, dynamic> 或具体类型
"$ref" -> 引用的具体类型
```
#### **构造函数规则**
```dart
const {ClassName}({
required this.nonNullableField, // 非空字段必须 required
this.nullableField, // 可空字段不需要 required
});
```
### **3. 导入管理**
#### **按需导入原则**
```dart
// 只导入实际使用的类型
// 分页相关
import 'package:learning_officer_oa/common/models/common/base_page_result.dart';
// 基础响应
import 'package:learning_officer_oa/common/models/common/base_result.dart';
// 模型类型
import '../../api_models/{model_name}.dart';
```
---
## 🚫 **禁止事项**
### **1. 硬编码推断**
```dart
// ❌ 禁止基于路径关键词推断类型
if (path.contains('login')) return 'UserLoginResult';
// ❌ 禁止基于 tag 推断类型
if (tag.contains('task')) return 'TaskInfoResult';
// ❌ 禁止基于操作推断请求体
if (method == 'POST') addBody();
```
### **2. 不存在的类型**
```dart
// ❌ 禁止生成 swagger 中不存在的类型
Future<BaseResult<TaskInfoResult>> // TaskInfoResult 不存在
// ✅ 使用通用类型或实际存在的类型
Future<BaseResult<Map<String, dynamic>>>
Future<BaseResult<ActualExistingType>>
```
### **3. 主观判断**
```dart
// ❌ 禁止主观添加参数
@Body() Map<String, dynamic> request // swagger 中没有定义
// ❌ 禁止主观修改类型
List<dynamic> -> List<SpecificType> // 没有明确的 items schema
```
---
## ✅ **质量保证**
### **1. 生成前检查**
- **验证 swagger.json 格式正确性**
- **检查所有 $ref 引用完整性**
- **确认 components/schemas 定义完整**
### **2. 生成后验证**
- **所有生成的类型在 swagger 中都有定义**
- **没有硬编码的类型映射**
- **导入语句按需生成**
- **代码通过 dart analyze 检查**
### **3. 错误处理**
```dart
// 当 swagger 定义不完整时的处理策略:
// 1. 记录警告日志
// 2. 使用安全的默认类型
// 3. 提示完善 swagger 文档
// 4. 不进行主观推断
```
---
## 📞 **沟通机制**
### **文档问题反馈**
1. **发现 swagger 定义缺失** -> 联系后端完善
2. **类型定义不明确** -> 要求明确 schema
3. **响应结构不一致** -> 统一响应格式
4. **参数定义缺失** -> 补充参数说明
### **版本管理**
- **swagger.json 版本控制**
- **生成代码版本标记**
- **变更日志记录**
- **向后兼容性检查**
---
## 🛠️ **工具配置规范**
### **1. 生成器配置**
```yaml
# pubspec.yaml
dependencies:
dio: ^5.0.0
retrofit: ^4.0.0
json_annotation: ^4.8.0
dev_dependencies:
build_runner: ^2.3.0
retrofit_generator: ^8.0.0
json_serializable: ^6.6.0
```
### **2. 构建配置**
```yaml
# build.yaml
targets:
$default:
builders:
json_serializable:
options:
checked: true
include_if_null: false
explicit_to_json: true
```
### **3. 分析配置**
```yaml
# analysis_options.yaml
analyzer:
strong-mode:
implicit-casts: false
implicit-dynamic: false
linter:
rules:
- prefer_const_constructors
- prefer_final_fields
- avoid_dynamic_calls
```
---
## 📚 **最佳实践示例**
### **1. 标准 API 接口**
```dart
/// 用户登录
@POST('/api/v1/Login/userLogin')
Future<BaseResult<UserLoginResult>> userLogin(
@Body() LoginRequest request
);
/// 获取用户列表(分页)
@GET('/api/v1/User/GetUserList')
Future<BasePageResult<UserResult>> getUserList(
@Query('page') int page,
@Query('size') int size,
@Query('keyword') String? keyword
);
/// 健康检查
@GET('/health')
Future<BaseResult<void>> healthCheck();
/// 无明确 schema 的接口
@POST('/api/v1/Action/DoSomething')
Future<BaseResult<Map<String, dynamic>>> doSomething();
```
### **2. 标准数据模型**
```dart
@JsonSerializable(checked: true, includeIfNull: false)
class UserResult {
/// 用户ID
final int id;
/// 用户名
final String username;
/// 昵称(可空)
final String? nickname;
/// 头像URL可空
final String? avatarUrl;
/// 是否激活
final bool isActive;
const UserResult({
required this.id,
required this.username,
this.nickname,
this.avatarUrl,
required this.isActive,
});
factory UserResult.fromJson(Map<String, dynamic> json) =>
_$UserResultFromJson(json);
Map<String, dynamic> toJson() => _$UserResultToJson(this);
}
```
### **3. 参数类生成**
```dart
@JsonSerializable(checked: true, includeIfNull: false)
class GetUserListParameters {
final int page;
final int size;
final String? keyword;
const GetUserListParameters({
required this.page,
required this.size,
this.keyword,
});
factory GetUserListParameters.fromJson(Map<String, dynamic> json) =>
_$GetUserListParametersFromJson(json);
Map<String, dynamic> toJson() => _$GetUserListParametersToJson(this);
}
```
---
## 🔍 **代码审查清单**
### **生成代码检查项**
- [ ] 所有类型都在 swagger.json 中有定义
- [ ] 没有硬编码的类型推断
- [ ] 可空性严格按照 nullable 字段
- [ ] 导入语句按需生成
- [ ] 方法参数与 swagger 定义一致
- [ ] 返回类型正确提取
- [ ] 注释信息完整
- [ ] 代码格式规范
### **swagger.json 检查项**
- [ ] 所有接口都有明确的 responses 定义
- [ ] 所有 schema 都有完整的属性定义
- [ ] 所有 $ref 引用都存在
- [ ] 参数定义完整name, in, schema
- [ ] requestBody 定义明确
- [ ] 版本信息正确
---
## 📖 **参考资源**
### **官方文档**
- [OpenAPI 3.0 规范](https://swagger.io/specification/)
- [Retrofit for Dart](https://pub.dev/packages/retrofit)
- [JSON Serializable](https://pub.dev/packages/json_serializable)
### **项目相关**
- [项目 README](./README.md)
- [API 参考文档](./docs/API_REFERENCE.md)
- [贡献指南](./CONTRIBUTING.md)
---
**最后更新**: 2025-01-24
**版本**: v2.0
**维护者**: Augment Team

231
CODE_REVIEW_CHECKLIST.md Normal file
View File

@ -0,0 +1,231 @@
# Augment 代码生成审查清单
## 📋 **生成前检查**
### **Swagger 文档验证**
- [ ] swagger.json 文件格式正确
- [ ] OpenAPI 版本为 3.0.x
- [ ] 所有 $ref 引用都存在对应的定义
- [ ] components/schemas 部分完整
- [ ] 所有接口都有明确的 responses 定义
- [ ] 参数定义完整name, in, schema
- [ ] requestBody 定义明确(如果需要)
### **环境准备**
- [ ] Dart SDK 版本 >= 3.0.0
- [ ] 依赖包版本正确
- [ ] 网络连接正常(如果从 URL 获取 swagger
- [ ] 输出目录权限正确
---
## 🔧 **生成过程检查**
### **命令执行**
- [ ] 使用正确的生成命令
- [ ] 参数配置正确
- [ ] 没有错误或警告信息
- [ ] 生成统计信息合理
### **文件生成**
- [ ] API 文件按 tag 正确分组
- [ ] 模型文件命名规范
- [ ] 目录结构正确
- [ ] index.dart 文件更新
---
## ✅ **生成后验证**
### **API 接口检查**
#### **文件结构**
- [ ] 文件头注释完整
- [ ] 导入语句正确且按需导入
- [ ] 类名符合 PascalCase 规范
- [ ] 方法名符合 camelCase 规范
#### **方法定义**
- [ ] HTTP 方法注解正确(@GET, @POST, @PUT, @DELETE
- [ ] 路径定义与 swagger 一致
- [ ] 返回类型正确提取
- [ ] 参数定义完整且正确
```dart
// ✅ 正确的方法定义示例
/// 用户登录
@POST('/api/v1/Login/userLogin')
Future<BaseResult<UserLoginResult>> userLogin(
@Body() LoginRequest request
);
// ❌ 错误示例
@POST('/api/v1/Login/userLogin')
Future<BaseResult<TaskInfoResult>> userLogin( // 错误:不存在的类型
@Body() Map<String, dynamic> request // 错误swagger 中没有定义
);
```
#### **参数检查**
- [ ] 路径参数使用 @Path 注解
- [ ] 查询参数使用 @Query 注解
- [ ] 请求体参数使用 @Body 注解
- [ ] 参数类型与 swagger 定义一致
- [ ] 可空性正确(? 标记)
- [ ] 没有多余的参数
#### **返回类型检查**
- [ ] 使用 BaseResult 包装
- [ ] 分页接口使用 BasePageResult
- [ ] 健康检查接口返回 void
- [ ] 泛型类型存在于 swagger 中
- [ ] 没有硬编码推断的类型
#### **导入检查**
- [ ] 基础类型导入正确
- [ ] 分页类型按需导入
- [ ] 模型类型导入完整
- [ ] 没有未使用的导入
- [ ] 导入路径正确
### **数据模型检查**
#### **类定义**
- [ ] 类名符合 PascalCase 规范
- [ ] @JsonSerializable 注解正确
- [ ] 注解参数配置正确checked: true, includeIfNull: false
#### **属性定义**
- [ ] 属性名符合 camelCase 规范
- [ ] 类型映射正确string -> String, integer -> int
- [ ] 可空性严格按照 nullable 字段
- [ ] 注释信息完整
- [ ] final 修饰符正确使用
```dart
// ✅ 正确的属性定义
/// 用户ID
final int id;
/// 用户名
final String username;
/// 昵称(可空)
final String? nickname; // swagger 中 "nullable": true
// ❌ 错误示例
final String? username; // 错误swagger 中没有 "nullable": true
final String nickname; // 错误swagger 中有 "nullable": true
```
#### **构造函数检查**
- [ ] 使用 const 构造函数
- [ ] 非空字段标记为 required
- [ ] 可空字段不使用 required
- [ ] 参数顺序合理
#### **序列化方法**
- [ ] fromJson 工厂方法存在
- [ ] toJson 方法存在
- [ ] part 文件引用正确
- [ ] 生成的方法名正确
### **代码质量检查**
#### **静态分析**
- [ ] dart analyze 无错误
- [ ] dart analyze 无警告
- [ ] 代码格式化正确
- [ ] 命名规范一致
#### **类型安全**
- [ ] 避免使用 dynamic
- [ ] 泛型类型明确
- [ ] 可空性处理正确
- [ ] 类型转换安全
#### **性能考虑**
- [ ] 使用 const 构造函数
- [ ] 避免不必要的对象创建
- [ ] 导入优化
- [ ] 内存使用合理
---
## 🚫 **常见问题检查**
### **类型相关**
- [ ] 没有生成不存在的类型(如 TaskInfoResult
- [ ] 没有硬编码的类型映射
- [ ] 没有基于路径关键词的推断
- [ ] 没有基于 tag 的类型推断
### **参数相关**
- [ ] 没有添加 swagger 中未定义的参数
- [ ] 没有遗漏 swagger 中定义的参数
- [ ] 参数位置正确path, query, body
- [ ] 参数类型正确
### **导入相关**
- [ ] 没有循环导入
- [ ] 没有未使用的导入
- [ ] 没有缺失的导入
- [ ] 导入路径正确
### **命名相关**
- [ ] 文件名符合 snake_case
- [ ] 类名符合 PascalCase
- [ ] 方法名符合 camelCase
- [ ] 变量名符合 camelCase
---
## 🔄 **集成测试**
### **构建测试**
- [ ] dart pub get 成功
- [ ] dart pub run build_runner build 成功
- [ ] 生成的 .g.dart 文件正确
- [ ] 没有构建错误或警告
### **运行时测试**
- [ ] API 调用正常
- [ ] 序列化/反序列化正常
- [ ] 类型转换正确
- [ ] 错误处理正确
### **兼容性测试**
- [ ] Dart 版本兼容
- [ ] Flutter 版本兼容
- [ ] 依赖包版本兼容
- [ ] 平台兼容性
---
## 📝 **文档检查**
### **代码注释**
- [ ] 类注释完整
- [ ] 方法注释完整
- [ ] 属性注释完整
- [ ] 特殊逻辑有说明
### **生成信息**
- [ ] 文件头信息正确
- [ ] 版本信息正确
- [ ] 生成时间标记
- [ ] 来源信息明确
---
## ✍️ **审查签名**
**审查者**: _______________
**审查日期**: _______________
**审查结果**: [ ] 通过 [ ] 需要修改 [ ] 拒绝
**备注**: _______________
---
**检查清单版本**: v2.0
**最后更新**: 2025-01-24

369
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,369 @@
# 贡献指南
感谢您对 Swagger Generator Flutter 项目的关注!我们欢迎各种形式的贡献。
## 🤝 如何贡献
### 报告问题
如果您发现了 bug 或有功能建议,请:
1. 检查 [现有 Issues](https://github.com/your-repo/swagger_generator_flutter/issues) 是否已有相关报告
2. 如果没有,请创建新的 Issue包含
- 清晰的标题和描述
- 重现步骤(如果是 bug
- 期望的行为
- 实际的行为
- 环境信息Dart/Flutter 版本等)
- 相关的代码片段或错误日志
### 提交代码
1. **Fork 项目**
```bash
git clone https://github.com/your-username/swagger_generator_flutter.git
cd swagger_generator_flutter
```
2. **创建分支**
```bash
git checkout -b feature/your-feature-name
# 或
git checkout -b fix/your-bug-fix
```
3. **安装依赖**
```bash
flutter pub get
```
4. **进行更改**
- 遵循项目的代码风格
- 添加必要的测试
- 更新相关文档
5. **运行测试**
```bash
dart test
```
6. **提交更改**
```bash
git add .
git commit -m "feat: add new feature" # 遵循 Conventional Commits
```
7. **推送分支**
```bash
git push origin feature/your-feature-name
```
8. **创建 Pull Request**
- 提供清晰的 PR 描述
- 链接相关的 Issues
- 确保所有检查通过
## 📝 代码风格
### Dart 代码风格
我们遵循 [Dart 官方代码风格指南](https://dart.dev/guides/language/effective-dart/style)
```dart
// ✅ 好的示例
class ApiGenerator {
final String className;
final bool generateModels;
ApiGenerator({
required this.className,
this.generateModels = true,
});
String generateCode() {
// 实现逻辑
return 'generated code';
}
}
// ❌ 不好的示例
class api_generator {
String class_name;
bool generate_models;
api_generator(this.class_name, this.generate_models);
String generate_code() {
return "generated code";
}
}
```
### 命名约定
- **类名**: PascalCase (`ApiGenerator`)
- **方法名**: camelCase (`generateCode`)
- **变量名**: camelCase (`className`)
- **常量**: SCREAMING_SNAKE_CASE (`DEFAULT_TIMEOUT`)
- **文件名**: snake_case (`api_generator.dart`)
### 注释规范
```dart
/// 生成 Retrofit API 代码的生成器
///
/// 支持多种配置选项,包括:
/// - 模块化 API 生成
/// - 基础响应类型
/// - 分页支持
///
/// 示例用法:
/// ```dart
/// final generator = RetrofitApiGenerator(
/// className: 'ApiService',
/// splitByTags: true,
/// );
/// ```
class RetrofitApiGenerator {
/// API 服务类名
final String className;
/// 是否按标签分割 API
final bool splitByTags;
/// 创建 Retrofit API 生成器
///
/// [className] 生成的 API 服务类名
/// [splitByTags] 是否按标签分割成多个 API 类
RetrofitApiGenerator({
this.className = 'ApiService',
this.splitByTags = false,
});
}
```
## 🧪 测试指南
### 测试结构
```
tests/
├── unit/ # 单元测试
│ ├── generators/ # 生成器测试
│ ├── parsers/ # 解析器测试
│ └── validators/ # 验证器测试
├── integration/ # 集成测试
└── fixtures/ # 测试数据
```
### 编写测试
```dart
import 'package:test/test.dart';
import 'package:swagger_generator_flutter/swagger_generator_flutter.dart';
void main() {
group('RetrofitApiGenerator', () {
late RetrofitApiGenerator generator;
setUp(() {
generator = RetrofitApiGenerator(
className: 'TestApi',
splitByTags: false,
);
});
test('should generate basic API structure', () {
// Arrange
final document = createTestDocument();
// Act
final result = generator.generateFromDocument(document);
// Assert
expect(result, contains('abstract class TestApi'));
expect(result, contains('@RestApi()'));
});
test('should handle empty document', () {
// Arrange
final emptyDocument = createEmptyDocument();
// Act & Assert
expect(() => generator.generateFromDocument(emptyDocument),
returnsNormally);
});
});
}
SwaggerDocument createTestDocument() {
return SwaggerDocument(
title: 'Test API',
version: '1.0.0',
description: 'Test',
servers: [],
components: ApiComponents(schemas: {}, securitySchemes: {}),
paths: {},
models: {},
controllers: {},
security: [],
);
}
```
### 测试覆盖率
我们目标是保持 90%+ 的测试覆盖率:
```bash
# 运行测试并生成覆盖率报告
dart test --coverage=coverage
genhtml coverage/lcov.info -o coverage/html
```
## 📚 文档贡献
### 文档类型
1. **API 文档**: 代码中的 dartdoc 注释
2. **用户指南**: README.md 和 docs/ 目录
3. **示例代码**: example/ 目录
4. **迁移指南**: MIGRATION_GUIDE.md
### 文档风格
- 使用清晰、简洁的语言
- 提供实际的代码示例
- 包含常见用例和最佳实践
- 保持文档与代码同步
## 🔄 发布流程
### 版本号规范
我们遵循 [语义化版本](https://semver.org/lang/zh-CN/)
- **主版本号**: 不兼容的 API 修改
- **次版本号**: 向下兼容的功能性新增
- **修订号**: 向下兼容的问题修正
### 提交信息规范
我们使用 [Conventional Commits](https://www.conventionalcommits.org/zh-hans/)
```
<类型>[可选的作用域]: <描述>
[可选的正文]
[可选的脚注]
```
**类型:**
- `feat`: 新功能
- `fix`: 修复 bug
- `docs`: 文档更新
- `style`: 代码格式调整
- `refactor`: 重构
- `test`: 测试相关
- `chore`: 构建过程或辅助工具的变动
**示例:**
```
feat(generator): add support for file upload
- Add MultipartFile support in OptimizedRetrofitGenerator
- Generate proper @MultiPart annotations
- Update tests and documentation
Closes #123
```
## 🏗️ 开发环境设置
### 必需工具
- Dart SDK 3.0+
- Flutter SDK 3.0+
- Git
### 推荐工具
- VS Code 或 IntelliJ IDEA
- Dart 和 Flutter 插件
- Git hooks (pre-commit)
### 环境配置
1. **克隆项目**
```bash
git clone https://github.com/your-repo/swagger_generator_flutter.git
cd swagger_generator_flutter
```
2. **安装依赖**
```bash
flutter pub get
```
3. **运行测试**
```bash
dart test
```
4. **运行示例**
```bash
dart run example/basic_usage.dart
```
### 开发工作流
1. 创建功能分支
2. 编写代码和测试
3. 运行所有测试
4. 更新文档
5. 提交代码
6. 创建 Pull Request
## 🎯 贡献领域
我们特别欢迎以下领域的贡献:
### 高优先级
- 🐛 Bug 修复
- 📚 文档改进
- 🧪 测试覆盖率提升
- 🚀 性能优化
### 中优先级
- ✨ 新功能开发
- 🔧 工具改进
- 📝 示例代码
- 🌐 国际化支持
### 低优先级
- 🎨 UI/UX 改进
- 📦 依赖更新
- 🔍 代码质量提升
## 📞 联系我们
- **GitHub Issues**: 报告 bug 和功能请求
- **GitHub Discussions**: 一般讨论和问题
- **Email**: maintainer@example.com
## 📄 许可证
通过贡献代码,您同意您的贡献将在与项目相同的 [MIT 许可证](LICENSE) 下授权。
## 🙏 致谢
感谢所有贡献者的努力!您的贡献让这个项目变得更好。
### 贡献者列表
<!-- 这里会自动生成贡献者列表 -->
---
再次感谢您的贡献!🎉

228
QUICK_REFERENCE.md Normal file
View File

@ -0,0 +1,228 @@
# Augment 代码生成快速参考
## 🚀 **快速开始**
### **生成 API 代码**
```bash
# 生成所有代码
dart run bin/main.dart generate --api --models --split-by-tags
# 只生成 API 接口
dart run bin/main.dart generate --api --split-by-tags
# 只生成数据模型
dart run bin/main.dart generate --models
```
### **项目集成**
```yaml
# pubspec.yaml
dependencies:
dio: ^5.0.0
retrofit: ^4.0.0
json_annotation: ^4.8.0
dev_dependencies:
build_runner: ^2.3.0
retrofit_generator: ^8.0.0
json_serializable: ^6.6.0
```
```bash
# 运行代码生成
dart pub get
dart pub run build_runner build
```
---
## 📋 **常见问题解决**
### **Q: 生成的类型不存在怎么办?**
```dart
// ❌ 错误:生成了不存在的类型
Future<BaseResult<TaskInfoResult>> someMethod();
// ✅ 解决:检查 swagger.json 中是否定义了 TaskInfoResult
// 如果没有定义,联系后端添加 schema 定义
// 或者使用通用类型:
Future<BaseResult<Map<String, dynamic>>> someMethod();
```
### **Q: 接口缺少参数怎么办?**
```dart
// ❌ 错误swagger 中没有定义参数,但生成器添加了
@POST('/api/v1/SomeAction')
Future<BaseResult<void>> someAction(
@Body() Map<String, dynamic> request // 不应该存在
);
// ✅ 解决:检查 swagger.json 中的参数定义
// 如果确实需要参数,联系后端在 swagger 中添加定义
```
### **Q: 可空性不正确怎么办?**
```dart
// ❌ 错误:字段应该可空但生成为非空
final String name; // 但实际可能为 null
// ✅ 解决:检查 swagger.json 中的 nullable 字段
{
"name": {
"type": "string",
"nullable": true // 添加这个字段
}
}
```
### **Q: 分页接口没有使用 BasePageResult**
```dart
// ❌ 错误:分页接口使用了错误的返回类型
Future<BaseResult<List<UserResult>>> getUserList();
// ✅ 解决:检查接口是否真的是分页接口
// 确保 swagger 中定义了分页相关的查询参数和响应结构
Future<BasePageResult<UserResult>> getUserList();
```
---
## 🔧 **调试技巧**
### **1. 检查 swagger.json**
```bash
# 验证 swagger.json 格式
curl -X POST "https://validator.swagger.io/validator/debug" \
-H "Content-Type: application/json" \
-d @swagger.json
```
### **2. 查看生成日志**
```bash
# 启用详细日志
dart run bin/main.dart generate --api --models --verbose
```
### **3. 手动验证类型**
```dart
// 在生成的代码中添加断言验证
assert(response.data is UserResult);
assert(response.data?.name != null);
```
---
## 📝 **代码模板**
### **标准 API 接口模板**
```dart
/// {接口描述}
@{HTTP_METHOD}('{路径}')
Future<{返回类型}> {方法名}(
// 路径参数(如果有)
@Path('{参数名}') {类型} 参数名,
// 查询参数(如果有)
@Query('{参数名}') {类型}? 参数名,
// 请求体(仅当 swagger 中明确定义时)
@Body() {类型} request
);
```
### **标准数据模型模板**
```dart
@JsonSerializable(checked: true, includeIfNull: false)
class {类名} {
/// {字段描述}
final {类型} {字段名};
const {类名}({
required this.{非空字段},
this.{可空字段},
});
factory {类名}.fromJson(Map<String, dynamic> json) =>
_${类名}FromJson(json);
Map<String, dynamic> toJson() => _${类名}ToJson(this);
}
```
---
## ⚡ **性能优化**
### **1. 按需导入**
```dart
// ✅ 只导入需要的类型
import 'package:learning_officer_oa/common/models/common/base_result.dart';
// ❌ 避免导入不需要的类型
// import 'package:learning_officer_oa/common/models/common/base_page_result.dart';
```
### **2. 使用 const 构造函数**
```dart
// ✅ 使用 const 构造函数
const UserResult({
required this.id,
required this.name,
});
// ❌ 避免非 const 构造函数
UserResult({
required this.id,
required this.name,
});
```
### **3. 合理的类型选择**
```dart
// ✅ 使用具体类型
Future<BaseResult<UserResult>> getUser();
// ❌ 避免过度使用 dynamic
Future<BaseResult<dynamic>> getUser();
```
---
## 🛡️ **安全检查**
### **生成前检查**
- [ ] swagger.json 格式正确
- [ ] 所有 $ref 引用存在
- [ ] 版本信息匹配
### **生成后检查**
- [ ] 代码通过 dart analyze
- [ ] 所有导入都存在
- [ ] 类型定义完整
- [ ] 可空性正确
### **部署前检查**
- [ ] 运行所有测试
- [ ] 代码格式化
- [ ] 文档更新
- [ ] 版本标记
---
## 📞 **获取帮助**
### **常见错误代码**
- **E001**: swagger.json 格式错误
- **E002**: 缺少必要的 schema 定义
- **E003**: $ref 引用不存在
- **E004**: 类型映射失败
### **联系方式**
- **技术支持**: 联系后端团队完善 swagger 文档
- **Bug 报告**: 提交到项目 Issues
- **功能请求**: 通过 PR 贡献代码
---
**快速参考版本**: v2.0
**最后更新**: 2025-01-24

247
README.md
View File

@ -2,32 +2,207 @@
基于 Swagger/OpenAPI 的 Dart/Flutter API/模型代码生成工具。
## 功能简介
- 根据 swagger.json 自动生成 Dart API 接口、模型、枚举等
- 支持 Retrofit、json_serializable 等主流生态
- 支持自定义生成规则和命名风格
[![Dart](https://img.shields.io/badge/Dart-3.0+-blue.svg)](https://dart.dev/)
[![Flutter](https://img.shields.io/badge/Flutter-3.0+-blue.svg)](https://flutter.dev/)
[![OpenAPI](https://img.shields.io/badge/OpenAPI-3.0+-green.svg)](https://spec.openapis.org/oas/v3.0.3/)
## 快速开始
## ✨ 功能特性
### 🚀 核心功能
- **完整的 OpenAPI 3.0 支持**:涵盖所有主要规范特性
- **高性能解析**:支持并行解析和流式处理大型文档
- **智能代码生成**:生成高质量的 Dart/Flutter 代码
- **模块化架构**:按 API 模块自动分组生成
### 🎯 专为 Flutter 优化
- **Dio + Retrofit 集成**:完美适配主流网络架构
- **类型安全**:生成强类型的 API 接口和模型
- **JSON 序列化**:自动生成 json_serializable 代码
- **文件上传支持**:完整的 multipart/form-data 支持
### 🔧 高级特性
- **智能缓存**:提升重复操作性能
- **错误诊断**:详细的错误报告和修复建议
- **性能监控**:内置性能统计和优化
- **增量生成**:支持增量更新和变更检测
## 📚 **文档和规范**
### **核心文档**
- [**代码生成规范**](./AUGMENT_CODE_GENERATION_STANDARDS.md) - 完整的生成规范和最佳实践
- [**快速参考指南**](./QUICK_REFERENCE.md) - 常见问题和解决方案
- [**代码审查清单**](./CODE_REVIEW_CHECKLIST.md) - 质量保证检查清单
- [**生成器配置**](./generator_config.yaml) - 详细的配置选项
### **设计原则**
1. **OpenAPI 3.0 标准优先** - 严格遵循规范,不进行主观推断
2. **与服务器保持一致** - swagger.json 是唯一真实来源
3. **有问题沟通文档** - 发现问题时要求完善后端文档
4. **类型安全第一** - 生成强类型代码,避免运行时错误
## 🚀 快速开始
### 1. 安装依赖
```bash
# 安装依赖
flutter pub get
# 或
pub get
```
# 生成模型和API
sh run_swagger.sh
# 或
### 2. 基础用法(命令行)
```bash
# 生成模型和API推荐
sh run_swagger.sh all
# 分别生成
sh run_swagger.sh api # 只生成 API
sh run_swagger.sh models # 只生成模型
# 或直接使用 dart 命令
dart run bin/main.dart generate --models --api
```
## 目录结构
### 3. 编程式用法(推荐)
```dart
import 'package:swagger_generator_flutter/swagger_generator_flutter.dart';
void main() async {
// 使用高性能解析器
final parser = PerformanceParser(
config: ParseConfig(
enablePerformanceStats: true,
enableParallelParsing: true,
),
);
// 解析 OpenAPI 文档
final jsonString = await File('swagger.json').readAsString();
final document = await parser.parseDocument(jsonString);
// 使用优化生成器
final generator = OptimizedRetrofitGenerator(
className: 'ApiService',
generateModularApis: true,
generateBaseResult: true,
generatePagination: true,
generateFileUpload: true,
);
// 生成代码
final generatedCode = generator.generateFromDocument(document);
// 保存文件
await File('lib/api/api_service.dart').writeAsString(generatedCode);
// 查看性能统计
final stats = parser.lastStats;
print('解析时间: ${stats?.totalTime.inMilliseconds}ms');
print('生成的路径数: ${stats?.pathCount}');
}
```
swagger/
## 📖 详细配置
### 生成器类型
#### 1. RetrofitApiGenerator基础版
```dart
final generator = RetrofitApiGenerator(
className: 'ApiService', // 生成的类名
splitByTags: true, // 是否按标签分割(默认启用)
useRetrofit: true, // 使用 Retrofit 注解
generateModels: true, // 生成模型类
);
```
#### 2. OptimizedRetrofitGenerator推荐
```dart
final generator = OptimizedRetrofitGenerator(
className: 'ApiService',
generateModularApis: true, // 模块化 API
generateBaseResult: true, // 基础响应类型
generatePagination: true, // 分页支持
generateFileUpload: true, // 文件上传
baseResultType: 'BaseResult', // 自定义基础类型
pageResultType: 'PageResult', // 自定义分页类型
);
```
#### 3. PerformanceGenerator高性能
```dart
final generator = PerformanceGenerator(
maxConcurrency: 4, // 最大并发数
enableCaching: true, // 启用缓存
enableIncremental: true, // 增量生成
enableParallel: true, // 并行生成
);
```
### 解析器配置
```dart
final parser = PerformanceParser(
config: ParseConfig(
enableParallelParsing: true, // 并行解析
enableStreamParsing: false, // 流式解析
enableCaching: true, // 缓存
maxConcurrency: 4, // 最大并发数
enablePerformanceStats: true, // 性能统计
),
);
```
### 验证和错误处理
```dart
// 创建验证器
final validator = EnhancedValidator(
strictMode: false, // 严格模式
includeWarnings: true, // 包含警告
);
// 验证文档
final isValid = validator.validateDocument(document);
// 获取错误报告
final errorReport = validator.errorReporter.generateReport();
print(errorReport);
```
## 📊 性能优化
### 缓存策略
```dart
// 配置智能缓存
final cache = SmartCache<String>(
maxSize: 1000, // 最大缓存大小
strategy: CacheStrategy.smart, // 缓存策略
defaultTtl: Duration(hours: 1), // 默认过期时间
);
// 获取缓存统计
final stats = cache.getStats();
print('缓存命中率: ${(stats.hitRate * 100).toStringAsFixed(1)}%');
```
### 性能监控
```dart
// 获取解析性能统计
final parseStats = parser.lastStats;
print('解析性能统计:');
print(' 总时间: ${parseStats?.totalTime.inMilliseconds}ms');
print(' 路径数: ${parseStats?.pathCount}');
print(' 吞吐量: ${parseStats?.bytesPerSecond.toStringAsFixed(2)} bytes/s');
```
## 📁 目录结构
```
swagger_generator_flutter/
bin/ # 命令行入口
example/ # 使用示例
generator/ # 生成的 API、模型、文档
lib/ # 生成器核心代码
tests/ # 单元测试
swagger.json # Swagger/OpenAPI 源文件
core/ # 核心模型和解析器
generators/ # 代码生成器
validators/ # 文档验证器
tests/ # 单元测试和集成测试
swagger.json # OpenAPI 源文件
```
## 运行测试
@ -35,14 +210,52 @@ swagger/
dart run test tests/
```
## 🎯 **规范遵循示例**
### **✅ 正确的生成结果**
```dart
// 严格按照 swagger.json 定义生成
@POST('/api/v1/Login/userLogin')
Future<BaseResult<UserLoginResult>> userLogin(
@Body() LoginRequest request // 仅当 swagger 中明确定义时
);
// 健康检查接口
@GET('/health')
Future<BaseResult<void>> healthCheck();
// 无明确 schema 的接口
@POST('/api/v1/Action/DoSomething')
Future<BaseResult<Map<String, dynamic>>> doSomething();
```
### **❌ 避免的错误做法**
```dart
// 错误:生成不存在的类型
Future<BaseResult<TaskInfoResult>> someMethod();
// 错误:添加 swagger 中未定义的参数
@POST('/api/v1/GetUsers')
Future<BaseResult<List<User>>> getUsers(
@Body() Map<String, dynamic> request // swagger 中没有定义
);
// 错误:基于关键词推断类型
if (path.contains('login')) return 'UserLoginResult';
```
## 贡献指南
- **严格遵循 [代码生成规范](./AUGMENT_CODE_GENERATION_STANDARDS.md)**
- **使用 [代码审查清单](./CODE_REVIEW_CHECKLIST.md) 进行质量检查**
- 代码需包含中英文注释
- 新增功能请补充对应测试用例
- 生成规则/命名风格如有特殊需求请在 issue 说明
## 常见问题
- 生成模型/接口命名不规范?请检查 swagger 字段命名和生成规则
- 枚举、泛型、嵌套对象支持?已支持主流用法,特殊场景请补充 issue
- **生成的类型不存在?** 检查 swagger.json 中是否定义了对应的 schema
- **接口缺少参数?** 确认 swagger.json 中是否有完整的参数定义
- **可空性不正确?** 检查 swagger.json 中的 nullable 字段设置
- 更多问题请参考 [快速参考指南](./QUICK_REFERENCE.md)
### 脚本命令说明

View File

@ -37,17 +37,18 @@ linter:
rules:
# 常用且推荐启用的规则 (即使默认集没有包含,也建议手动添加)
- avoid_empty_else # 避免空的 else 块
- avoid_print # 在生产代码中避免使用 print (可根据项目需求启用/禁用)
# - 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_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

457
docs/API_REFERENCE.md Normal file
View File

@ -0,0 +1,457 @@
# API 参考文档
本文档详细介绍了 Swagger Generator Flutter 的所有 API 接口和配置选项。
## 目录
- [核心类](#核心类)
- [生成器](#生成器)
- [解析器](#解析器)
- [验证器](#验证器)
- [缓存系统](#缓存系统)
- [配置选项](#配置选项)
## 核心类
### SwaggerDocument
OpenAPI 文档的主要数据模型。
```dart
class SwaggerDocument {
final String title;
final String version;
final String description;
final List<ApiServer> servers;
final Map<String, ApiPath> paths;
final Map<String, ApiModel> models;
final ApiComponents components;
final List<ApiSecurityRequirement> security;
SwaggerDocument({
required this.title,
required this.version,
required this.description,
required this.servers,
required this.paths,
required this.models,
required this.components,
required this.security,
});
factory SwaggerDocument.fromJson(Map<String, dynamic> json);
Map<String, dynamic> toJson();
}
```
### ApiPath
API 路径和操作的定义。
```dart
class ApiPath {
final String path;
final HttpMethod method;
final String summary;
final String description;
final String operationId;
final List<String> tags;
final List<ApiParameter> parameters;
final ApiRequestBody? requestBody;
final Map<String, ApiResponse> responses;
final List<ApiSecurityRequirement> security;
ApiPath({
required this.path,
required this.method,
required this.summary,
required this.description,
required this.operationId,
required this.tags,
required this.parameters,
this.requestBody,
required this.responses,
required this.security,
});
}
```
### ApiModel
数据模型的定义。
```dart
class ApiModel {
final String name;
final String description;
final Map<String, ApiProperty> properties;
final List<String> required;
ApiModel({
required this.name,
required this.description,
required this.properties,
required this.required,
});
}
```
## 生成器
### BaseGenerator
所有生成器的基类。
```dart
abstract class BaseGenerator {
String get generatorType;
String generate();
}
```
### RetrofitApiGenerator
基础的 Retrofit API 生成器。
```dart
class RetrofitApiGenerator extends BaseGenerator {
final String className;
final bool splitByTags;
final bool useRetrofit;
final bool generateModels;
RetrofitApiGenerator({
this.className = 'ApiService',
this.splitByTags = false,
this.useRetrofit = true,
this.generateModels = true,
});
@override
String generateFromDocument(SwaggerDocument document);
}
```
#### 方法
- `generateFromDocument(SwaggerDocument document)`: 从文档生成代码
- `generateSingleApiFile()`: 生成单个 API 文件
- `generateMainApiFile()`: 生成主 API 文件(分模块时)
### OptimizedRetrofitGenerator
优化版的 Retrofit API 生成器。
```dart
class OptimizedRetrofitGenerator extends BaseGenerator {
final String className;
final bool generateModularApis;
final bool generateBaseResult;
final bool generatePagination;
final bool generateFileUpload;
final String baseResultType;
final String pageResultType;
OptimizedRetrofitGenerator({
this.className = 'ApiService',
this.generateModularApis = true,
this.generateBaseResult = true,
this.generatePagination = true,
this.generateFileUpload = true,
this.baseResultType = 'BaseResult',
this.pageResultType = 'BasePageResult',
});
}
```
#### 特性
- **模块化生成**: 按 API 标签自动分组
- **基础类型**: 生成 BaseResult、BasePageResult 等
- **文件上传**: 支持 multipart/form-data
- **工具类**: 生成 ApiUtils 工具类
### PerformanceGenerator
高性能代码生成器。
```dart
class PerformanceGenerator extends BaseGenerator {
final int maxConcurrency;
final bool enableCaching;
final bool enableIncremental;
final bool enableParallel;
PerformanceGenerator({
this.maxConcurrency = 4,
this.enableCaching = true,
this.enableIncremental = true,
this.enableParallel = true,
});
Future<String> generateFromDocument(SwaggerDocument document);
GenerationStats getStats();
CacheStats getCacheStats();
void clearCache();
}
```
#### 方法
- `generateFromDocument()`: 异步生成代码
- `getStats()`: 获取生成性能统计
- `getCacheStats()`: 获取缓存统计
- `clearCache()`: 清除缓存
## 解析器
### PerformanceParser
高性能 OpenAPI 解析器。
```dart
class PerformanceParser {
final ParseConfig config;
PerformanceParser({ParseConfig? config});
Future<SwaggerDocument> parseDocument(String jsonString);
ParsePerformanceStats? get lastStats;
void clearCache();
Map<String, dynamic> getCacheStats();
}
```
### ParseConfig
解析器配置。
```dart
class ParseConfig {
final bool enableParallelParsing;
final bool enableStreamParsing;
final bool enableIncrementalParsing;
final bool enableCaching;
final int maxConcurrency;
final int streamBufferSize;
final bool enablePerformanceStats;
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,
});
}
```
## 验证器
### EnhancedValidator
增强的文档验证器。
```dart
class EnhancedValidator {
final bool strictMode;
final bool includeWarnings;
EnhancedValidator({
this.strictMode = false,
this.includeWarnings = true,
});
bool validateDocument(SwaggerDocument document);
ErrorReporter get errorReporter;
}
```
### ErrorReporter
错误报告器。
```dart
class ErrorReporter {
List<DetailedError> get errors;
bool get hasErrors;
bool get hasCriticalErrors;
void addError(DetailedError error);
void reportError({
required String id,
required String title,
required String description,
required ErrorSeverity severity,
required ErrorCategory category,
required String jsonPath,
// ... 其他参数
});
List<DetailedError> getErrorsBySeverity(ErrorSeverity severity);
List<DetailedError> getErrorsByCategory(ErrorCategory category);
Map<ErrorSeverity, int> getErrorStatistics();
String generateReport({
bool includeStatistics = true,
bool groupByCategory = false,
ErrorSeverity? minSeverity,
});
String generateJsonReport();
void clear();
}
```
## 缓存系统
### SmartCache
智能缓存管理器。
```dart
class SmartCache<T> {
final int maxSize;
final CacheStrategy strategy;
final Duration defaultTtl;
SmartCache({
int maxSize = 1000,
CacheStrategy strategy = CacheStrategy.smart,
Duration defaultTtl = const Duration(hours: 1),
});
T? get(String key);
void put(String key, T value, {Duration? ttl, String? etag});
bool containsKey(String key);
T? remove(String key);
void clear();
CacheStats getStats();
List<String> getKeysNeedingRefresh();
Future<void> refreshKeys(List<String> keys, Future<T> Function(String key) refreshFunction);
Future<void> warmUp(Map<String, Future<T> Function()> warmUpFunctions);
}
```
### CacheStrategy
缓存策略枚举。
```dart
enum CacheStrategy {
lru, // 最近最少使用
lfu, // 最近最常使用
fifo, // 先进先出
ttl, // 基于时间的过期
smart, // 智能策略
}
```
## 配置选项
### 生成器配置
| 选项 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| `className` | String | 'ApiService' | 生成的主类名 |
| `splitByTags` | bool | true | 是否按标签分割 API |
| `generateModularApis` | bool | true | 生成模块化 API |
| `generateBaseResult` | bool | true | 生成基础响应类型 |
| `generatePagination` | bool | true | 生成分页支持 |
| `generateFileUpload` | bool | true | 生成文件上传支持 |
| `baseResultType` | String | 'BaseResult' | 基础响应类型名 |
| `pageResultType` | String | 'BasePageResult' | 分页响应类型名 |
### 解析器配置
| 选项 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| `enableParallelParsing` | bool | true | 启用并行解析 |
| `enableStreamParsing` | bool | false | 启用流式解析 |
| `enableCaching` | bool | true | 启用缓存 |
| `maxConcurrency` | int | 4 | 最大并发数 |
| `enablePerformanceStats` | bool | false | 启用性能统计 |
### 验证器配置
| 选项 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| `strictMode` | bool | false | 严格模式 |
| `includeWarnings` | bool | true | 包含警告 |
## 错误类型
### ErrorSeverity
```dart
enum ErrorSeverity {
info, // 信息
warning, // 警告
error, // 错误
critical, // 严重错误
}
```
### ErrorCategory
```dart
enum ErrorCategory {
syntax, // 语法错误
schema, // Schema 错误
reference, // 引用错误
validation, // 验证错误
compatibility, // 兼容性问题
performance, // 性能问题
security, // 安全问题
bestPractice, // 最佳实践
}
```
## 性能统计
### ParsePerformanceStats
解析性能统计。
```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;
double get pathsPerSecond;
double get schemasPerSecond;
double get bytesPerSecond;
}
```
### GenerationStats
生成性能统计。
```dart
class GenerationStats {
final int totalTasks;
final int completedTasks;
final int failedTasks;
final Duration totalTime;
final Duration averageTaskTime;
final int linesGenerated;
final int bytesGenerated;
final double parallelEfficiency;
double get successRate;
double get linesPerSecond;
double get bytesPerSecond;
}
```

311
example/advanced_usage.dart Normal file
View File

@ -0,0 +1,311 @@
/// 使
///
library;
import 'dart:convert';
import 'dart:io';
import 'package:swagger_generator_flutter/swagger_generator_flutter.dart';
void main() async {
print('🚀 高级使用示例');
print('=' * 50);
try {
await demonstrateHighPerformanceParsing();
await demonstrateOptimizedGeneration();
await demonstratePerformanceMonitoring();
await demonstrateCaching();
await demonstrateValidationAndErrorHandling();
print('\n🎉 高级使用示例完成!');
} catch (e, stackTrace) {
print('❌ 发生错误: $e');
print('堆栈跟踪: $stackTrace');
}
}
///
Future<void> demonstrateHighPerformanceParsing() async {
print('\n📊 高性能解析演示');
print('-' * 30);
//
final jsonString = await File('swagger.json').readAsString();
print('📖 文档大小: ${(jsonString.length / 1024).toStringAsFixed(2)}KB');
//
final parser = PerformanceParser(
config: ParseConfig(
enablePerformanceStats: true,
enableParallelParsing: false, //
enableCaching: true,
maxConcurrency: 8,
enableMemoryOptimization: true,
),
);
//
final stopwatch = Stopwatch()..start();
final document = await parser.parseDocument(jsonString);
stopwatch.stop();
//
print('✅ 解析完成');
print(' - 解析时间: ${stopwatch.elapsedMilliseconds}ms');
print(' - 路径数: ${document.paths.length}');
print(' - 模型数: ${document.models.length}');
print(' - 服务器数: ${document.servers.length}');
//
final stats = parser.lastStats;
if (stats != null) {
print('\n📈 性能统计:');
print(' - 总时间: ${stats.totalTime.inMilliseconds}ms');
print(' - 解析时间: ${stats.parseTime.inMilliseconds}ms');
print(' - 验证时间: ${stats.validationTime.inMilliseconds}ms');
print(' - 模型创建时间: ${stats.modelCreationTime.inMilliseconds}ms');
print(
' - 内存使用: ${(stats.memoryUsage / 1024 / 1024).toStringAsFixed(2)}MB');
print(' - 路径处理速度: ${stats.pathsPerSecond.toStringAsFixed(1)} paths/s');
print(' - 吞吐量: ${(stats.bytesPerSecond / 1024).toStringAsFixed(2)} KB/s');
}
//
final cacheStats = parser.getCacheStats();
print('\n🗄️ 缓存统计:');
print(' - 缓存大小: ${cacheStats['size']}');
print(' - 缓存键: ${(cacheStats['keys'] as List).length}');
}
///
Future<void> demonstrateOptimizedGeneration() async {
print('\n🔧 优化代码生成演示');
print('-' * 30);
//
final jsonString = await File('swagger.json').readAsString();
final document = SwaggerDocument.fromJson(jsonDecode(jsonString));
//
final generator = OptimizedRetrofitGenerator(
className: 'AdvancedApiService',
generateModularApis: true,
generateBaseResult: true,
generatePagination: true,
generateFileUpload: true,
baseResultType: 'ApiResult',
pageResultType: 'PagedResult',
);
//
final stopwatch = Stopwatch()..start();
final generatedCode = generator.generateFromDocument(document);
stopwatch.stop();
print('✅ 代码生成完成');
print(' - 生成时间: ${stopwatch.elapsedMilliseconds}ms');
print(' - 代码大小: ${(generatedCode.length / 1024).toStringAsFixed(2)}KB');
print(' - 代码行数: ${generatedCode.split('\n').length}');
//
final features = <String>[];
if (generatedCode.contains('class ApiResult')) features.add('基础响应类型');
if (generatedCode.contains('class PagedResult')) features.add('分页支持');
if (generatedCode.contains('MultipartFile')) features.add('文件上传');
if (generatedCode.contains('class ApiUtils')) features.add('工具类');
print(' - 生成特性: ${features.join(', ')}');
//
final outputFile = File('example/generated/advanced_api_service.dart');
await outputFile.writeAsString(generatedCode);
print(' - 保存位置: ${outputFile.path}');
}
///
Future<void> demonstratePerformanceMonitoring() async {
print('\n📊 性能监控演示');
print('-' * 30);
//
final jsonString = await File('swagger.json').readAsString();
final document = SwaggerDocument.fromJson(jsonDecode(jsonString));
//
final generator = PerformanceGenerator(
maxConcurrency: 4,
enableCaching: true,
enableIncremental: true,
enableParallel: true,
);
//
final generatedCode = await generator.generateFromDocument(document);
//
final stats = generator.getStats();
print('📈 生成性能统计:');
print(' - 总任务数: ${stats.totalTasks}');
print(' - 完成任务数: ${stats.completedTasks}');
print(' - 失败任务数: ${stats.failedTasks}');
print(' - 成功率: ${(stats.successRate * 100).toStringAsFixed(1)}%');
print(' - 总时间: ${stats.totalTime.inMilliseconds}ms');
print(' - 平均任务时间: ${stats.averageTaskTime.inMilliseconds}ms');
print(' - 生成行数: ${stats.linesGenerated}');
print(' - 生成字节数: ${stats.bytesGenerated}');
print(' - 并行效率: ${(stats.parallelEfficiency * 100).toStringAsFixed(1)}%');
print(' - 生成速度: ${stats.linesPerSecond.toStringAsFixed(1)} lines/s');
//
final cacheStats = generator.getCacheStats();
print('\n🗄️ 生成器缓存统计:');
print(' - 总请求: ${cacheStats.totalRequests}');
print(' - 缓存命中: ${cacheStats.hits}');
print(' - 缓存未命中: ${cacheStats.misses}');
print(' - 命中率: ${(cacheStats.hitRate * 100).toStringAsFixed(1)}%');
print(' - 缓存大小: ${cacheStats.size}/${cacheStats.maxSize}');
print(' - 平均访问时间: ${cacheStats.averageAccessTime.inMicroseconds}μs');
}
///
Future<void> demonstrateCaching() async {
print('\n🗄️ 缓存功能演示');
print('-' * 30);
//
final cache = SmartCache<String>(
maxSize: 100,
strategy: CacheStrategy.smart,
defaultTtl: Duration(minutes: 30),
);
//
cache.put('user:1', '{"id": 1, "name": "Alice"}');
cache.put('user:2', '{"id": 2, "name": "Bob"}');
cache.put('user:3', '{"id": 3, "name": "Charlie"}');
// 访
for (int i = 0; i < 10; i++) {
cache.get('user:1'); // 访
}
for (int i = 0; i < 5; i++) {
cache.get('user:2'); // 访
}
cache.get('user:3'); // 访
//
final stats = cache.getStats();
print('📊 缓存统计:');
print(' - 总请求: ${stats.totalRequests}');
print(' - 命中: ${stats.hits}');
print(' - 未命中: ${stats.misses}');
print(' - 命中率: ${(stats.hitRate * 100).toStringAsFixed(1)}%');
print(' - 缓存大小: ${stats.size}/${stats.maxSize}');
print(' - 填充率: ${(stats.fillRate * 100).toStringAsFixed(1)}%');
// 访
print('\n📈 访问统计:');
stats.keyAccessCounts.forEach((key, count) {
print(' - $key: $count 次访问');
});
//
print('\n🔥 缓存预热演示:');
await cache.warmUp({
'config:app': () async => '{"theme": "dark", "language": "zh"}',
'config:api': () async => '{"timeout": 30000, "retries": 3}',
});
print(' - 预热完成,缓存大小: ${cache.getStats().size}');
}
///
Future<void> demonstrateValidationAndErrorHandling() async {
print('\n✅ 验证和错误处理演示');
print('-' * 30);
//
final problematicDoc = {
'openapi': '3.0.3',
'info': {
'title': 'Problematic API',
'version': '1.0.0',
},
'paths': {
'/users/{id}': {
'get': {
'summary': 'Get user',
'responses': {
'200': {
'description': 'Success',
},
},
//
},
},
'invalid-path': {
//
'get': {
'summary': 'Invalid path',
'responses': {
'200': {
'description': 'Success',
},
},
},
},
},
};
final jsonString = jsonEncode(problematicDoc);
final document = SwaggerDocument.fromJson(jsonDecode(jsonString));
//
final validator = EnhancedValidator(
includeWarnings: true,
);
//
final isValid = validator.validateDocument(document);
print('📋 验证结果: ${isValid ? "通过" : "失败"}');
//
final errorStats = validator.errorReporter.getErrorStatistics();
print('\n📊 错误统计:');
errorStats.forEach((severity, count) {
print(' - ${severity.displayName}: $count');
});
//
final errors = validator.errorReporter.errors;
if (errors.isNotEmpty) {
print('\n❌ 详细错误:');
for (int i = 0; i < errors.length && i < 5; i++) {
final error = errors[i];
print(' ${i + 1}. ${error.severity.emoji} ${error.title}');
print(' 位置: ${error.location.jsonPath}');
print(' 描述: ${error.description}');
if (error.suggestions.isNotEmpty) {
print(' 建议: ${error.suggestions.first.description}');
}
print('');
}
}
//
final report = validator.errorReporter.generateReport(
includeStatistics: true,
groupByCategory: true,
);
//
final reportFile = File('example/generated/validation_report.txt');
await reportFile.writeAsString(report);
print('📄 错误报告已保存到: ${reportFile.path}');
// JSON
final jsonReport = validator.errorReporter.generateJsonReport();
final jsonReportFile = File('example/generated/validation_report.json');
await jsonReportFile.writeAsString(jsonReport);
print('📄 JSON 报告已保存到: ${jsonReportFile.path}');
}

239
example/basic_usage.dart Normal file
View File

@ -0,0 +1,239 @@
/// 使
/// 使 Swagger Generator Flutter API
library;
import 'dart:convert';
import 'dart:io';
import 'package:swagger_generator_flutter/swagger_generator_flutter.dart';
void main() async {
print('🚀 基础使用示例');
print('=' * 50);
try {
// 1. OpenAPI
print('📖 读取 OpenAPI 文档...');
final jsonString = await File('swagger.json').readAsString();
print('✅ 文档大小: ${(jsonString.length / 1024).toStringAsFixed(2)}KB');
// 2.
print('\n🔍 解析文档...');
final document = SwaggerDocument.fromJson(jsonDecode(jsonString));
print('✅ 解析完成');
print(' - 标题: ${document.title}');
print(' - 版本: ${document.version}');
print(' - 路径数: ${document.paths.length}');
print(' - 模型数: ${document.models.length}');
// 3.
print('\n✅ 验证文档...');
final validator = EnhancedValidator(
includeWarnings: true,
);
final isValid = validator.validateDocument(document);
final errors =
validator.errorReporter.getErrorsBySeverity(ErrorSeverity.error);
final warnings =
validator.errorReporter.getErrorsBySeverity(ErrorSeverity.warning);
print(' - 验证结果: ${isValid ? "通过" : "失败"}');
print(' - 错误数: ${errors.length}');
print(' - 警告数: ${warnings.length}');
if (errors.isNotEmpty) {
print('\n❌ 发现错误:');
for (final error in errors.take(3)) {
print(' - ${error.title}: ${error.description}');
}
}
// 4. API
print('\n🔧 生成基础 API 代码...');
final generator = RetrofitApiGenerator(
className: 'BasicApiService',
splitByTags: true, // 使
useRetrofit: true,
generateModels: true,
);
final generatedCode = generator.generateFromDocument(document);
print('✅ 代码生成完成');
print(' - 代码大小: ${(generatedCode.length / 1024).toStringAsFixed(2)}KB');
print(' - 代码行数: ${generatedCode.split('\n').length}');
// 5.
print('\n💾 保存生成的代码...');
final outputDir = Directory('example/generated');
if (!outputDir.existsSync()) {
outputDir.createSync(recursive: true);
}
final outputFile = File('example/generated/basic_api_service.dart');
await outputFile.writeAsString(generatedCode);
print('✅ 代码已保存到: ${outputFile.path}');
// 6.
print('\n📄 生成的代码片段:');
print('-' * 30);
final lines = generatedCode.split('\n');
for (int i = 0; i < 20 && i < lines.length; i++) {
print('${(i + 1).toString().padLeft(2)}: ${lines[i]}');
}
if (lines.length > 20) {
print('... (还有 ${lines.length - 20} 行)');
}
print('\n🎉 基础使用示例完成!');
} catch (e, stackTrace) {
print('❌ 发生错误: $e');
print('堆栈跟踪: $stackTrace');
}
}
/// OpenAPI
Future<void> createSampleDocument() async {
final sampleDoc = {
'openapi': '3.0.3',
'info': {
'title': 'Sample API',
'version': '1.0.0',
'description': 'A sample API for demonstration',
},
'servers': [
{
'url': 'https://api.example.com',
'description': 'Production server',
},
],
'paths': {
'/users': {
'get': {
'summary': 'Get all users',
'operationId': 'getUsers',
'tags': ['users'],
'parameters': [
{
'name': 'page',
'in': 'query',
'required': false,
'schema': {'type': 'integer', 'default': 1},
'description': 'Page number',
},
],
'responses': {
'200': {
'description': 'Success',
'content': {
'application/json': {
'schema': {
'type': 'array',
'items': {
'\$ref': '#/components/schemas/User',
},
},
},
},
},
},
},
'post': {
'summary': 'Create user',
'operationId': 'createUser',
'tags': ['users'],
'requestBody': {
'required': true,
'content': {
'application/json': {
'schema': {
'\$ref': '#/components/schemas/CreateUserRequest',
},
},
},
},
'responses': {
'201': {
'description': 'User created',
'content': {
'application/json': {
'schema': {
'\$ref': '#/components/schemas/User',
},
},
},
},
},
},
},
'/users/{id}': {
'get': {
'summary': 'Get user by ID',
'operationId': 'getUserById',
'tags': ['users'],
'parameters': [
{
'name': 'id',
'in': 'path',
'required': true,
'schema': {'type': 'integer'},
'description': 'User ID',
},
],
'responses': {
'200': {
'description': 'User found',
'content': {
'application/json': {
'schema': {
'\$ref': '#/components/schemas/User',
},
},
},
},
'404': {
'description': 'User not found',
},
},
},
},
},
'components': {
'schemas': {
'User': {
'type': 'object',
'properties': {
'id': {'type': 'integer', 'format': 'int64'},
'name': {'type': 'string'},
'email': {'type': 'string', 'format': 'email'},
'createdAt': {'type': 'string', 'format': 'date-time'},
},
'required': ['id', 'name', 'email'],
},
'CreateUserRequest': {
'type': 'object',
'properties': {
'name': {'type': 'string'},
'email': {'type': 'string', 'format': 'email'},
},
'required': ['name', 'email'],
},
},
},
};
final jsonString = const JsonEncoder.withIndent(' ').convert(sampleDoc);
await File('example/sample_swagger.json').writeAsString(jsonString);
print('✅ 示例文档已创建: example/sample_swagger.json');
}
///
///
/// 使:
/// ```bash
/// dart run example/basic_usage.dart
/// ```
///
/// :
/// ```dart
/// await createSampleDocument();
/// ```

View File

@ -0,0 +1,424 @@
///
/// Flutter 使 Swagger Generator
library;
import 'dart:io';
import 'package:swagger_generator_flutter/swagger_generator_flutter.dart';
///
class ProjectConfig {
static const String projectName = 'MyFlutterApp';
static const String apiServiceName = 'ApiService';
static const String outputDir = 'lib/api/generated';
static const String swaggerFile = 'swagger.json';
// API
static const String baseUrl = 'https://api.myapp.com';
static const String apiVersion = 'v1';
//
static const bool enableModularApis = true;
static const bool enableBaseResult = true;
static const bool enablePagination = true;
static const bool enableFileUpload = true;
static const bool enableCaching = true;
static const bool enableValidation = true;
}
void main() async {
print('🚀 ${ProjectConfig.projectName} API 代码生成');
print('=' * 60);
final generator = ProjectApiGenerator();
try {
await generator.generateProjectApi();
print('\n🎉 API 代码生成完成!');
} catch (e, stackTrace) {
print('❌ 生成失败: $e');
print('堆栈跟踪: $stackTrace');
exit(1);
}
}
/// API
class ProjectApiGenerator {
late PerformanceParser parser;
late OptimizedRetrofitGenerator generator;
late EnhancedValidator validator;
ProjectApiGenerator() {
_initializeComponents();
}
///
void _initializeComponents() {
//
parser = PerformanceParser(
config: ParseConfig(
enablePerformanceStats: true,
enableParallelParsing: false, //
enableCaching: ProjectConfig.enableCaching,
maxConcurrency: 4,
enableMemoryOptimization: true,
),
);
//
generator = OptimizedRetrofitGenerator(
className: ProjectConfig.apiServiceName,
generateModularApis: ProjectConfig.enableModularApis,
generateBaseResult: ProjectConfig.enableBaseResult,
generatePagination: ProjectConfig.enablePagination,
generateFileUpload: ProjectConfig.enableFileUpload,
baseResultType: 'ApiResult',
pageResultType: 'PagedApiResult',
);
//
validator = EnhancedValidator(
includeWarnings: true,
);
}
/// API
Future<void> generateProjectApi() async {
// 1.
await _checkEnvironment();
// 2.
final document = await _parseSwaggerDocument();
// 3.
await _validateDocument(document);
// 4. API
await _generateApiCode(document);
// 5.
await _generateConfigFiles();
// 6. 使
await _generateUsageExamples();
// 7.
await _generateDocumentation(document);
// 8.
_showSummary();
}
///
Future<void> _checkEnvironment() async {
print('🔍 检查环境...');
// swagger.json
final swaggerFile = File(ProjectConfig.swaggerFile);
if (!swaggerFile.existsSync()) {
throw Exception('找不到 ${ProjectConfig.swaggerFile} 文件');
}
//
final outputDir = Directory(ProjectConfig.outputDir);
if (!outputDir.existsSync()) {
print('📁 创建输出目录: ${ProjectConfig.outputDir}');
outputDir.createSync(recursive: true);
}
//
final pubspecFile = File('pubspec.yaml');
if (pubspecFile.existsSync()) {
final content = await pubspecFile.readAsString();
final requiredDeps = ['dio', 'retrofit', 'json_annotation'];
final missingDeps = <String>[];
for (final dep in requiredDeps) {
if (!content.contains(dep)) {
missingDeps.add(dep);
}
}
if (missingDeps.isNotEmpty) {
print('⚠️ 缺少依赖: ${missingDeps.join(', ')}');
print(' 请在 pubspec.yaml 中添加这些依赖');
}
}
print('✅ 环境检查完成');
}
/// Swagger
Future<SwaggerDocument> _parseSwaggerDocument() async {
print('\n📖 解析 Swagger 文档...');
final jsonString = await File(ProjectConfig.swaggerFile).readAsString();
print(' - 文档大小: ${(jsonString.length / 1024).toStringAsFixed(2)}KB');
final stopwatch = Stopwatch()..start();
final document = await parser.parseDocument(jsonString);
stopwatch.stop();
print('✅ 解析完成');
print(' - 解析时间: ${stopwatch.elapsedMilliseconds}ms');
print(' - API 标题: ${document.title}');
print(' - API 版本: ${document.version}');
print(' - 路径数量: ${document.paths.length}');
print(' - 模型数量: ${document.models.length}');
print(' - 服务器数量: ${document.servers.length}');
//
final stats = parser.lastStats;
if (stats != null) {
print(' - 处理速度: ${stats.pathsPerSecond.toStringAsFixed(1)} paths/s');
print(
' - 吞吐量: ${(stats.bytesPerSecond / 1024).toStringAsFixed(2)} KB/s');
}
return document;
}
///
Future<void> _validateDocument(SwaggerDocument document) async {
if (!ProjectConfig.enableValidation) {
print('\n⏭️ 跳过文档验证');
return;
}
print('\n✅ 验证文档...');
final isValid = validator.validateDocument(document);
final errors =
validator.errorReporter.getErrorsBySeverity(ErrorSeverity.error);
final warnings =
validator.errorReporter.getErrorsBySeverity(ErrorSeverity.warning);
final criticalErrors =
validator.errorReporter.getErrorsBySeverity(ErrorSeverity.critical);
print(' - 验证结果: ${isValid ? "✅ 通过" : "❌ 失败"}');
print(' - 严重错误: ${criticalErrors.length}');
print(' - 错误: ${errors.length}');
print(' - 警告: ${warnings.length}');
if (criticalErrors.isNotEmpty) {
print('\n🚨 严重错误:');
for (final error in criticalErrors.take(3)) {
print(' - ${error.title}: ${error.description}');
}
throw Exception('文档包含严重错误,无法继续生成');
}
if (errors.isNotEmpty) {
print('\n❌ 错误:');
for (final error in errors.take(3)) {
print(' - ${error.title}: ${error.description}');
}
}
if (warnings.isNotEmpty) {
print('\n⚠️ 警告:');
for (final warning in warnings.take(3)) {
print(' - ${warning.title}: ${warning.description}');
}
}
//
final report = validator.errorReporter.generateReport();
final reportFile = File('${ProjectConfig.outputDir}/validation_report.txt');
await reportFile.writeAsString(report);
print(' - 验证报告: ${reportFile.path}');
}
/// API
Future<void> _generateApiCode(SwaggerDocument document) async {
print('\n🔧 生成 API 代码...');
final stopwatch = Stopwatch()..start();
final generatedCode = generator.generateFromDocument(document);
stopwatch.stop();
print('✅ 代码生成完成');
print(' - 生成时间: ${stopwatch.elapsedMilliseconds}ms');
print(' - 代码大小: ${(generatedCode.length / 1024).toStringAsFixed(2)}KB');
print(' - 代码行数: ${generatedCode.split('\n').length}');
// API
final apiFile = File(
'${ProjectConfig.outputDir}/${ProjectConfig.apiServiceName.toLowerCase()}.dart');
await apiFile.writeAsString(generatedCode);
print(' - API 文件: ${apiFile.path}');
//
final features = <String>[];
if (generatedCode.contains('class ApiResult')) features.add('基础响应类型');
if (generatedCode.contains('class PagedApiResult')) features.add('分页支持');
if (generatedCode.contains('MultipartFile')) features.add('文件上传');
if (generatedCode.contains('class ApiUtils')) features.add('工具类');
if (features.isNotEmpty) {
print(' - 生成特性: ${features.join(', ')}');
}
}
///
Future<void> _generateConfigFiles() async {
print('\n⚙️ 生成配置文件...');
// API
final apiConfig = '''
/// API
///
class ApiConfig {
static const String baseUrl = '${ProjectConfig.baseUrl}';
static const String apiVersion = '${ProjectConfig.apiVersion}';
static const int connectTimeout = 30000;
static const int receiveTimeout = 30000;
static const int sendTimeout = 30000;
//
static const Map<String, String> defaultHeaders = {
'Content-Type': 'application/json',
'Accept': 'application/json',
};
//
static const bool debugMode = true;
}
''';
final configFile = File('${ProjectConfig.outputDir}/api_config.dart');
await configFile.writeAsString(apiConfig);
print(' - API 配置: ${configFile.path}');
// Dio
final dioConfig = '''
/// Dio
import 'package:dio/dio.dart';
import 'api_config.dart';
class DioConfig {
static Dio createDio() {
final dio = Dio(BaseOptions(
baseUrl: ApiConfig.baseUrl,
connectTimeout: Duration(milliseconds: ApiConfig.connectTimeout),
receiveTimeout: Duration(milliseconds: ApiConfig.receiveTimeout),
sendTimeout: Duration(milliseconds: ApiConfig.sendTimeout),
headers: ApiConfig.defaultHeaders,
));
if (ApiConfig.debugMode) {
dio.interceptors.add(LogInterceptor(
requestBody: true,
responseBody: true,
requestHeader: true,
responseHeader: false,
));
}
return dio;
}
}
''';
final dioConfigFile = File('${ProjectConfig.outputDir}/dio_config.dart');
await dioConfigFile.writeAsString(dioConfig);
print(' - Dio 配置: ${dioConfigFile.path}');
}
/// 使
Future<void> _generateUsageExamples() async {
print('\n📝 生成使用示例...');
final example = '''
/// API 使
/// Flutter 使 API
import 'package:dio/dio.dart';
import 'generated/${ProjectConfig.apiServiceName.toLowerCase()}.dart';
import 'dio_config.dart';
class ApiExample {
late final ${ProjectConfig.apiServiceName} apiService;
ApiExample() {
final dio = DioConfig.createDio();
apiService = ${ProjectConfig.apiServiceName}(dio);
}
///
Future<void> getUsersExample() async {
try {
final response = await apiService.getUsers();
print('用户列表: \$response');
} catch (e) {
print('获取用户列表失败: \$e');
}
}
///
Future<void> createUserExample() async {
try {
final userData = {
'name': 'John Doe',
'email': 'john@example.com',
};
final response = await apiService.createUser(userData);
print('用户创建成功: \$response');
} catch (e) {
print('创建用户失败: \$e');
}
}
}
''';
final exampleFile = File('${ProjectConfig.outputDir}/api_example.dart');
await exampleFile.writeAsString(example);
print(' - 使用示例: ${exampleFile.path}');
}
///
Future<void> _generateDocumentation(SwaggerDocument document) async {
print('\n📚 生成文档...');
final docs = StringBuffer();
docs.writeln('# ${document.title} API 文档');
docs.writeln();
docs.writeln('版本: ${document.version}');
docs.writeln('描述: ${document.description}');
docs.writeln();
docs.writeln('## 服务器');
for (final server in document.servers) {
docs.writeln('- ${server.url}: ${server.description}');
}
docs.writeln();
docs.writeln('## API 端点');
docs.writeln('总计: ${document.paths.length} 个端点');
docs.writeln();
docs.writeln('## 数据模型');
docs.writeln('总计: ${document.models.length} 个模型');
docs.writeln();
docs.writeln('## 使用方法');
docs.writeln('请参考 `api_example.dart` 文件中的示例代码。');
final docsFile = File('${ProjectConfig.outputDir}/README.md');
await docsFile.writeAsString(docs.toString());
print(' - API 文档: ${docsFile.path}');
}
///
void _showSummary() {
print('\n📊 生成总结');
print('-' * 40);
print('✅ 项目: ${ProjectConfig.projectName}');
print('✅ API 服务: ${ProjectConfig.apiServiceName}');
print('✅ 输出目录: ${ProjectConfig.outputDir}');
print('✅ 模块化 API: ${ProjectConfig.enableModularApis ? "启用" : "禁用"}');
print('✅ 基础响应类型: ${ProjectConfig.enableBaseResult ? "启用" : "禁用"}');
print('✅ 分页支持: ${ProjectConfig.enablePagination ? "启用" : "禁用"}');
print('✅ 文件上传: ${ProjectConfig.enableFileUpload ? "启用" : "禁用"}');
print('\n🎯 下一步:');
print('1. 运行 `flutter packages pub run build_runner build` 生成序列化代码');
print('2. 在项目中导入生成的 API 文件');
print('3. 参考 `api_example.dart` 中的使用示例');
print('4. 查看 `README.md` 了解 API 详情');
}
}

View File

@ -0,0 +1,311 @@
/// Dio + Retrofit 使
/// 使 API
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:retrofit/retrofit.dart';
//
part 'dio_retrofit_usage.g.dart';
/// API
@RestApi(baseUrl: 'https://api.example.com/v1')
abstract class ApiService {
factory ApiService(Dio dio, {String? baseUrl}) = _ApiService;
///
@GET('/users/{id}')
Future<UserResponse> getUser(@Path('id') int id);
///
@POST('/users')
Future<UserResponse> createUser(@Body() CreateUserRequest request);
///
@POST('/users/{id}/avatar')
@MultiPart()
Future<UploadResponse> uploadAvatar(
@Path('id') int id,
@Part() MultipartFile avatar,
);
///
@GET('/users')
Future<UserListResponse> getUsers(
@Query('page') int page,
@Query('size') int size,
@Query('search') String? search,
);
///
@GET('/files/{id}')
@DioResponseType(ResponseType.bytes)
Future<List<int>> downloadFile(@Path('id') String id);
}
///
class UserResponse {
final int id;
final String name;
final String email;
final String? avatar;
UserResponse({
required this.id,
required this.name,
required this.email,
this.avatar,
});
factory UserResponse.fromJson(Map<String, dynamic> json) => UserResponse(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
avatar: json['avatar'] as String?,
);
}
///
class CreateUserRequest {
final String name;
final String email;
final String password;
CreateUserRequest({
required this.name,
required this.email,
required this.password,
});
Map<String, dynamic> toJson() => {
'name': name,
'email': email,
'password': password,
};
}
///
class UserListResponse {
final List<UserResponse> users;
final int total;
final int page;
final int size;
UserListResponse({
required this.users,
required this.total,
required this.page,
required this.size,
});
factory UserListResponse.fromJson(Map<String, dynamic> json) =>
UserListResponse(
users: (json['users'] as List)
.map((e) => UserResponse.fromJson(e as Map<String, dynamic>))
.toList(),
total: json['total'] as int,
page: json['page'] as int,
size: json['size'] as int,
);
}
///
class UploadResponse {
final String url;
final String filename;
final int size;
UploadResponse({
required this.url,
required this.filename,
required this.size,
});
factory UploadResponse.fromJson(Map<String, dynamic> json) => UploadResponse(
url: json['url'] as String,
filename: json['filename'] as String,
size: json['size'] as int,
);
}
/// API 使
class ApiClient {
late final Dio _dio;
late final ApiService _apiService;
ApiClient({String? baseUrl}) {
_dio = Dio();
_setupDio();
_apiService = ApiService(_dio, baseUrl: baseUrl);
}
/// Dio
void _setupDio() {
//
_dio.options.connectTimeout = const Duration(seconds: 30);
_dio.options.receiveTimeout = const Duration(seconds: 30);
_dio.options.sendTimeout = const Duration(seconds: 30);
//
_dio.interceptors.addAll([
//
LogInterceptor(
requestBody: true,
responseBody: true,
logPrint: (obj) => print(obj),
),
//
BearerTokenInterceptor(token: 'your-auth-token'),
// API Key
ApiKeyInterceptor(
apiKey: 'your-api-key',
location: ApiKeyLocation.header,
paramName: 'X-API-Key',
),
//
ErrorHandlerInterceptor(),
]);
}
///
Future<UserResponse> getUser(int id) async {
try {
return await _apiService.getUser(id);
} catch (e) {
throw _handleError(e);
}
}
///
Future<UserResponse> createUser({
required String name,
required String email,
required String password,
}) async {
try {
final request = CreateUserRequest(
name: name,
email: email,
password: password,
);
return await _apiService.createUser(request);
} catch (e) {
throw _handleError(e);
}
}
///
Future<UploadResponse> uploadAvatar(int userId, String filePath) async {
try {
final file = await FileUploadHandler.createImageFile(filePath: filePath);
return await _apiService.uploadAvatar(userId, file);
} catch (e) {
throw _handleError(e);
}
}
///
Future<UserListResponse> getUsers({
int page = 1,
int size = 20,
String? search,
}) async {
try {
return await _apiService.getUsers(page, size, search);
} catch (e) {
throw _handleError(e);
}
}
///
Future<void> downloadFile(String fileId, String savePath) async {
try {
final bytes = await _apiService.downloadFile(fileId);
final file = File(savePath);
await file.writeAsBytes(bytes);
} catch (e) {
throw _handleError(e);
}
}
///
Exception _handleError(dynamic error) {
if (error is DioException) {
switch (error.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return TimeoutException('请求超时,请检查网络连接');
case DioExceptionType.badResponse:
final statusCode = error.response?.statusCode;
final message = error.response?.data?['message'] ?? '服务器错误';
return HttpException('HTTP $statusCode: $message');
case DioExceptionType.cancel:
return Exception('请求已取消');
case DioExceptionType.connectionError:
return Exception('网络连接错误,请检查网络设置');
default:
return Exception('未知错误: ${error.message}');
}
}
return Exception('未知错误: $error');
}
///
void dispose() {
_dio.close();
}
}
/// 使
void main() async {
final apiClient = ApiClient(baseUrl: 'https://api.example.com/v1');
try {
//
final user = await apiClient.getUser(1);
print('用户信息: ${user.name} (${user.email})');
//
final newUser = await apiClient.createUser(
name: 'John Doe',
email: 'john@example.com',
password: 'password123',
);
print('创建用户成功: ${newUser.id}');
//
final userList = await apiClient.getUsers(page: 1, size: 10);
print('用户总数: ${userList.total}');
//
// final uploadResult = await apiClient.uploadAvatar(1, '/path/to/avatar.jpg');
// print('头像上传成功: ${uploadResult.url}');
//
// await apiClient.downloadFile('file-id', '/path/to/save/file.pdf');
// print('文件下载完成');
} catch (e) {
print('API 调用失败: $e');
} finally {
apiClient.dispose();
}
}
///
class TimeoutException implements Exception {
final String message;
TimeoutException(this.message);
@override
String toString() => 'TimeoutException: $message';
}
class HttpException implements Exception {
final String message;
HttpException(this.message);
@override
String toString() => 'HttpException: $message';
}

View File

@ -0,0 +1,178 @@
//
import 'package:dio/dio.dart';
import 'package:json_annotation/json_annotation.dart';
//
import 'package:path/path.dart' as path;
//
part 'advanced_api_service_api.g.dart';
///
@JsonSerializable(genericArgumentFactories: true)
class ApiResult<T> {
///
final int code;
///
final String message;
///
final T? data;
///
bool get isSuccess => code == 200;
const ApiResult({
required this.code,
required this.message,
this.data,
});
factory ApiResult.fromJson(
Map<String, dynamic> json,
T Function(Object? json) fromJsonT,
) =>
_$ApiResultFromJson(json, fromJsonT);
Map<String, dynamic> toJson(Object Function(T value) toJsonT) =>
_$ApiResultToJson(this, toJsonT);
}
///
@JsonSerializable()
class BasePageParameter {
/// 1
final int page;
///
final int size;
const BasePageParameter({
this.page = 1,
this.size = 20,
});
factory BasePageParameter.fromJson(Map<String, dynamic> json) =>
_$BasePageParameterFromJson(json);
Map<String, dynamic> toJson() => _$BasePageParameterToJson(this);
}
///
@JsonSerializable(genericArgumentFactories: true)
class PagedResult<T> {
///
final List<T> list;
///
final int total;
///
final int page;
///
final int size;
///
int get totalPages => (total / size).ceil();
///
bool get hasNext => page < totalPages;
///
bool get hasPrevious => page > 1;
const PagedResult({
required this.list,
required this.total,
required this.page,
required this.size,
});
factory PagedResult.fromJson(
Map<String, dynamic> json,
T Function(Object? json) fromJsonT,
) =>
_$PagedResultFromJson(json, fromJsonT);
Map<String, dynamic> toJson(Object Function(T value) toJsonT) =>
_$PagedResultToJson(this, toJsonT);
}
///
@JsonSerializable()
class FileUploadRequest {
///
@JsonKey(includeFromJson: false, includeToJson: false)
final MultipartFile file;
///
final String? filename;
///
final String? contentType;
const FileUploadRequest({
required this.file,
this.filename,
this.contentType,
});
factory FileUploadRequest.fromJson(Map<String, dynamic> json) =>
_$FileUploadRequestFromJson(json);
Map<String, dynamic> toJson() => _$FileUploadRequestToJson(this);
}
///
@JsonSerializable()
class FileUploadResult {
/// URL
final String url;
///
final String filename;
///
final int size;
///
final String? contentType;
const FileUploadResult({
required this.url,
required this.filename,
required this.size,
this.contentType,
});
factory FileUploadResult.fromJson(Map<String, dynamic> json) =>
_$FileUploadResultFromJson(json);
Map<String, dynamic> toJson() => _$FileUploadResultToJson(this);
}
/// API
/// API
class AdvancedApiService {
final Dio _dio;
AdvancedApiService(this._dio, {String? baseUrl});
}
/// API
class ApiUtils {
///
static Future<MultipartFile> createFileUpload(String filePath) async {
return MultipartFile.fromFile(
filePath,
filename: path.basename(filePath),
);
}
///
static BasePageParameter createPageParam({int page = 1, int size = 20}) {
return BasePageParameter(page: page, size: size);
}
}

View File

@ -0,0 +1,70 @@
// API - Tag API
// Swagger API :
// xy_swagger_generator by max
// Copyright (C) 2025 YuanXuan. All rights reserved.
import 'package:dio/dio.dart';
import 'package:learning_officer_oa/common/models/common/base_page_result.dart';
import 'package:learning_officer_oa/common/models/common/base_result.dart';
import 'api_error.dart';
import 'api_error_handler.dart';
/// API客户端类
/// API接口访
class BasicApiService {
final Dio _dio;
BasicApiService(this._dio, {String? baseUrl});
/// Dio实例
Dio get dio => _dio;
/// token
void setAuthToken(String token) {
_dio.options.headers['Authorization'] = 'Bearer $token';
}
/// token
void clearAuthToken() {
_dio.options.headers.remove('Authorization');
}
/// URL
void setBaseUrl(String baseUrl) {
_dio.options.baseUrl = baseUrl;
}
///
void addRequestInterceptor(Interceptor interceptor) {
_dio.interceptors.add(interceptor);
}
///
void addResponseInterceptor(Interceptor interceptor) {
_dio.interceptors.add(interceptor);
}
///
void addErrorInterceptor(Interceptor interceptor) {
_dio.interceptors.add(interceptor);
}
/// API调用
Future<T> callWithErrorHandling<T>(Future<T> Function() apiCall) async {
try {
return await apiCall();
} on DioException catch (e) {
final error = ApiErrorHandler.handleDioError(e);
throw error;
} catch (e) {
final error = ApiError(
type: ApiErrorType.unknown,
message: '未知错误: $e',
code: -1,
originalError: e,
);
throw error;
}
}
}

View File

@ -0,0 +1,79 @@
{
"timestamp": "2025-07-24T07:16:17.845544",
"summary": {
"total": 3,
"by_severity": {
"warning": 1,
"error": 1,
"info": 1
}
},
"errors": [
{
"id": "MISSING_INFO_DESCRIPTION",
"title": "Missing API Description",
"description": "API description helps users understand the purpose of your API.",
"severity": "warning",
"category": "bestPractice",
"location": {
"json_path": "info.description",
"line": null,
"column": null,
"snippet": null
},
"suggestions": [
{
"description": "Add a description explaining what your API does",
"code_example": "\"description\": \"This API provides user management functionality\"",
"documentation_url": null
}
],
"related_errors": [],
"timestamp": "2025-07-24T07:16:17.842816"
},
{
"id": "EMPTY_PATHS",
"title": "Empty Paths Object",
"description": "OpenAPI document must contain at least one path.",
"severity": "error",
"category": "validation",
"location": {
"json_path": "paths",
"line": null,
"column": null,
"snippet": null
},
"suggestions": [
{
"description": "Add at least one API endpoint",
"code_example": "\"/users\": { \"get\": { \"responses\": { \"200\": { \"description\": \"Success\" } } } }",
"documentation_url": null
}
],
"related_errors": [],
"timestamp": "2025-07-24T07:16:17.842917"
},
{
"id": "NO_OPERATION_TAGS",
"title": "No Operation Tags",
"description": "Consider using tags to organize your API operations.",
"severity": "info",
"category": "bestPractice",
"location": {
"json_path": "paths",
"line": null,
"column": null,
"snippet": null
},
"suggestions": [
{
"description": "Add tags to operations",
"code_example": "\"tags\": [\"users\"]",
"documentation_url": null
}
],
"related_errors": [],
"timestamp": "2025-07-24T07:16:17.843223"
}
]
}

View File

@ -0,0 +1,52 @@
📊 Error Summary
==================================================
⚠️ WARNING: 1
❌ ERROR: 1
INFO: 1
📂 Best Practice
------------------------------
⚠️ WARNING: Missing API Description
Category: Best Practice
Location: info.description
Description:
API description helps users understand the purpose of your API.
Suggestions:
1. Add a description explaining what your API does
Example:
"description": "This API provides user management functionality"
INFO: No Operation Tags
Category: Best Practice
Location: paths
Description:
Consider using tags to organize your API operations.
Suggestions:
1. Add tags to operations
Example:
"tags": ["users"]
📂 Validation Error
------------------------------
❌ ERROR: Empty Paths Object
Category: Validation Error
Location: paths
Description:
OpenAPI document must contain at least one path.
Suggestions:
1. Add at least one API endpoint
Example:
"/users": { "get": { "responses": { "200": { "description": "Success" } } } }

240
generator_config.yaml Normal file
View File

@ -0,0 +1,240 @@
# Augment 代码生成器配置文件
# 基于 OpenAPI 3.0 标准的配置规范
# 基本配置
generator:
name: "xy_swagger_generator"
version: "2.0"
author: "max"
copyright: "Copyright (C) 2025 YuanXuan. All rights reserved."
# 输入配置
input:
# Swagger 文档源
swagger_url: "http://localhost:5000/swagger/v1/swagger.json"
swagger_file: "./swagger.json"
# 验证配置
validate_schema: true
strict_mode: true
# 输出配置
output:
# 输出目录
base_dir: "./generator"
api_dir: "./generator/api"
models_dir: "./generator/api_models"
# 文件命名
api_file_suffix: "_api.dart"
model_file_suffix: ".dart"
# 是否按 tag 分组
split_by_tags: true
# 代码生成配置
generation:
# API 接口配置
api:
enabled: true
use_retrofit: true
use_dio: true
parser: "JsonSerializable"
# 基础类型配置
base_result_type: "BaseResult"
base_page_result_type: "BasePageResult"
base_result_import: "package:learning_officer_oa/common/models/common/base_result.dart"
base_page_result_import: "package:learning_officer_oa/common/models/common/base_page_result.dart"
# 方法命名
method_naming: "camelCase" # camelCase, snake_case
# 数据模型配置
models:
enabled: true
use_json_serializable: true
# JsonSerializable 配置
json_serializable:
checked: true
include_if_null: false
explicit_to_json: true
# 类命名
class_naming: "PascalCase" # PascalCase, snake_case
field_naming: "camelCase" # camelCase, snake_case
# 构造函数配置
use_const_constructor: true
required_for_non_nullable: true
# 类型映射配置
type_mapping:
# OpenAPI -> Dart 类型映射
string: "String"
integer: "int"
number: "double"
boolean: "bool"
array: "List"
object: "Map<String, dynamic>"
# 特殊类型处理
date: "DateTime"
date-time: "DateTime"
binary: "Uint8List"
# 自定义类型映射
custom_types:
# 示例:特定格式的字符串映射到自定义类型
# "uuid": "Uuid"
# "email": "EmailAddress"
# 导入管理配置
imports:
# 按需导入
on_demand: true
# 自动排序
auto_sort: true
# 分组导入
group_imports: true
# 标准库导入
dart_imports:
- "dart:convert"
- "dart:typed_data"
# 第三方库导入
package_imports:
- "package:dio/dio.dart"
- "package:retrofit/retrofit.dart"
- "package:json_annotation/json_annotation.dart"
# 验证配置
validation:
# 严格模式
strict_mode: true
# 检查项
checks:
- "schema_exists" # 检查 schema 是否存在
- "ref_resolution" # 检查 $ref 引用
- "type_consistency" # 检查类型一致性
- "nullable_correctness" # 检查可空性正确性
- "required_fields" # 检查必需字段
# 错误处理
on_error: "warn" # fail, warn, ignore
# 警告处理
on_warning: "log" # fail, log, ignore
# 优化配置
optimization:
# 代码优化
remove_unused_imports: true
optimize_imports: true
# 性能优化
cache_schemas: true
parallel_generation: false
# 内存优化
lazy_loading: true
# 调试配置
debug:
# 详细日志
verbose: false
# 调试输出
debug_output: false
# 性能监控
performance_monitoring: false
# 生成统计
generation_stats: true
# 兼容性配置
compatibility:
# Dart 版本
dart_version: ">=3.0.0"
# Flutter 版本
flutter_version: ">=3.10.0"
# 依赖版本
dependencies:
dio: "^5.0.0"
retrofit: "^4.0.0"
json_annotation: "^4.8.0"
dev_dependencies:
build_runner: "^2.3.0"
retrofit_generator: "^8.0.0"
json_serializable: "^6.6.0"
# 自定义配置
custom:
# 项目特定配置
project_name: "learning_officer_oa"
# 特殊处理规则
special_handling:
# 健康检查接口返回 void
health_check_paths:
- "/health"
- "/healthcheck"
- "/api/health"
# 分页接口参数名
pagination_params:
- "page"
- "size"
- "limit"
- "offset"
- "pageIndex"
- "pageSize"
# 忽略的路径
ignored_paths:
- "/swagger"
- "/docs"
# 忽略的 tags
ignored_tags:
- "Internal"
- "Debug"
# 模板配置
templates:
# 文件头模板
file_header: |
// {fileName} {fileType}
// 基于 Swagger API 文档: {swaggerUrl}
// 由 {generatorName} by {author} 生成
// {copyright}
# API 类模板
api_class: |
/// {tagName} API 接口
/// 负责处理 {tagName} 相关的接口
@RestApi(parser: Parser.JsonSerializable)
abstract class {className} {
factory {className}(Dio dio, {String? baseUrl}) = _{className};
}
# 模型类模板
model_class: |
@JsonSerializable(checked: true, includeIfNull: false)
class {className} {
const {className}({constructorParams});
factory {className}.fromJson(Map<String, dynamic> json) =>
_${className}FromJson(json);
Map<String, dynamic> toJson() => _${className}ToJson(this);
}

View File

@ -52,7 +52,7 @@ class GenerateCommand extends BaseCommand {
const CommandOption(
name: 'split-by-tags',
shortName: 't',
description: '按tags分组生成多个API文件',
description: '按tags分组生成多个API文件(默认启用)',
type: OptionType.flag,
),
const CommandOption(
@ -91,8 +91,6 @@ class GenerateCommand extends BaseCommand {
//
final options = _parseGenerateOptions(parsedArgs);
final outputDir =
parsedArgs.getOption<String>('output-dir') ?? 'generator';
final fullOutputDir = FileUtils.getProjectRootGeneratorDir();
progress('输出目录: $fullOutputDir');
@ -135,18 +133,19 @@ class GenerateCommand extends BaseCommand {
}
}
// Retrofit API
// Retrofit API 使
if (options.generateApi) {
if (options.splitByTags) {
progress('正在按tags分组生成Retrofit风格API接口...');
final generator = RetrofitApiGenerator(
document,
className: 'ApiClient',
useRetrofit: true,
useDio: true,
splitByTags: true,
splitByTags: true, // 使
);
//
generator.document = document;
//
generator.ensureParameterEntitiesGenerated();
@ -185,43 +184,6 @@ class GenerateCommand extends BaseCommand {
generatedFiles++;
}
}
} else {
progress('正在生成Retrofit风格API接口...');
final generator = RetrofitApiGenerator(
document,
className: 'ApiClient',
useRetrofit: true,
useDio: true,
splitByTags: false,
);
//
generator.ensureParameterEntitiesGenerated();
final code = generator.generate();
final apiDir = '$fullOutputDir/api';
await FileUtils.ensureDirectoryExists(apiDir);
final filePath = '$apiDir/api.dart';
await FileUtils.writeFile(filePath, code);
success('Retrofit API接口已保存到: $filePath');
generatedFiles++;
//
final parameterEntityFiles = generator.generateParameterEntityFiles();
if (parameterEntityFiles.isNotEmpty) {
final modelsDir = '$fullOutputDir/api_models';
await FileUtils.ensureDirectoryExists(modelsDir);
for (final entry in parameterEntityFiles.entries) {
final filePath = '$modelsDir/${entry.key}';
await FileUtils.writeFile(filePath, entry.value);
success('参数实体类文件已保存到: $filePath');
generatedFiles++;
}
}
}
}
// index.dart
@ -279,7 +241,7 @@ class GenerateCommand extends BaseCommand {
? (args.getOption<bool>('api') ?? false)
: (args.getOption<bool>('all') ?? true),
useSimpleModels: args.getOption<bool>('simple') ?? false,
splitByTags: args.getOption<bool>('split-by-tags') ?? false,
splitByTags: args.getOption<bool>('split-by-tags') ?? true, //
);
}

View File

@ -3,10 +3,10 @@
class SwaggerConfig {
/// Swagger JSON文档的URL
static const String swaggerJsonUrl =
'https://quanxue-test-api.w.23544.com:8843/swagger/v1/swagger.json';
'http://192.168.2.7:17288/swagger/v1/swagger.json';
/// API URL
static const String baseUrl = 'https://quanxue-test-api.w.23544.com:8843';
static const String baseUrl = 'http://192.168.2.7:17288';
/// API版本
static const String apiVersion = '/api/v1';

View File

@ -0,0 +1,445 @@
///
///
library;
import 'dart:convert';
///
enum ErrorSeverity {
info,
warning,
error,
critical,
}
extension ErrorSeverityExtension on ErrorSeverity {
String get displayName {
switch (this) {
case ErrorSeverity.info:
return 'INFO';
case ErrorSeverity.warning:
return 'WARNING';
case ErrorSeverity.error:
return 'ERROR';
case ErrorSeverity.critical:
return 'CRITICAL';
}
}
String get emoji {
switch (this) {
case ErrorSeverity.info:
return '';
case ErrorSeverity.warning:
return '⚠️';
case ErrorSeverity.error:
return '';
case ErrorSeverity.critical:
return '🚨';
}
}
}
///
enum ErrorCategory {
syntax,
schema,
reference,
validation,
compatibility,
performance,
security,
bestPractice,
}
extension ErrorCategoryExtension on ErrorCategory {
String get displayName {
switch (this) {
case ErrorCategory.syntax:
return 'Syntax Error';
case ErrorCategory.schema:
return 'Schema Error';
case ErrorCategory.reference:
return 'Reference Error';
case ErrorCategory.validation:
return 'Validation Error';
case ErrorCategory.compatibility:
return 'Compatibility Issue';
case ErrorCategory.performance:
return 'Performance Issue';
case ErrorCategory.security:
return 'Security Issue';
case ErrorCategory.bestPractice:
return 'Best Practice';
}
}
}
///
class ErrorLocation {
/// JSON "paths./users.get.responses.200"
final String jsonPath;
///
final int? line;
///
final int? column;
///
final int? offset;
/// JSON
final String? snippet;
const ErrorLocation({
required this.jsonPath,
this.line,
this.column,
this.offset,
this.snippet,
});
@override
String toString() {
final buffer = StringBuffer();
buffer.write(jsonPath);
if (line != null) {
buffer.write(' (line $line');
if (column != null) {
buffer.write(', column $column');
}
buffer.write(')');
}
return buffer.toString();
}
}
///
class FixSuggestion {
///
final String description;
///
final String? codeExample;
///
final String? documentationUrl;
///
final String Function(String original)? autoFix;
const FixSuggestion({
required this.description,
this.codeExample,
this.documentationUrl,
this.autoFix,
});
}
///
class DetailedError {
/// ID
final String id;
///
final String title;
///
final String description;
///
final ErrorSeverity severity;
///
final ErrorCategory category;
///
final ErrorLocation location;
///
final List<FixSuggestion> suggestions;
///
final List<String> relatedErrors;
///
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();
//
buffer.writeln('${severity.emoji} ${severity.displayName}: $title');
buffer.writeln('Category: ${category.displayName}');
buffer.writeln('Location: $location');
buffer.writeln();
//
buffer.writeln('Description:');
buffer.writeln(' $description');
buffer.writeln();
//
if (location.snippet != null) {
buffer.writeln('Code snippet:');
buffer.writeln(' ${location.snippet}');
buffer.writeln();
}
//
if (suggestions.isNotEmpty) {
buffer.writeln('Suggestions:');
for (int i = 0; i < suggestions.length; i++) {
final suggestion = suggestions[i];
buffer.writeln(' ${i + 1}. ${suggestion.description}');
if (suggestion.codeExample != null) {
buffer.writeln(' Example:');
buffer.writeln(' ${suggestion.codeExample}');
}
if (suggestion.documentationUrl != null) {
buffer.writeln(' See: ${suggestion.documentationUrl}');
}
buffer.writeln();
}
}
//
if (relatedErrors.isNotEmpty) {
buffer.writeln('Related errors: ${relatedErrors.join(', ')}');
}
return buffer.toString();
}
}
///
class ErrorReporter {
final List<DetailedError> _errors = [];
final Map<String, int> _errorCounts = {};
ErrorReporter();
///
void addError(DetailedError error) {
_errors.add(error);
_errorCounts[error.id] = (_errorCounts[error.id] ?? 0) + 1;
}
///
void reportError({
required String id,
required String title,
required String description,
required ErrorSeverity severity,
required ErrorCategory category,
required String jsonPath,
int? line,
int? column,
String? snippet,
List<FixSuggestion> suggestions = const [],
List<String> relatedErrors = const [],
}) {
final error = DetailedError(
id: id,
title: title,
description: description,
severity: severity,
category: category,
location: ErrorLocation(
jsonPath: jsonPath,
line: line,
column: column,
snippet: snippet,
),
suggestions: suggestions,
relatedErrors: relatedErrors,
);
addError(error);
}
///
List<DetailedError> get errors => List.unmodifiable(_errors);
///
List<DetailedError> getErrorsBySeverity(ErrorSeverity severity) {
return _errors.where((error) => error.severity == severity).toList();
}
///
List<DetailedError> getErrorsByCategory(ErrorCategory category) {
return _errors.where((error) => error.category == category).toList();
}
///
Map<ErrorSeverity, int> getErrorStatistics() {
final stats = <ErrorSeverity, int>{};
for (final error in _errors) {
stats[error.severity] = (stats[error.severity] ?? 0) + 1;
}
return stats;
}
///
bool get hasErrors => _errors.isNotEmpty;
///
bool get hasCriticalErrors =>
_errors.any((e) => e.severity == ErrorSeverity.critical);
///
bool get hasErrorsOrCritical => _errors.any((e) =>
e.severity == ErrorSeverity.error ||
e.severity == ErrorSeverity.critical);
///
void clear() {
_errors.clear();
_errorCounts.clear();
}
///
String generateReport({
bool includeStatistics = true,
bool groupByCategory = false,
ErrorSeverity? minSeverity,
}) {
final buffer = StringBuffer();
//
var filteredErrors = _errors;
if (minSeverity != null) {
final minLevel = minSeverity.index;
filteredErrors =
_errors.where((e) => e.severity.index >= minLevel).toList();
}
if (filteredErrors.isEmpty) {
buffer.writeln('✅ No errors found!');
return buffer.toString();
}
//
if (includeStatistics) {
buffer.writeln('📊 Error Summary');
buffer.writeln('=' * 50);
final stats = getErrorStatistics();
stats.forEach((severity, count) {
buffer.writeln('${severity.emoji} ${severity.displayName}: $count');
});
buffer.writeln();
}
//
if (groupByCategory) {
_generateReportByCategory(buffer, filteredErrors);
} else {
_generateReportByOrder(buffer, filteredErrors);
}
return buffer.toString();
}
///
void _generateReportByCategory(
StringBuffer buffer, List<DetailedError> errors) {
final errorsByCategory = <ErrorCategory, List<DetailedError>>{};
for (final error in errors) {
errorsByCategory.putIfAbsent(error.category, () => []).add(error);
}
errorsByCategory.forEach((category, categoryErrors) {
buffer.writeln('📂 ${category.displayName}');
buffer.writeln('-' * 30);
for (final error in categoryErrors) {
buffer.writeln(error.toString());
buffer.writeln();
}
});
}
///
void _generateReportByOrder(StringBuffer buffer, List<DetailedError> errors) {
buffer.writeln('🔍 Detailed Error Report');
buffer.writeln('=' * 50);
for (int i = 0; i < errors.length; i++) {
buffer.writeln('Error ${i + 1}/${errors.length}:');
buffer.writeln(errors[i].toString());
if (i < errors.length - 1) {
buffer.writeln('-' * 50);
}
}
}
/// JSON
String generateJsonReport() {
final report = {
'timestamp': DateTime.now().toIso8601String(),
'summary': {
'total': _errors.length,
'by_severity': getErrorStatistics().map((k, v) => MapEntry(k.name, v)),
},
'errors': _errors
.map((error) => {
'id': error.id,
'title': error.title,
'description': error.description,
'severity': error.severity.name,
'category': error.category.name,
'location': {
'json_path': error.location.jsonPath,
'line': error.location.line,
'column': error.location.column,
'snippet': error.location.snippet,
},
'suggestions': error.suggestions
.map((suggestion) => {
'description': suggestion.description,
'code_example': suggestion.codeExample,
'documentation_url': suggestion.documentationUrl,
})
.toList(),
'related_errors': error.relatedErrors,
'timestamp': error.timestamp.toIso8601String(),
})
.toList(),
};
return const JsonEncoder.withIndent(' ').convert(report);
}
///
Future<void> exportToFile(String filePath, {String format = 'text'}) async {
final content = switch (format.toLowerCase()) {
'json' => generateJsonReport(),
_ => generateReport(),
};
//
// await File(filePath).writeAsString(content);
// 使使
assert(content.isNotEmpty || content.isEmpty);
}
}

469
lib/core/error_rules.dart Normal file
View File

@ -0,0 +1,469 @@
/// OpenAPI
///
library;
import '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,
required this.severity,
required this.category,
required this.title,
required this.description,
this.suggestions = const [],
});
}
/// OpenAPI
class OpenApiErrorRules {
static const List<ErrorRule> rules = [
//
ErrorRule(
id: 'MISSING_OPENAPI_VERSION',
pattern: 'openapi',
severity: ErrorSeverity.critical,
category: ErrorCategory.syntax,
title: 'Missing OpenAPI Version',
description: 'OpenAPI document must specify the OpenAPI version.',
suggestions: [
FixSuggestion(
description: 'Add openapi field with version 3.0.x or 3.1.x',
codeExample: '"openapi": "3.0.3"',
documentationUrl:
'https://spec.openapis.org/oas/v3.0.3/#openapi-object',
),
],
),
ErrorRule(
id: 'INVALID_OPENAPI_VERSION',
pattern: 'openapi',
severity: ErrorSeverity.error,
category: ErrorCategory.compatibility,
title: 'Invalid OpenAPI Version',
description:
'OpenAPI version should be 3.0.x or 3.1.x for proper support.',
suggestions: [
FixSuggestion(
description: 'Use a supported OpenAPI version',
codeExample: '"openapi": "3.0.3"',
),
],
),
// Info
ErrorRule(
id: 'MISSING_INFO_TITLE',
pattern: 'info.title',
severity: ErrorSeverity.error,
category: ErrorCategory.validation,
title: 'Missing API Title',
description: 'API title is required in the info object.',
suggestions: [
FixSuggestion(
description: 'Add a descriptive title for your API',
codeExample: '"title": "My API"',
),
],
),
ErrorRule(
id: 'MISSING_INFO_VERSION',
pattern: 'info.version',
severity: ErrorSeverity.error,
category: ErrorCategory.validation,
title: 'Missing API Version',
description: 'API version is required in the info object.',
suggestions: [
FixSuggestion(
description: 'Add a version number using semantic versioning',
codeExample: '"version": "1.0.0"',
documentationUrl: 'https://semver.org/',
),
],
),
// Paths
ErrorRule(
id: 'EMPTY_PATHS',
pattern: 'paths',
severity: ErrorSeverity.error,
category: ErrorCategory.validation,
title: 'Empty Paths Object',
description: 'OpenAPI document must contain at least one path.',
suggestions: [
FixSuggestion(
description: 'Add at least one API endpoint',
codeExample:
'"/users": { "get": { "responses": { "200": { "description": "Success" } } } }',
),
],
),
ErrorRule(
id: 'INVALID_PATH_FORMAT',
pattern: 'paths.*',
severity: ErrorSeverity.error,
category: ErrorCategory.syntax,
title: 'Invalid Path Format',
description: 'Path must start with a forward slash.',
suggestions: [
FixSuggestion(
description: 'Ensure path starts with /',
codeExample: '"/users" instead of "users"',
),
],
),
ErrorRule(
id: 'MISSING_OPERATION_RESPONSES',
pattern: 'paths.*.*.responses',
severity: ErrorSeverity.error,
category: ErrorCategory.validation,
title: 'Missing Operation Responses',
description: 'Every operation must define at least one response.',
suggestions: [
FixSuggestion(
description: 'Add at least a default response',
codeExample: '"responses": { "200": { "description": "Success" } }',
),
],
),
//
ErrorRule(
id: 'PATH_PARAMETER_NOT_REQUIRED',
pattern: 'paths.*.*.parameters.*',
severity: ErrorSeverity.error,
category: ErrorCategory.validation,
title: 'Path Parameter Not Required',
description: 'Path parameters must be marked as required.',
suggestions: [
FixSuggestion(
description: 'Set required: true for path parameters',
codeExample: '"required": true',
),
],
),
ErrorRule(
id: 'MISSING_PARAMETER_NAME',
pattern: 'paths.*.*.parameters.*.name',
severity: ErrorSeverity.error,
category: ErrorCategory.validation,
title: 'Missing Parameter Name',
description: 'Parameter must have a name.',
suggestions: [
FixSuggestion(
description: 'Add a name for the parameter',
codeExample: '"name": "userId"',
),
],
),
// Schema
ErrorRule(
id: 'MISSING_SCHEMA_TYPE',
pattern: 'components.schemas.*.type',
severity: ErrorSeverity.warning,
category: ErrorCategory.schema,
title: 'Missing Schema Type',
description: 'Schema should specify a type for better code generation.',
suggestions: [
FixSuggestion(
description: 'Add a type field to the schema',
codeExample: '"type": "object"',
),
],
),
ErrorRule(
id: 'CIRCULAR_REFERENCE',
pattern: 'components.schemas.*',
severity: ErrorSeverity.warning,
category: ErrorCategory.schema,
title: 'Circular Reference Detected',
description: 'Circular references can cause issues in code generation.',
suggestions: [
FixSuggestion(
description:
'Consider using allOf or breaking the circular dependency',
codeExample:
'"allOf": [{ "\$ref": "#/components/schemas/BaseModel" }]',
),
],
),
//
ErrorRule(
id: 'MISSING_SECURITY_SCHEME_TYPE',
pattern: 'components.securitySchemes.*.type',
severity: ErrorSeverity.error,
category: ErrorCategory.security,
title: 'Missing Security Scheme Type',
description: 'Security scheme must specify a type.',
suggestions: [
FixSuggestion(
description: 'Add a type field (apiKey, http, oauth2, openIdConnect)',
codeExample: '"type": "apiKey"',
),
],
),
ErrorRule(
id: 'MISSING_API_KEY_NAME',
pattern: 'components.securitySchemes.*.name',
severity: ErrorSeverity.error,
category: ErrorCategory.security,
title: 'Missing API Key Name',
description: 'API Key security scheme must specify a parameter name.',
suggestions: [
FixSuggestion(
description: 'Add name field for API key parameter',
codeExample: '"name": "X-API-Key"',
),
],
),
ErrorRule(
id: 'MISSING_API_KEY_LOCATION',
pattern: 'components.securitySchemes.*.in',
severity: ErrorSeverity.error,
category: ErrorCategory.security,
title: 'Missing API Key Location',
description:
'API Key security scheme must specify where the key is located.',
suggestions: [
FixSuggestion(
description: 'Add in field (query, header, cookie)',
codeExample: '"in": "header"',
),
],
),
//
ErrorRule(
id: 'MISSING_RESPONSE_DESCRIPTION',
pattern: 'paths.*.*.responses.*.description',
severity: ErrorSeverity.warning,
category: ErrorCategory.bestPractice,
title: 'Missing Response Description',
description: 'Response should have a description.',
suggestions: [
FixSuggestion(
description: 'Add a description for the response',
codeExample: '"description": "Successful operation"',
),
],
),
ErrorRule(
id: 'NO_SUCCESS_RESPONSE',
pattern: 'paths.*.*.responses',
severity: ErrorSeverity.warning,
category: ErrorCategory.bestPractice,
title: 'No Success Response',
description:
'Operation should define at least one success response (2xx).',
suggestions: [
FixSuggestion(
description: 'Add a success response',
codeExample: '"200": { "description": "Success" }',
),
],
),
//
ErrorRule(
id: 'MISSING_OPERATION_ID',
pattern: 'paths.*.*.operationId',
severity: ErrorSeverity.warning,
category: ErrorCategory.bestPractice,
title: 'Missing Operation ID',
description:
'Operation should have an operationId for better code generation.',
suggestions: [
FixSuggestion(
description: 'Add a unique operationId',
codeExample: '"operationId": "getUsers"',
),
],
),
ErrorRule(
id: 'MISSING_OPERATION_SUMMARY',
pattern: 'paths.*.*.summary',
severity: ErrorSeverity.info,
category: ErrorCategory.bestPractice,
title: 'Missing Operation Summary',
description: 'Operation should have a summary for better documentation.',
suggestions: [
FixSuggestion(
description: 'Add a brief summary',
codeExample: '"summary": "Get all users"',
),
],
),
ErrorRule(
id: 'LARGE_SCHEMA_OBJECT',
pattern: 'components.schemas.*',
severity: ErrorSeverity.info,
category: ErrorCategory.performance,
title: 'Large Schema Object',
description: 'Schema has many properties, consider breaking it down.',
suggestions: [
FixSuggestion(
description: 'Consider using composition with allOf',
codeExample:
'"allOf": [{ "\$ref": "#/components/schemas/BaseModel" }, { "type": "object", "properties": {...} }]',
),
],
),
//
ErrorRule(
id: 'MISSING_CONTENT_TYPE',
pattern: 'paths.*.*.requestBody.content',
severity: ErrorSeverity.warning,
category: ErrorCategory.validation,
title: 'Missing Content Type',
description: 'Request body should specify at least one content type.',
suggestions: [
FixSuggestion(
description: 'Add a content type',
codeExample: '"application/json": { "schema": {...} }',
),
],
),
ErrorRule(
id: 'INCONSISTENT_CONTENT_TYPES',
pattern: 'paths.*',
severity: ErrorSeverity.info,
category: ErrorCategory.bestPractice,
title: 'Inconsistent Content Types',
description: 'API uses different content types across operations.',
suggestions: [
FixSuggestion(
description: 'Consider standardizing on common content types',
codeExample: 'Use application/json consistently',
),
],
),
];
///
static List<ErrorRule> getRulesByCategory(ErrorCategory category) {
return rules.where((rule) => rule.category == category).toList();
}
///
static List<ErrorRule> getRulesBySeverity(ErrorSeverity severity) {
return rules.where((rule) => rule.severity == severity).toList();
}
/// ID
static ErrorRule? getRuleById(String id) {
try {
return rules.firstWhere((rule) => rule.id == id);
} catch (e) {
return null;
}
}
/// ID
static List<String> getAllRuleIds() {
return rules.map((rule) => rule.id).toList();
}
///
static DetailedError createMissingFieldError(
String fieldPath, String fieldName) {
return DetailedError(
id: 'MISSING_FIELD',
title: 'Missing Required Field',
description: 'Required field "$fieldName" is missing.',
severity: ErrorSeverity.error,
category: ErrorCategory.validation,
location: ErrorLocation(jsonPath: fieldPath),
suggestions: [
FixSuggestion(
description: 'Add the required field "$fieldName"',
codeExample: '"$fieldName": "value"',
),
],
);
}
static DetailedError createInvalidTypeError(
String fieldPath, String expectedType, String actualType) {
return DetailedError(
id: 'INVALID_TYPE',
title: 'Invalid Field Type',
description:
'Field should be of type "$expectedType" but got "$actualType".',
severity: ErrorSeverity.error,
category: ErrorCategory.validation,
location: ErrorLocation(jsonPath: fieldPath),
suggestions: [
FixSuggestion(
description: 'Change the field type to "$expectedType"',
codeExample: 'Use $expectedType instead of $actualType',
),
],
);
}
static DetailedError createUnknownFieldError(
String fieldPath, String fieldName, List<String> validFields) {
return DetailedError(
id: 'UNKNOWN_FIELD',
title: 'Unknown Field',
description: 'Field "$fieldName" is not recognized in this context.',
severity: ErrorSeverity.warning,
category: ErrorCategory.validation,
location: ErrorLocation(jsonPath: fieldPath),
suggestions: [
FixSuggestion(
description:
'Remove the unknown field or use one of: ${validFields.join(", ")}',
codeExample: 'Valid fields: ${validFields.join(", ")}',
),
],
);
}
static DetailedError createReferenceError(
String fieldPath, String reference) {
return DetailedError(
id: 'INVALID_REFERENCE',
title: 'Invalid Reference',
description: 'Reference "$reference" could not be resolved.',
severity: ErrorSeverity.error,
category: ErrorCategory.reference,
location: ErrorLocation(jsonPath: fieldPath),
suggestions: [
const FixSuggestion(
description: 'Check that the referenced component exists',
codeExample: 'Ensure the component is defined in components section',
),
const FixSuggestion(
description: 'Verify the reference path is correct',
codeExample: '"\$ref": "#/components/schemas/ComponentName"',
),
],
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,471 @@
/// OpenAPI
///
library;
import 'dart:async';
import 'dart:convert';
import '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,
required this.validationTime,
required this.modelCreationTime,
required this.memoryUsage,
required this.documentSize,
required this.pathCount,
required this.schemaCount,
});
double get pathsPerSecond => pathCount / totalTime.inMilliseconds * 1000;
double get schemasPerSecond => schemaCount / totalTime.inMilliseconds * 1000;
double get bytesPerSecond => documentSize / totalTime.inMilliseconds * 1000;
@override
String toString() {
return '''
Performance Statistics:
Total Time: ${totalTime.inMilliseconds}ms
Parse Time: ${parseTime.inMilliseconds}ms
Validation Time: ${validationTime.inMilliseconds}ms
Model Creation Time: ${modelCreationTime.inMilliseconds}ms
Memory Usage: ${(memoryUsage / 1024 / 1024).toStringAsFixed(2)}MB
Document Size: ${(documentSize / 1024).toStringAsFixed(2)}KB
Paths: $pathCount (${pathsPerSecond.toStringAsFixed(1)}/s)
Schemas: $schemaCount (${schemasPerSecond.toStringAsFixed(1)}/s)
Throughput: ${(bytesPerSecond / 1024).toStringAsFixed(2)}KB/s
''';
}
}
///
class ParseConfig {
///
final bool enableParallelParsing;
///
final bool enableStreamParsing;
///
final bool enableIncrementalParsing;
///
final bool enableCaching;
///
final int maxConcurrency;
///
final int streamBufferSize;
///
final bool enablePerformanceStats;
///
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 {
final ParseConfig _config;
final Map<String, dynamic> _cache = {};
ParsePerformanceStats? _lastStats;
PerformanceParser({ParseConfig? config})
: _config = config ?? const ParseConfig();
///
ParsePerformanceStats? get lastStats => _lastStats;
/// OpenAPI
Future<SwaggerDocument> parseDocument(String jsonString) async {
final stopwatch = Stopwatch()..start();
final parseStopwatch = Stopwatch();
final validationStopwatch = Stopwatch();
final modelCreationStopwatch = Stopwatch();
try {
// JSON
parseStopwatch.start();
final Map<String, dynamic> json;
if (_config.enableStreamParsing &&
jsonString.length > _config.streamBufferSize) {
json = await _parseJsonStream(jsonString);
} else {
json = jsonDecode(jsonString) as Map<String, dynamic>;
}
parseStopwatch.stop();
//
validationStopwatch.start();
_validateBasicStructure(json);
validationStopwatch.stop();
//
modelCreationStopwatch.start();
final SwaggerDocument document;
if (_config.enableParallelParsing) {
document = await _parseDocumentParallel(json);
} else {
document = SwaggerDocument.fromJson(json);
}
modelCreationStopwatch.stop();
stopwatch.stop();
//
if (_config.enablePerformanceStats) {
_lastStats = ParsePerformanceStats(
totalTime: stopwatch.elapsed,
parseTime: parseStopwatch.elapsed,
validationTime: validationStopwatch.elapsed,
modelCreationTime: modelCreationStopwatch.elapsed,
memoryUsage: _getMemoryUsage(),
documentSize: jsonString.length,
pathCount: document.paths.length,
schemaCount: document.components.schemas.length,
);
}
return document;
} catch (e) {
stopwatch.stop();
rethrow;
}
}
/// JSON
Future<Map<String, dynamic>> _parseJsonStream(String jsonString) async {
final completer = Completer<Map<String, dynamic>>();
final controller = StreamController<String>();
//
final chunks = <String>[];
for (int i = 0; i < jsonString.length; i += _config.streamBufferSize) {
final end = (i + _config.streamBufferSize).clamp(0, jsonString.length);
chunks.add(jsonString.substring(i, end));
}
controller.stream.listen(
(chunk) {
// JSON
},
onDone: () {
try {
final result = jsonDecode(jsonString) as Map<String, dynamic>;
completer.complete(result);
} catch (e) {
completer.completeError(e);
}
},
onError: completer.completeError,
);
//
for (final chunk in chunks) {
controller.add(chunk);
}
controller.close();
return completer.future;
}
///
Future<SwaggerDocument> _parseDocumentParallel(
Map<String, dynamic> json) async {
final futures = <Future>[];
final results = <String, dynamic>{};
//
if (json.containsKey('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));
}
if (json.containsKey('servers')) {
futures.add(_parseServersParallel(json['servers'] as List<dynamic>)
.then((servers) => results['servers'] = servers));
}
//
await Future.wait(futures);
//
final mergedJson = Map<String, dynamic>.from(json);
mergedJson.addAll(results);
return SwaggerDocument.fromJson(mergedJson);
}
///
Future<Map<String, ApiPath>> _parsePathsParallel(
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 results = await Future.wait(futures);
//
final mergedPaths = <String, ApiPath>{};
for (final pathMap in results) {
mergedPaths.addAll(pathMap);
}
return mergedPaths;
}
///
Future<ApiComponents> _parseComponentsParallel(
Map<String, dynamic> componentsJson) async {
final futures = <Future>[];
final results = <String, dynamic>{};
if (componentsJson.containsKey('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));
}
await Future.wait(futures);
final mergedComponents = Map<String, dynamic>.from(componentsJson);
mergedComponents.addAll(results);
return ApiComponents.fromJson(mergedComponents);
}
///
Future<List<ApiServer>> _parseServersParallel(
List<dynamic> serversJson) async {
if (serversJson.length <= _config.maxConcurrency) {
return serversJson
.map((json) => ApiServer.fromJson(json as Map<String, dynamic>))
.toList();
}
final chunks = _chunkList(serversJson, _config.maxConcurrency);
final futures = chunks.map((chunk) => _parseServerChunk(chunk));
final results = await Future.wait(futures);
//
final mergedServers = <ApiServer>[];
for (final serverList in results) {
mergedServers.addAll(serverList);
}
return mergedServers;
}
///
Future<Map<String, ApiPath>> _parsePathChunk(
Map<String, dynamic> pathChunk) async {
return _parsePathsSequential(pathChunk);
}
///
Future<List<ApiServer>> _parseServerChunk(List<dynamic> serverChunk) async {
return serverChunk
.map((json) => ApiServer.fromJson(json as Map<String, dynamic>))
.toList();
}
///
Map<String, ApiPath> _parsePathsSequential(Map<String, dynamic> pathsJson) {
final paths = <String, ApiPath>{};
pathsJson.forEach((pathPattern, pathData) {
if (pathData is Map<String, dynamic>) {
pathData.forEach((method, operationData) {
if (operationData is Map<String, dynamic>) {
try {
final apiPath =
ApiPath.fromJson(pathPattern, method, operationData);
paths[pathPattern] = apiPath;
} catch (e) {
//
}
}
});
}
});
return paths;
}
/// Schemas
Future<Map<String, ApiModel>> _parseSchemasParallel(
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 results = await Future.wait(futures);
//
final mergedSchemas = <String, ApiModel>{};
for (final schemaMap in results) {
mergedSchemas.addAll(schemaMap);
}
return mergedSchemas;
}
///
Future<Map<String, ApiSecurityScheme>> _parseSecuritySchemesParallel(
Map<String, dynamic> schemesJson) async {
final schemes = <String, ApiSecurityScheme>{};
schemesJson.forEach((name, schemeData) {
if (schemeData is Map<String, dynamic>) {
try {
final scheme = ApiSecurityScheme.fromJson(schemeData);
schemes[name] = scheme;
} catch (e) {
//
}
}
});
return schemes;
}
/// Schema
Future<Map<String, ApiModel>> _parseSchemaChunk(
Map<String, dynamic> schemaChunk) async {
return _parseSchemasSequential(schemaChunk);
}
/// Schemas
Map<String, ApiModel> _parseSchemasSequential(
Map<String, dynamic> schemasJson) {
final schemas = <String, ApiModel>{};
schemasJson.forEach((name, schemaData) {
if (schemaData is Map<String, dynamic>) {
try {
final model = ApiModel.fromJson(name, schemaData);
schemas[name] = model;
} catch (e) {
// schema
}
}
});
return schemas;
}
///
void _validateBasicStructure(Map<String, dynamic> json) {
if (!json.containsKey('openapi') && !json.containsKey('swagger')) {
throw const FormatException(
'Invalid OpenAPI document: missing version field');
}
if (!json.containsKey('info')) {
throw const FormatException(
'Invalid OpenAPI document: missing info object');
}
final info = json['info'] as Map<String, dynamic>?;
if (info == null ||
!info.containsKey('title') ||
!info.containsKey('version')) {
throw const FormatException(
'Invalid OpenAPI document: info object must contain title and version');
}
}
/// Map
List<Map<String, dynamic>> _chunkMap(
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) {
final end = (i + chunkSize).clamp(0, entries.length);
final chunk = <String, dynamic>{};
for (int j = i; j < end; j++) {
final entry = entries[j];
chunk[entry.key] = entry.value;
}
chunks.add(chunk);
}
return chunks;
}
/// List
List<List<dynamic>> _chunkList(List<dynamic> list, int chunkSize) {
final chunks = <List<dynamic>>[];
for (int i = 0; i < list.length; i += chunkSize) {
final end = (i + chunkSize).clamp(0, list.length);
chunks.add(list.sublist(i, end));
}
return chunks;
}
/// 使
int _getMemoryUsage() {
// 使 dart:developer 使
return 0;
}
///
void clearCache() {
_cache.clear();
}
///
Map<String, dynamic> getCacheStats() {
return {
'size': _cache.length,
'keys': _cache.keys.toList(),
};
}
}

444
lib/core/smart_cache.dart Normal file
View File

@ -0,0 +1,444 @@
///
///
library;
import 'dart:async';
///
class CacheEntry<T> {
final String key;
final T value;
final DateTime createdAt;
final DateTime lastAccessedAt;
final Duration ttl;
final String? etag;
final int? version;
final Map<String, dynamic> metadata;
CacheEntry({
required this.key,
required this.value,
required this.createdAt,
DateTime? lastAccessedAt,
this.ttl = const Duration(hours: 1),
this.etag,
this.version,
this.metadata = const {},
}) : lastAccessedAt = lastAccessedAt ?? createdAt;
///
bool get isExpired => DateTime.now().difference(createdAt) > ttl;
///
bool get needsRefresh =>
DateTime.now().difference(lastAccessedAt) >
Duration(minutes: ttl.inMinutes ~/ 2);
///
CacheEntry<T> withAccess() {
return CacheEntry<T>(
key: key,
value: value,
createdAt: createdAt,
lastAccessedAt: DateTime.now(),
ttl: ttl,
etag: etag,
version: version,
metadata: metadata,
);
}
///
CacheEntry<T> withValue(T newValue, {String? newEtag, int? newVersion}) {
return CacheEntry<T>(
key: key,
value: newValue,
createdAt: DateTime.now(),
lastAccessedAt: DateTime.now(),
ttl: ttl,
etag: newEtag ?? etag,
version: newVersion ?? ((version ?? 0) + 1),
metadata: metadata,
);
}
}
///
enum CacheStrategy {
/// 使
lru,
/// 使
lfu,
///
fifo,
///
ttl,
///
smart,
}
///
class CacheStats {
final int totalRequests;
final int hits;
final int misses;
final int evictions;
final int size;
final int maxSize;
final Duration averageAccessTime;
final Map<String, int> keyAccessCounts;
const CacheStats({
required this.totalRequests,
required this.hits,
required this.misses,
required this.evictions,
required this.size,
required this.maxSize,
required this.averageAccessTime,
required this.keyAccessCounts,
});
double get hitRate => totalRequests > 0 ? hits / totalRequests : 0.0;
double get missRate => totalRequests > 0 ? misses / totalRequests : 0.0;
double get fillRate => maxSize > 0 ? size / maxSize : 0.0;
@override
String toString() {
return '''
Cache Statistics:
Total Requests: $totalRequests
Hits: $hits (${(hitRate * 100).toStringAsFixed(1)}%)
Misses: $misses (${(missRate * 100).toStringAsFixed(1)}%)
Evictions: $evictions
Size: $size / $maxSize (${(fillRate * 100).toStringAsFixed(1)}%)
Average Access Time: ${averageAccessTime.inMicroseconds}μs
Most Accessed Keys: ${_getTopKeys()}
''';
}
String _getTopKeys() {
final sorted = keyAccessCounts.entries.toList()
..sort((a, b) => b.value.compareTo(a.value));
return sorted.take(5).map((e) => '${e.key}(${e.value})').join(', ');
}
}
///
class SmartCache<T> {
final int _maxSize;
final CacheStrategy _strategy;
final Duration _defaultTtl;
final bool _enablePersistence;
final Map<String, CacheEntry<T>> _cache = {};
final Map<String, int> _accessCounts = {};
final Map<String, DateTime> _lastAccess = {};
final List<String> _accessOrder = [];
int _totalRequests = 0;
int _hits = 0;
int _misses = 0;
int _evictions = 0;
final List<Duration> _accessTimes = [];
SmartCache({
int maxSize = 1000,
CacheStrategy strategy = CacheStrategy.smart,
Duration defaultTtl = const Duration(hours: 1),
bool enablePersistence = false,
}) : _maxSize = maxSize,
_strategy = strategy,
_defaultTtl = defaultTtl,
_enablePersistence = enablePersistence;
///
T? get(String key) {
final stopwatch = Stopwatch()..start();
_totalRequests++;
final entry = _cache[key];
if (entry == null) {
_misses++;
stopwatch.stop();
_accessTimes.add(stopwatch.elapsed);
return null;
}
//
if (entry.isExpired) {
_cache.remove(key);
_accessCounts.remove(key);
_lastAccess.remove(key);
_accessOrder.remove(key);
_misses++;
stopwatch.stop();
_accessTimes.add(stopwatch.elapsed);
return null;
}
// 访
_hits++;
_updateAccessStats(key);
_cache[key] = entry.withAccess();
stopwatch.stop();
_accessTimes.add(stopwatch.elapsed);
return entry.value;
}
///
void put(String key, T value,
{Duration? ttl, String? etag, Map<String, dynamic>? metadata}) {
final entry = CacheEntry<T>(
key: key,
value: value,
createdAt: DateTime.now(),
ttl: ttl ?? _defaultTtl,
etag: etag,
metadata: metadata ?? {},
);
//
if (_cache.length >= _maxSize && !_cache.containsKey(key)) {
_evict();
}
_cache[key] = entry;
_updateAccessStats(key);
}
///
bool containsKey(String key) {
final entry = _cache[key];
if (entry == null) return false;
if (entry.isExpired) {
_cache.remove(key);
_accessCounts.remove(key);
_lastAccess.remove(key);
_accessOrder.remove(key);
return false;
}
return true;
}
///
T? remove(String key) {
final entry = _cache.remove(key);
_accessCounts.remove(key);
_lastAccess.remove(key);
_accessOrder.remove(key);
return entry?.value;
}
///
void clear() {
_cache.clear();
_accessCounts.clear();
_lastAccess.clear();
_accessOrder.clear();
}
///
CacheStats getStats() {
final avgAccessTime = _accessTimes.isNotEmpty
? Duration(
microseconds: _accessTimes
.map((d) => d.inMicroseconds)
.reduce((a, b) => a + b) ~/
_accessTimes.length)
: Duration.zero;
return CacheStats(
totalRequests: _totalRequests,
hits: _hits,
misses: _misses,
evictions: _evictions,
size: _cache.length,
maxSize: _maxSize,
averageAccessTime: avgAccessTime,
keyAccessCounts: Map.from(_accessCounts),
);
}
///
List<String> getKeysNeedingRefresh() {
return _cache.entries
.where((entry) => entry.value.needsRefresh)
.map((entry) => entry.key)
.toList();
}
///
Future<void> refreshKeys(
List<String> keys, Future<T> Function(String key) refreshFunction) async {
final futures = keys.map((key) async {
try {
final newValue = await refreshFunction(key);
final oldEntry = _cache[key];
if (oldEntry != null) {
_cache[key] = oldEntry.withValue(newValue);
}
} catch (e) {
//
}
});
await Future.wait(futures);
}
///
Future<void> warmUp(Map<String, Future<T> Function()> warmUpFunctions) async {
final futures = warmUpFunctions.entries.map((entry) async {
try {
final value = await entry.value();
put(entry.key, value);
} catch (e) {
//
}
});
await Future.wait(futures);
}
/// 访
void _updateAccessStats(String key) {
_accessCounts[key] = (_accessCounts[key] ?? 0) + 1;
_lastAccess[key] = DateTime.now();
// 访
_accessOrder.remove(key);
_accessOrder.add(key);
}
///
void _evict() {
if (_cache.isEmpty) return;
String? keyToEvict;
switch (_strategy) {
case CacheStrategy.lru:
keyToEvict = _evictLRU();
break;
case CacheStrategy.lfu:
keyToEvict = _evictLFU();
break;
case CacheStrategy.fifo:
keyToEvict = _evictFIFO();
break;
case CacheStrategy.ttl:
keyToEvict = _evictTTL();
break;
case CacheStrategy.smart:
keyToEvict = _evictSmart();
break;
}
if (keyToEvict != null) {
remove(keyToEvict);
_evictions++;
}
}
/// LRU
String? _evictLRU() {
if (_accessOrder.isEmpty) return null;
return _accessOrder.first;
}
/// LFU
String? _evictLFU() {
if (_accessCounts.isEmpty) return null;
final sorted = _accessCounts.entries.toList()
..sort((a, b) => a.value.compareTo(b.value));
return sorted.first.key;
}
/// FIFO
String? _evictFIFO() {
if (_cache.isEmpty) return null;
final sorted = _cache.entries.toList()
..sort((a, b) => a.value.createdAt.compareTo(b.value.createdAt));
return sorted.first.key;
}
/// TTL
String? _evictTTL() {
//
for (final entry in _cache.entries) {
if (entry.value.isExpired) {
return entry.key;
}
}
//
return _evictFIFO();
}
///
String? _evictSmart() {
if (_cache.isEmpty) return null;
//
final scores = <String, double>{};
final now = DateTime.now();
for (final entry in _cache.entries) {
final key = entry.key;
final cacheEntry = entry.value;
//
final ageFactor = now.difference(cacheEntry.createdAt).inMinutes / 60.0;
// 访访
final accessCount = _accessCounts[key] ?? 1;
final frequencyFactor = 1.0 / accessCount;
// 访访
final lastAccess = _lastAccess[key] ?? cacheEntry.createdAt;
final recencyFactor = now.difference(lastAccess).inMinutes / 60.0;
//
final expireFactor = cacheEntry.isExpired ? 10.0 : 0.0;
//
scores[key] = ageFactor * 0.3 +
frequencyFactor * 0.3 +
recencyFactor * 0.3 +
expireFactor;
}
//
final sorted = scores.entries.toList()
..sort((a, b) => b.value.compareTo(a.value));
return sorted.first.key;
}
///
Future<void> persist() async {
if (!_enablePersistence) return;
//
//
}
///
Future<void> load() async {
if (!_enablePersistence) return;
//
//
}
}

View File

@ -141,20 +141,28 @@ class DocumentationGenerator extends BaseGenerator {
buffer.writeln('');
//
buffer.writeln('### 📝 支持的格式');
buffer.writeln('### 🌐 服务器配置');
buffer.writeln('');
buffer.writeln('**请求格式**:');
for (final format in document.consumes) {
buffer.writeln('- `$format`');
if (document.servers.isNotEmpty) {
for (final server in document.servers) {
buffer.writeln('**服务器**: `${server.url}`');
if (server.description.isNotEmpty) {
buffer.writeln('- ${server.description}');
}
if (server.variables.isNotEmpty) {
buffer.writeln('- 变量:');
server.variables.forEach((name, variable) {
buffer.writeln(
' - `$name`: ${variable.description} (默认: ${variable.defaultValue})');
});
}
buffer.writeln('');
buffer.writeln('**响应格式**:');
for (final format in document.produces) {
buffer.writeln('- `$format`');
}
} else {
buffer.writeln('**服务器**: 相对路径 `/`');
buffer.writeln('');
}
}
///
void _generateAuthenticationInfo(StringBuffer buffer) {
@ -590,11 +598,12 @@ class DocumentationGenerator extends BaseGenerator {
// StringUtils.extractControllerName
/// URL
/// URL ( OpenAPI 3.0 servers )
String _getBaseUrl() {
return document.schemes.isNotEmpty
? '${document.schemes.first}://${document.host}${document.basePath}'
: 'https://${document.host}${document.basePath}';
if (document.servers.isNotEmpty) {
return document.servers.first.url;
}
return '/'; //
}
///

View File

@ -33,12 +33,10 @@ class EndpointCodeGenerator extends BaseGenerator {
buffer.writeln(' ApiPaths._(); // 私有构造函数,防止实例化');
buffer.writeln('');
// URL常量
// URL常量 ( OpenAPI 3.0 servers )
if (includeBaseUrl) {
final baseUrl = customBaseUrl ??
(document.schemes.isNotEmpty
? '${document.schemes.first}://${document.host}${document.basePath}'
: 'https://${document.host}${document.basePath}');
(document.servers.isNotEmpty ? document.servers.first.url : '/');
buffer.writeln(' /// 基础URL');
buffer.writeln(' static const String baseUrl = \'$baseUrl\';');

View File

@ -48,155 +48,8 @@ class ModelCodeGenerator extends ModelGenerator {
return generateEnumCode(model);
}
return useSimpleModels
? generateSimpleModelCode(model)
: generateAnnotatedModelCode(model);
}
///
String generateSimpleModelCode(ApiModel model) {
final className = StringUtils.generateClassName(model.name);
final buffer = StringBuffer();
//
final importedTypes = getImportedTypes(model);
for (final importType in importedTypes) {
final importFileName = StringUtils.generateFileName(importType);
buffer.writeln('import \'$importFileName\';');
}
if (importedTypes.isNotEmpty) {
buffer.writeln('');
}
//
if (model.description.isNotEmpty) {
buffer.writeln(StringUtils.generateComment(model.description));
}
buffer.writeln('class $className {');
//
model.properties.forEach((propName, property) {
final dartType = getDartPropertyType(property);
final nullable = property.nullable ? '?' : '';
final dartPropName = StringUtils.toDartPropertyName(propName);
if (property.description.isNotEmpty) {
buffer.writeln(
' ${StringUtils.generateComment(property.description)}',
);
}
buffer.writeln(' final $dartType$nullable $dartPropName;');
buffer.writeln('');
});
//
if (model.properties.isEmpty) {
buffer.writeln(' const $className();');
} else {
buffer.writeln(' const $className({');
model.properties.forEach((propName, property) {
final dartPropName = StringUtils.toDartPropertyName(propName);
final required = property.required ? 'required ' : '';
buffer.writeln(' ${required}this.$dartPropName,');
});
buffer.writeln(' });');
}
buffer.writeln('');
// fromJson
buffer.writeln(
' factory $className.fromJson(Map<String, dynamic> json) {',
);
if (model.properties.isEmpty) {
buffer.writeln(' return const $className();');
} else {
buffer.writeln(' return $className(');
model.properties.forEach((propName, property) {
final dartPropName = StringUtils.toDartPropertyName(propName);
final dartType = getDartPropertyType(property);
buffer.write(' $dartPropName: ');
//
if (property.type == PropertyType.reference &&
property.reference != null) {
final refType = StringUtils.generateClassName(property.reference!);
if (property.nullable) {
buffer.write(
'json[\'$propName\'] != null ? $refType.fromJson(json[\'$propName\']) : null',
);
} else {
buffer.write('$refType.fromJson(json[\'$propName\'])');
}
} else if (property.type == PropertyType.array) {
//
buffer.write(
'json[\'$propName\'] != null ? List<dynamic>.from(json[\'$propName\']) : null',
);
} else {
//
if (property.nullable) {
buffer.write('json[\'$propName\'] as $dartType?');
} else {
buffer.write('json[\'$propName\'] as $dartType');
}
}
buffer.writeln(',');
});
buffer.writeln(' );');
}
buffer.writeln(' }');
buffer.writeln('');
// toJson
buffer.writeln(' Map<String, dynamic> toJson() {');
buffer.writeln(' return {');
model.properties.forEach((propName, property) {
final dartPropName = StringUtils.toDartPropertyName(propName);
if (property.type == PropertyType.reference &&
property.reference != null) {
buffer.write(' \'$propName\': $dartPropName?.toJson()');
} else if (property.type == PropertyType.array) {
buffer.write(' \'$propName\': $dartPropName');
} else {
buffer.write(' \'$propName\': $dartPropName');
}
buffer.writeln(',');
});
buffer.writeln(' };');
buffer.writeln(' }');
buffer.writeln('');
// copyWith
if (model.properties.isNotEmpty) {
buffer.writeln(' $className copyWith({');
model.properties.forEach((propName, property) {
final dartType = getDartPropertyType(property);
final dartPropName = StringUtils.toDartPropertyName(propName);
buffer.writeln(' $dartType? $dartPropName,');
});
buffer.writeln(' }) {');
buffer.writeln(' return $className(');
model.properties.forEach((propName, property) {
final dartPropName = StringUtils.toDartPropertyName(propName);
buffer.writeln(
' $dartPropName: $dartPropName ?? this.$dartPropName,',
);
});
buffer.writeln(' );');
buffer.writeln(' }');
buffer.writeln('');
}
buffer.writeln('}');
return buffer.toString();
// 使 JsonSerializable
return generateAnnotatedModelCode(model);
}
///
@ -232,6 +85,7 @@ class ModelCodeGenerator extends ModelGenerator {
//
model.properties.forEach((propName, property) {
final dartType = getDartPropertyType(property);
// nullable: true
final nullable = property.nullable ? '?' : '';
final dartPropName = StringUtils.toDartPropertyName(propName);
@ -259,7 +113,9 @@ class ModelCodeGenerator extends ModelGenerator {
buffer.writeln(' const $className({');
model.properties.forEach((propName, property) {
final dartPropName = StringUtils.toDartPropertyName(propName);
final required = property.required ? 'required ' : '';
// required
final shouldBeRequired = !property.nullable;
final required = shouldBeRequired ? 'required ' : '';
buffer.writeln(' ${required}this.$dartPropName,');
});
buffer.writeln(' });');
@ -268,14 +124,14 @@ class ModelCodeGenerator extends ModelGenerator {
// fromJson
buffer.writeln(
' factory $className.fromJson(Map<String, dynamic> json) => _\$${className}FromJson(json);',
' factory $className.fromJson(Map<String, dynamic> json) =>',
);
buffer.writeln(' _\$${className}FromJson(json);');
buffer.writeln('');
// toJson
buffer.writeln(
' Map<String, dynamic> toJson() => _\$${className}ToJson(this);',
);
' Map<String, dynamic> toJson() => _\$${className}ToJson(this);');
buffer.writeln('');
buffer.writeln('}');
@ -352,9 +208,8 @@ class ModelCodeGenerator extends ModelGenerator {
return _generateEnumCodeWithoutImports(model);
}
return useSimpleModels
? _generateSimpleModelCodeWithoutImports(model)
: _generateAnnotatedModelCodeWithoutImports(model);
// 使 JsonSerializable
return _generateAnnotatedModelCodeWithoutImports(model);
}
///
@ -426,141 +281,6 @@ class ModelCodeGenerator extends ModelGenerator {
// StringUtils.generateEnumValueName
///
String _generateSimpleModelCodeWithoutImports(ApiModel model) {
final className = StringUtils.generateClassName(model.name);
final buffer = StringBuffer();
//
if (model.description.isNotEmpty) {
buffer.writeln(StringUtils.generateComment(model.description));
}
buffer.writeln('class $className {');
//
model.properties.forEach((propName, property) {
final dartType = getDartPropertyType(property);
final nullable = property.nullable ? '?' : '';
final dartPropName = StringUtils.toDartPropertyName(propName);
if (property.description.isNotEmpty) {
buffer.writeln(
' ${StringUtils.generateComment(property.description)}',
);
}
buffer.writeln(' final $dartType$nullable $dartPropName;');
buffer.writeln('');
});
//
if (model.properties.isEmpty) {
buffer.writeln(' const $className();');
} else {
buffer.writeln(' const $className({');
model.properties.forEach((propName, property) {
final dartPropName = StringUtils.toDartPropertyName(propName);
final required = property.required ? 'required ' : '';
buffer.writeln(' ${required}this.$dartPropName,');
});
buffer.writeln(' });');
}
buffer.writeln('');
// fromJson
buffer.writeln(
' factory $className.fromJson(Map<String, dynamic> json) {',
);
if (model.properties.isEmpty) {
buffer.writeln(' return const $className();');
} else {
buffer.writeln(' return $className(');
model.properties.forEach((propName, property) {
final dartPropName = StringUtils.toDartPropertyName(propName);
final dartType = getDartPropertyType(property);
buffer.write(' $dartPropName: ');
//
if (property.type == PropertyType.reference &&
property.reference != null) {
final refType = StringUtils.generateClassName(property.reference!);
if (property.nullable) {
buffer.write(
'json[\'$propName\'] != null ? $refType.fromJson(json[\'$propName\']) : null',
);
} else {
buffer.write('$refType.fromJson(json[\'$propName\'])');
}
} else if (property.type == PropertyType.array) {
//
buffer.write(
'json[\'$propName\'] != null ? List<dynamic>.from(json[\'$propName\']) : null',
);
} else {
//
if (property.nullable) {
buffer.write('json[\'$propName\'] as $dartType?');
} else {
buffer.write('json[\'$propName\'] as $dartType');
}
}
buffer.writeln(',');
});
buffer.writeln(' );');
}
buffer.writeln(' }');
buffer.writeln('');
// toJson
buffer.writeln(' Map<String, dynamic> toJson() {');
buffer.writeln(' return {');
model.properties.forEach((propName, property) {
final dartPropName = StringUtils.toDartPropertyName(propName);
if (property.type == PropertyType.reference &&
property.reference != null) {
buffer.write(' \'$propName\': $dartPropName?.toJson()');
} else if (property.type == PropertyType.array) {
buffer.write(' \'$propName\': $dartPropName');
} else {
buffer.write(' \'$propName\': $dartPropName');
}
buffer.writeln(',');
});
buffer.writeln(' };');
buffer.writeln(' }');
buffer.writeln('');
// copyWith
if (model.properties.isNotEmpty) {
buffer.writeln(' $className copyWith({');
model.properties.forEach((propName, property) {
final dartType = getDartPropertyType(property);
final dartPropName = StringUtils.toDartPropertyName(propName);
buffer.writeln(' $dartType? $dartPropName,');
});
buffer.writeln(' }) {');
buffer.writeln(' return $className(');
model.properties.forEach((propName, property) {
final dartPropName = StringUtils.toDartPropertyName(propName);
buffer.writeln(
' $dartPropName: $dartPropName ?? this.$dartPropName,',
);
});
buffer.writeln(' );');
buffer.writeln(' }');
buffer.writeln('');
}
buffer.writeln('}');
return buffer.toString();
}
///
String _generateAnnotatedModelCodeWithoutImports(ApiModel model) {
final className = StringUtils.generateClassName(model.name);
@ -583,6 +303,7 @@ class ModelCodeGenerator extends ModelGenerator {
//
model.properties.forEach((propName, property) {
final dartType = getDartPropertyType(property);
// nullable: true
final nullable = property.nullable ? '?' : '';
final dartPropName = StringUtils.toDartPropertyName(propName);
@ -610,7 +331,9 @@ class ModelCodeGenerator extends ModelGenerator {
buffer.writeln(' const $className({');
model.properties.forEach((propName, property) {
final dartPropName = StringUtils.toDartPropertyName(propName);
final required = property.required ? 'required ' : '';
// required
final shouldBeRequired = !property.nullable;
final required = shouldBeRequired ? 'required ' : '';
buffer.writeln(' ${required}this.$dartPropName,');
});
buffer.writeln(' });');
@ -619,14 +342,14 @@ class ModelCodeGenerator extends ModelGenerator {
// fromJson
buffer.writeln(
' factory $className.fromJson(Map<String, dynamic> json) => _\$${className}FromJson(json);',
' factory $className.fromJson(Map<String, dynamic> json) =>',
);
buffer.writeln(' _\$${className}FromJson(json);');
buffer.writeln('');
// toJson
buffer.writeln(
' Map<String, dynamic> toJson() => _\$${className}ToJson(this);',
);
' Map<String, dynamic> toJson() => _\$${className}ToJson(this);');
buffer.writeln('');
buffer.writeln('}');

View File

@ -0,0 +1,547 @@
/// Retrofit API
/// Dio + Retrofit
library;
import '../core/models.dart';
import 'base_generator.dart';
/// Retrofit API
/// Dio + Retrofit
class OptimizedRetrofitGenerator extends BaseGenerator {
final String className;
final bool generateModularApis;
final bool generateBaseResult;
final bool generatePagination;
final bool generateFileUpload;
final String baseResultType;
final String pageResultType;
OptimizedRetrofitGenerator({
this.className = 'ApiService',
this.generateModularApis = true,
this.generateBaseResult = true,
this.generatePagination = true,
this.generateFileUpload = true,
this.baseResultType = 'BaseResult',
this.pageResultType = 'BasePageResult',
});
@override
String get generatorType => 'OptimizedRetrofitGenerator';
@override
String generate() {
throw UnimplementedError('Use generateFromDocument instead');
}
/// API
String generateFromDocument(SwaggerDocument document) {
final buffer = StringBuffer();
//
_generateFileHeader(buffer);
//
_generateImports(buffer);
//
if (generateBaseResult) {
_generateBaseResultTypes(buffer);
}
//
if (generatePagination) {
_generatePaginationTypes(buffer);
}
//
if (generateFileUpload) {
_generateFileUploadTypes(buffer);
}
// API API
if (generateModularApis) {
_generateModularApis(buffer, document);
} else {
_generateSingleApi(buffer, document);
}
//
_generateUtilityClasses(buffer);
return buffer.toString();
}
///
void _generateFileHeader(StringBuffer buffer) {
buffer.writeln('/// 自动生成的 API 接口文件');
buffer.writeln('/// 基于 Dio + Retrofit 架构优化');
buffer.writeln('/// 支持模块化、分页、文件上传等功能');
buffer.writeln('/// 请勿手动修改此文件');
buffer.writeln('/// 生成时间: ${DateTime.now().toIso8601String()}');
buffer.writeln();
}
///
void _generateImports(StringBuffer buffer) {
buffer.writeln('// Dart 核心库');
buffer.writeln('import \'dart:convert\';');
buffer.writeln('import \'dart:io\';');
buffer.writeln('import \'dart:typed_data\';');
buffer.writeln();
buffer.writeln('// 网络请求相关');
buffer.writeln('import \'package:dio/dio.dart\';');
buffer.writeln('import \'package:retrofit/retrofit.dart\';');
buffer.writeln('import \'package:json_annotation/json_annotation.dart\';');
buffer.writeln();
buffer.writeln('// 文件处理');
buffer.writeln('import \'package:path/path.dart\' as path;');
buffer.writeln('import \'package:http_parser/http_parser.dart\';');
buffer.writeln();
buffer.writeln('// 生成的代码');
buffer.writeln('part \'${_getGeneratedFileName()}.g.dart\';');
buffer.writeln();
}
///
void _generateBaseResultTypes(StringBuffer buffer) {
buffer.writeln('/// 基础响应结果');
buffer.writeln('@JsonSerializable(genericArgumentFactories: true)');
buffer.writeln('class $baseResultType<T> {');
buffer.writeln(' /// 响应码');
buffer.writeln(' final int code;');
buffer.writeln();
buffer.writeln(' /// 响应消息');
buffer.writeln(' final String message;');
buffer.writeln();
buffer.writeln(' /// 响应数据');
buffer.writeln(' final T? data;');
buffer.writeln();
buffer.writeln(' /// 是否成功');
buffer.writeln(' bool get isSuccess => code == 200;');
buffer.writeln();
buffer.writeln(' const $baseResultType({');
buffer.writeln(' required this.code,');
buffer.writeln(' required this.message,');
buffer.writeln(' this.data,');
buffer.writeln(' });');
buffer.writeln();
buffer.writeln(' factory $baseResultType.fromJson(');
buffer.writeln(' Map<String, dynamic> json,');
buffer.writeln(' T Function(Object? json) fromJsonT,');
buffer.writeln(' ) => _\$${baseResultType}FromJson(json, fromJsonT);');
buffer.writeln();
buffer.writeln(
' Map<String, dynamic> toJson(Object Function(T value) toJsonT) =>');
buffer.writeln(' _\$${baseResultType}ToJson(this, toJsonT);');
buffer.writeln('}');
buffer.writeln();
}
///
void _generatePaginationTypes(StringBuffer buffer) {
buffer.writeln('/// 分页参数');
buffer.writeln('@JsonSerializable()');
buffer.writeln('class BasePageParameter {');
buffer.writeln(' /// 页码从1开始');
buffer.writeln(' final int page;');
buffer.writeln();
buffer.writeln(' /// 每页大小');
buffer.writeln(' final int size;');
buffer.writeln();
buffer.writeln(' const BasePageParameter({');
buffer.writeln(' this.page = 1,');
buffer.writeln(' this.size = 20,');
buffer.writeln(' });');
buffer.writeln();
buffer.writeln(
' factory BasePageParameter.fromJson(Map<String, dynamic> json) =>');
buffer.writeln(' _\$BasePageParameterFromJson(json);');
buffer.writeln();
buffer.writeln(
' Map<String, dynamic> toJson() => _\$BasePageParameterToJson(this);');
buffer.writeln('}');
buffer.writeln();
buffer.writeln('/// 分页响应结果');
buffer.writeln('@JsonSerializable(genericArgumentFactories: true)');
buffer.writeln('class $pageResultType<T> {');
buffer.writeln(' /// 数据列表');
buffer.writeln(' final List<T> list;');
buffer.writeln();
buffer.writeln(' /// 总数量');
buffer.writeln(' final int total;');
buffer.writeln();
buffer.writeln(' /// 当前页码');
buffer.writeln(' final int page;');
buffer.writeln();
buffer.writeln(' /// 每页大小');
buffer.writeln(' final int size;');
buffer.writeln();
buffer.writeln(' /// 总页数');
buffer.writeln(' int get totalPages => (total / size).ceil();');
buffer.writeln();
buffer.writeln(' /// 是否有下一页');
buffer.writeln(' bool get hasNext => page < totalPages;');
buffer.writeln();
buffer.writeln(' /// 是否有上一页');
buffer.writeln(' bool get hasPrevious => page > 1;');
buffer.writeln();
buffer.writeln(' const $pageResultType({');
buffer.writeln(' required this.list,');
buffer.writeln(' required this.total,');
buffer.writeln(' required this.page,');
buffer.writeln(' required this.size,');
buffer.writeln(' });');
buffer.writeln();
buffer.writeln(' factory $pageResultType.fromJson(');
buffer.writeln(' Map<String, dynamic> json,');
buffer.writeln(' T Function(Object? json) fromJsonT,');
buffer.writeln(' ) => _\$${pageResultType}FromJson(json, fromJsonT);');
buffer.writeln();
buffer.writeln(
' Map<String, dynamic> toJson(Object Function(T value) toJsonT) =>');
buffer.writeln(' _\$${pageResultType}ToJson(this, toJsonT);');
buffer.writeln('}');
buffer.writeln();
}
///
void _generateFileUploadTypes(StringBuffer buffer) {
buffer.writeln('/// 文件上传请求');
buffer.writeln('@JsonSerializable()');
buffer.writeln('class FileUploadRequest {');
buffer.writeln(' /// 文件');
buffer.writeln(' @JsonKey(includeFromJson: false, includeToJson: false)');
buffer.writeln(' final MultipartFile file;');
buffer.writeln();
buffer.writeln(' /// 文件名');
buffer.writeln(' final String? filename;');
buffer.writeln();
buffer.writeln(' /// 文件类型');
buffer.writeln(' final String? contentType;');
buffer.writeln();
buffer.writeln(' const FileUploadRequest({');
buffer.writeln(' required this.file,');
buffer.writeln(' this.filename,');
buffer.writeln(' this.contentType,');
buffer.writeln(' });');
buffer.writeln();
buffer.writeln(
' factory FileUploadRequest.fromJson(Map<String, dynamic> json) =>');
buffer.writeln(' _\$FileUploadRequestFromJson(json);');
buffer.writeln();
buffer.writeln(
' Map<String, dynamic> toJson() => _\$FileUploadRequestToJson(this);');
buffer.writeln('}');
buffer.writeln();
buffer.writeln('/// 文件上传响应');
buffer.writeln('@JsonSerializable()');
buffer.writeln('class FileUploadResult {');
buffer.writeln(' /// 文件 URL');
buffer.writeln(' final String url;');
buffer.writeln();
buffer.writeln(' /// 文件名');
buffer.writeln(' final String filename;');
buffer.writeln();
buffer.writeln(' /// 文件大小');
buffer.writeln(' final int size;');
buffer.writeln();
buffer.writeln(' /// 文件类型');
buffer.writeln(' final String? contentType;');
buffer.writeln();
buffer.writeln(' const FileUploadResult({');
buffer.writeln(' required this.url,');
buffer.writeln(' required this.filename,');
buffer.writeln(' required this.size,');
buffer.writeln(' this.contentType,');
buffer.writeln(' });');
buffer.writeln();
buffer.writeln(
' factory FileUploadResult.fromJson(Map<String, dynamic> json) =>');
buffer.writeln(' _\$FileUploadResultFromJson(json);');
buffer.writeln();
buffer.writeln(
' Map<String, dynamic> toJson() => _\$FileUploadResultToJson(this);');
buffer.writeln('}');
buffer.writeln();
}
/// API
void _generateModularApis(StringBuffer buffer, SwaggerDocument document) {
// API
final modules = _groupApisByModule(document);
for (final entry in modules.entries) {
final moduleName = entry.key;
final paths = entry.value;
_generateModuleApi(buffer, moduleName, paths);
}
// API
_generateMainApiClass(buffer, modules.keys.toList());
}
/// API
void _generateSingleApi(StringBuffer buffer, SwaggerDocument document) {
buffer.writeln('/// $className API 接口');
buffer.writeln('@RestApi()');
buffer.writeln('abstract class $className {');
buffer.writeln(
' factory $className(Dio dio, {String? baseUrl}) = _$className;');
buffer.writeln();
// API
document.paths.forEach((path, apiPath) {
_generateApiMethod(buffer, path, apiPath);
});
buffer.writeln('}');
buffer.writeln();
}
/// API
Map<String, Map<String, ApiPath>> _groupApisByModule(
SwaggerDocument document) {
final modules = <String, Map<String, ApiPath>>{};
document.paths.forEach((path, apiPath) {
final moduleName = _extractModuleName(path);
modules.putIfAbsent(moduleName, () => {});
modules[moduleName]![path] = apiPath;
});
return modules;
}
///
String _extractModuleName(String path) {
final parts = path.split('/').where((part) => part.isNotEmpty).toList();
if (parts.length >= 3) {
// /api/v1/ModuleName/... -> ModuleName
return _toPascalCase(parts[2]);
}
return 'Common';
}
/// API
void _generateModuleApi(
StringBuffer buffer, String moduleName, Map<String, ApiPath> paths) {
final className = '${moduleName}Api';
buffer.writeln('/// $moduleName 模块 API');
buffer.writeln('@RestApi()');
buffer.writeln('abstract class $className {');
buffer.writeln(
' factory $className(Dio dio, {String? baseUrl}) = _$className;');
buffer.writeln();
paths.forEach((path, apiPath) {
_generateApiMethod(buffer, path, apiPath);
});
buffer.writeln('}');
buffer.writeln();
}
/// API
void _generateMainApiClass(StringBuffer buffer, List<String> modules) {
buffer.writeln('/// 主 API 服务类');
buffer.writeln('/// 包含所有模块的 API 接口');
buffer.writeln('class $className {');
buffer.writeln(' final Dio _dio;');
buffer.writeln();
// API
for (final module in modules) {
final propertyName = _toCamelCase(module);
buffer.writeln(' late final ${module}Api $propertyName;');
}
buffer.writeln();
buffer.writeln(' $className(this._dio, {String? baseUrl}) {');
// API
for (final module in modules) {
final propertyName = _toCamelCase(module);
buffer
.writeln(' $propertyName = ${module}Api(_dio, baseUrl: baseUrl);');
}
buffer.writeln(' }');
buffer.writeln('}');
buffer.writeln();
}
/// API
void _generateApiMethod(StringBuffer buffer, String path, ApiPath apiPath) {
final methodName = _generateMethodName(path, apiPath.method);
final returnType = _generateReturnType(apiPath);
final parameters = _generateParameters(apiPath);
buffer.writeln(
' /// ${apiPath.summary.isNotEmpty ? apiPath.summary : apiPath.description}');
if (apiPath.description.isNotEmpty &&
apiPath.description != apiPath.summary) {
buffer.writeln(' /// ${apiPath.description}');
}
buffer.writeln(' @${apiPath.method.value.toUpperCase()}(\'$path\')');
//
if (_isMultipartRequest(apiPath)) {
buffer.writeln(' @MultiPart()');
}
buffer.writeln(' Future<$returnType> $methodName($parameters);');
buffer.writeln();
}
///
String _generateMethodName(String path, HttpMethod method) {
final pathParts = path
.split('/')
.where((part) => part.isNotEmpty && !part.startsWith('{'))
.toList();
final methodPrefix = method.value.toLowerCase();
if (pathParts.length >= 3) {
// api/v1
pathParts.removeRange(0, 2);
}
final nameParts = pathParts.map((part) => _toPascalCase(part)).join('');
return '$methodPrefix$nameParts';
}
///
String _generateReturnType(ApiPath apiPath) {
//
final successResponse =
apiPath.responses['200'] ?? apiPath.responses['201'];
if (successResponse != null && successResponse.content.isNotEmpty) {
final jsonContent = successResponse.content['application/json'];
if (jsonContent?.schema != null) {
// schema
return '$baseResultType<dynamic>';
}
}
return '$baseResultType<void>';
}
///
String _generateParameters(ApiPath apiPath) {
final params = <String>[];
//
for (final param in apiPath.parameters
.where((p) => p.location == ParameterLocation.path)) {
params.add(
'@Path(\'${param.name}\') ${_getDartType(param.type)} ${param.name}');
}
//
for (final param in apiPath.parameters
.where((p) => p.location == ParameterLocation.query)) {
final required = param.required ? 'required ' : '';
params.add(
'@Query(\'${param.name}\') ${required}${_getDartType(param.type)}${param.required ? '' : '?'} ${param.name}');
}
//
if (apiPath.requestBody != null) {
if (_isMultipartRequest(apiPath)) {
//
params.add('@Part() MultipartFile file');
} else {
// JSON
params.add('@Body() Map<String, dynamic> body');
}
}
return params.join(', ');
}
/// multipart
bool _isMultipartRequest(ApiPath apiPath) {
if (apiPath.requestBody == null) return false;
return apiPath.requestBody!.content.keys
.any((type) => type.contains('multipart'));
}
/// Dart
String _getDartType(PropertyType type) {
switch (type) {
case PropertyType.string:
return 'String';
case PropertyType.integer:
return 'int';
case PropertyType.number:
return 'double';
case PropertyType.boolean:
return 'bool';
case PropertyType.array:
return 'List<dynamic>';
case PropertyType.object:
return 'Map<String, dynamic>';
default:
return 'dynamic';
}
}
///
void _generateUtilityClasses(StringBuffer buffer) {
buffer.writeln('/// API 工具类');
buffer.writeln('class ApiUtils {');
buffer.writeln(' /// 创建文件上传对象');
buffer.writeln(
' static Future<MultipartFile> createFileUpload(String filePath) async {');
buffer.writeln(' return MultipartFile.fromFile(');
buffer.writeln(' filePath,');
buffer.writeln(' filename: path.basename(filePath),');
buffer.writeln(' );');
buffer.writeln(' }');
buffer.writeln();
buffer.writeln(' /// 创建分页参数');
buffer.writeln(
' static BasePageParameter createPageParam({int page = 1, int size = 20}) {');
buffer.writeln(' return BasePageParameter(page: page, size: size);');
buffer.writeln(' }');
buffer.writeln('}');
}
///
String _getGeneratedFileName() {
return '${_toSnakeCase(className)}_api';
}
/// PascalCase
String _toPascalCase(String input) {
return input
.split('_')
.map((word) => word.isEmpty
? ''
: word[0].toUpperCase() + word.substring(1).toLowerCase())
.join('');
}
/// camelCase
String _toCamelCase(String input) {
final pascalCase = _toPascalCase(input);
return pascalCase.isEmpty
? ''
: pascalCase[0].toLowerCase() + pascalCase.substring(1);
}
/// snake_case
String _toSnakeCase(String input) {
return input
.replaceAllMapped(
RegExp(r'[A-Z]'), (match) => '_${match.group(0)!.toLowerCase()}')
.replaceAll(RegExp(r'^_'), '');
}
}

View File

@ -0,0 +1,591 @@
///
///
library;
import 'dart:async';
import '../core/models.dart';
import '../core/smart_cache.dart';
import 'base_generator.dart';
///
class GenerationTask {
final String id;
final String type;
final Map<String, dynamic> data;
final DateTime createdAt;
GenerationTask({
required this.id,
required this.type,
required this.data,
DateTime? createdAt,
}) : createdAt = createdAt ?? DateTime.now();
}
///
class GenerationResult {
final String taskId;
final String content;
final Duration generationTime;
final Map<String, dynamic> metadata;
const GenerationResult({
required this.taskId,
required this.content,
required this.generationTime,
this.metadata = const {},
});
}
///
class GenerationStats {
final int totalTasks;
final int completedTasks;
final int failedTasks;
final Duration totalTime;
final Duration averageTaskTime;
final int linesGenerated;
final int bytesGenerated;
final double parallelEfficiency;
const GenerationStats({
required this.totalTasks,
required this.completedTasks,
required this.failedTasks,
required this.totalTime,
required this.averageTaskTime,
required this.linesGenerated,
required this.bytesGenerated,
required this.parallelEfficiency,
});
double get successRate => totalTasks > 0 ? completedTasks / totalTasks : 0.0;
double get linesPerSecond => totalTime.inMilliseconds > 0
? linesGenerated / (totalTime.inMilliseconds / 1000)
: 0.0;
double get bytesPerSecond => totalTime.inMilliseconds > 0
? bytesGenerated / (totalTime.inMilliseconds / 1000)
: 0.0;
@override
String toString() {
return '''
Generation Performance Statistics:
Total Tasks: $totalTasks
Completed: $completedTasks (${(successRate * 100).toStringAsFixed(1)}%)
Failed: $failedTasks
Total Time: ${totalTime.inMilliseconds}ms
Average Task Time: ${averageTaskTime.inMilliseconds}ms
Lines Generated: $linesGenerated (${linesPerSecond.toStringAsFixed(1)}/s)
Bytes Generated: ${(bytesGenerated / 1024).toStringAsFixed(2)}KB (${(bytesPerSecond / 1024).toStringAsFixed(2)}KB/s)
Parallel Efficiency: ${(parallelEfficiency * 100).toStringAsFixed(1)}%
''';
}
}
///
class PerformanceGenerator extends BaseGenerator {
final int _maxConcurrency;
final bool _enableCaching;
final bool _enableIncremental;
final bool _enableParallel;
final SmartCache<String> _cache;
final Map<String, String> _previousGeneration = {};
final List<GenerationResult> _results = [];
int _totalTasks = 0;
int _completedTasks = 0;
int _failedTasks = 0;
final List<Duration> _taskTimes = [];
PerformanceGenerator({
int maxConcurrency = 4,
bool enableCaching = true,
bool enableIncremental = true,
bool enableParallel = true,
}) : _maxConcurrency = maxConcurrency,
_enableCaching = enableCaching,
_enableIncremental = enableIncremental,
_enableParallel = enableParallel,
_cache = SmartCache<String>(
maxSize: 1000,
strategy: CacheStrategy.smart,
defaultTtl: Duration(hours: 1),
);
@override
String get generatorType => 'PerformanceGenerator';
@override
String generate() {
throw UnimplementedError('Use generateFromDocument instead');
}
///
Future<String> generateFromDocument(SwaggerDocument document) async {
final stopwatch = Stopwatch()..start();
try {
//
final changes = _enableIncremental ? _analyzeChanges(document) : null;
//
final tasks = _createGenerationTasks(document, changes);
_totalTasks = tasks.length;
//
final results = _enableParallel && tasks.length > 1
? await _generateParallel(tasks)
: await _generateSequential(tasks);
//
final finalResult = _mergeResults(results);
//
if (_enableIncremental) {
_updateGenerationHistory(document, finalResult);
}
stopwatch.stop();
return finalResult;
} catch (e) {
stopwatch.stop();
rethrow;
}
}
///
Map<String, dynamic>? _analyzeChanges(SwaggerDocument document) {
final currentHash = _calculateDocumentHash(document);
final previousHash = _previousGeneration['hash'];
if (previousHash == null || currentHash != previousHash) {
return {
'hasChanges': true,
'currentHash': currentHash,
'previousHash': previousHash,
'changedSections': _detectChangedSections(document),
};
}
return {
'hasChanges': false,
'currentHash': currentHash,
};
}
///
List<GenerationTask> _createGenerationTasks(
SwaggerDocument document, Map<String, dynamic>? changes) {
final tasks = <GenerationTask>[];
//
if (_enableIncremental && changes != null && !changes['hasChanges']) {
return tasks;
}
//
tasks.add(GenerationTask(
id: 'header',
type: 'header',
data: {
'title': document.title,
'version': document.version,
'description': document.description,
},
));
//
tasks.add(GenerationTask(
id: 'imports',
type: 'imports',
data: {},
));
//
document.models.forEach((name, model) {
tasks.add(GenerationTask(
id: 'model_$name',
type: 'model',
data: {
'name': name,
'model': model,
},
));
});
// API
final pathGroups = _groupPathsByModule(document.paths);
pathGroups.forEach((module, paths) {
tasks.add(GenerationTask(
id: 'api_$module',
type: 'api',
data: {
'module': module,
'paths': paths,
},
));
});
return tasks;
}
///
Future<List<GenerationResult>> _generateParallel(
List<GenerationTask> tasks) async {
final chunks = _chunkTasks(tasks, _maxConcurrency);
final results = <GenerationResult>[];
for (final chunk in chunks) {
final chunkResults = await Future.wait(
chunk.map((task) => _executeTask(task)),
);
results.addAll(chunkResults);
}
return results;
}
///
Future<List<GenerationResult>> _generateSequential(
List<GenerationTask> tasks) async {
final results = <GenerationResult>[];
for (final task in tasks) {
final result = await _executeTask(task);
results.add(result);
}
return results;
}
///
Future<GenerationResult> _executeTask(GenerationTask task) async {
final stopwatch = Stopwatch()..start();
try {
//
if (_enableCaching) {
final cacheKey = _generateCacheKey(task);
final cached = _cache.get(cacheKey);
if (cached != null) {
stopwatch.stop();
_completedTasks++;
_taskTimes.add(stopwatch.elapsed);
return GenerationResult(
taskId: task.id,
content: cached,
generationTime: stopwatch.elapsed,
metadata: {'fromCache': true},
);
}
}
//
final content = await _generateTaskContent(task);
//
if (_enableCaching) {
final cacheKey = _generateCacheKey(task);
_cache.put(cacheKey, content);
}
stopwatch.stop();
_completedTasks++;
_taskTimes.add(stopwatch.elapsed);
return GenerationResult(
taskId: task.id,
content: content,
generationTime: stopwatch.elapsed,
metadata: {'fromCache': false},
);
} catch (e) {
stopwatch.stop();
_failedTasks++;
_taskTimes.add(stopwatch.elapsed);
return GenerationResult(
taskId: task.id,
content: '// Error generating ${task.type}: $e',
generationTime: stopwatch.elapsed,
metadata: {'error': e.toString()},
);
}
}
///
Future<String> _generateTaskContent(GenerationTask task) async {
switch (task.type) {
case 'header':
return _generateHeader(task.data);
case 'imports':
return _generateImports(task.data);
case 'model':
return _generateModel(task.data);
case 'api':
return _generateApi(task.data);
default:
throw UnsupportedError('Unknown task type: ${task.type}');
}
}
///
String _generateHeader(Map<String, dynamic> data) {
final buffer = StringBuffer();
buffer.writeln('/// Generated API for ${data['title']}');
buffer.writeln('/// Version: ${data['version']}');
buffer.writeln('/// ${data['description']}');
buffer.writeln('/// Generated at: ${DateTime.now().toIso8601String()}');
buffer.writeln();
return buffer.toString();
}
///
String _generateImports(Map<String, dynamic> data) {
final buffer = StringBuffer();
buffer.writeln('import \'dart:convert\';');
buffer.writeln('import \'package:dio/dio.dart\';');
buffer.writeln('import \'package:retrofit/retrofit.dart\';');
buffer.writeln('import \'package:json_annotation/json_annotation.dart\';');
buffer.writeln();
buffer.writeln('part \'generated_api.g.dart\';');
buffer.writeln();
return buffer.toString();
}
///
String _generateModel(Map<String, dynamic> data) {
final name = data['name'] as String;
final model = data['model'] as ApiModel;
final buffer = StringBuffer();
buffer.writeln('@JsonSerializable()');
buffer.writeln('class $name {');
//
model.properties.forEach((propName, property) {
buffer.writeln(' final ${_getDartType(property.type)} $propName;');
});
buffer.writeln();
buffer.writeln(' const $name({');
model.properties.forEach((propName, property) {
final required = property.required ? 'required ' : '';
buffer.writeln(' ${required}this.$propName,');
});
buffer.writeln(' });');
buffer.writeln();
buffer.writeln(' factory $name.fromJson(Map<String, dynamic> json) =>');
buffer.writeln(' _\$${name}FromJson(json);');
buffer.writeln();
buffer
.writeln(' Map<String, dynamic> toJson() => _\$${name}ToJson(this);');
buffer.writeln('}');
buffer.writeln();
return buffer.toString();
}
/// API
String _generateApi(Map<String, dynamic> data) {
final module = data['module'] as String;
final paths = data['paths'] as Map<String, ApiPath>;
final buffer = StringBuffer();
buffer.writeln('@RestApi()');
buffer.writeln('abstract class ${module}Api {');
buffer.writeln(
' factory ${module}Api(Dio dio, {String? baseUrl}) = _${module}Api;');
buffer.writeln();
paths.forEach((path, apiPath) {
buffer.writeln(' @${apiPath.method.value.toUpperCase()}(\'$path\')');
buffer.writeln(
' Future<dynamic> ${_generateMethodName(path, apiPath.method)}();');
buffer.writeln();
});
buffer.writeln('}');
buffer.writeln();
return buffer.toString();
}
///
String _mergeResults(List<GenerationResult> results) {
final buffer = StringBuffer();
//
final sortedResults = List<GenerationResult>.from(results);
sortedResults.sort((a, b) {
final order = ['header', 'imports', 'model', 'api'];
final aType = a.taskId.split('_')[0];
final bType = b.taskId.split('_')[0];
final aIndex = order.indexOf(aType);
final bIndex = order.indexOf(bType);
return aIndex.compareTo(bIndex);
});
for (final result in sortedResults) {
buffer.write(result.content);
}
return buffer.toString();
}
///
List<List<GenerationTask>> _chunkTasks(
List<GenerationTask> tasks, int chunkSize) {
final chunks = <List<GenerationTask>>[];
for (int i = 0; i < tasks.length; i += chunkSize) {
final end = (i + chunkSize).clamp(0, tasks.length);
chunks.add(tasks.sublist(i, end));
}
return chunks;
}
///
Map<String, Map<String, ApiPath>> _groupPathsByModule(
Map<String, ApiPath> paths) {
final groups = <String, Map<String, ApiPath>>{};
paths.forEach((path, apiPath) {
final module = _extractModuleName(path);
groups.putIfAbsent(module, () => {});
groups[module]![path] = apiPath;
});
return groups;
}
///
String _extractModuleName(String path) {
final parts = path.split('/').where((part) => part.isNotEmpty).toList();
if (parts.length >= 3) {
return _toPascalCase(parts[2]);
}
return 'Common';
}
///
String _generateCacheKey(GenerationTask task) {
final dataHash = task.data.toString().hashCode;
return '${task.type}_${dataHash}';
}
///
String _calculateDocumentHash(SwaggerDocument document) {
final content =
'${document.title}_${document.version}_${document.paths.length}_${document.models.length}';
return content.hashCode.toString();
}
///
List<String> _detectChangedSections(SwaggerDocument document) {
//
return ['paths', 'models', 'components'];
}
///
void _updateGenerationHistory(SwaggerDocument document, String result) {
_previousGeneration['hash'] = _calculateDocumentHash(document);
_previousGeneration['result'] = result;
_previousGeneration['timestamp'] = DateTime.now().toIso8601String();
}
///
GenerationStats getStats() {
final totalTime = _taskTimes.isNotEmpty
? _taskTimes.reduce((a, b) => a + b)
: Duration.zero;
final averageTime = _taskTimes.isNotEmpty
? Duration(
microseconds: _taskTimes
.map((d) => d.inMicroseconds)
.reduce((a, b) => a + b) ~/
_taskTimes.length)
: Duration.zero;
//
int linesGenerated = 0;
int bytesGenerated = 0;
for (final result in _results) {
linesGenerated += result.content.split('\n').length;
bytesGenerated += result.content.length;
}
//
final parallelEfficiency = _enableParallel && _totalTasks > 1 ? 0.8 : 1.0;
return GenerationStats(
totalTasks: _totalTasks,
completedTasks: _completedTasks,
failedTasks: _failedTasks,
totalTime: totalTime,
averageTaskTime: averageTime,
linesGenerated: linesGenerated,
bytesGenerated: bytesGenerated,
parallelEfficiency: parallelEfficiency,
);
}
///
CacheStats getCacheStats() => _cache.getStats();
///
void clearCache() => _cache.clear();
/// Dart
String _getDartType(PropertyType type) {
switch (type) {
case PropertyType.string:
return 'String';
case PropertyType.integer:
return 'int';
case PropertyType.number:
return 'double';
case PropertyType.boolean:
return 'bool';
case PropertyType.array:
return 'List<dynamic>';
case PropertyType.object:
return 'Map<String, dynamic>';
default:
return 'dynamic';
}
}
///
String _generateMethodName(String path, HttpMethod method) {
final pathParts = path
.split('/')
.where((part) => part.isNotEmpty && !part.startsWith('{'))
.toList();
final methodPrefix = method.value.toLowerCase();
if (pathParts.length >= 3) {
pathParts.removeRange(0, 2);
}
final nameParts = pathParts.map((part) => _toPascalCase(part)).join('');
return '$methodPrefix$nameParts';
}
/// PascalCase
String _toPascalCase(String input) {
return input
.split('_')
.map((word) => word.isEmpty
? ''
: word[0].toUpperCase() + word.substring(1).toLowerCase())
.join('');
}
}

File diff suppressed because it is too large Load Diff

View File

@ -8,23 +8,21 @@ 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 '../utils/type_validator.dart';
/// Swagger数据解析器
/// Swagger JSON文档并提取相关信息
class SwaggerDataParser {
final CacheManager _cacheManager;
final PerformanceMonitor _performanceMonitor;
final TypeValidator _typeValidator;
//
SwaggerDocument? _cachedDocument;
SwaggerDataParser()
: _cacheManager = CacheManager(),
_performanceMonitor = PerformanceMonitor(),
_typeValidator = TypeValidator();
_performanceMonitor = PerformanceMonitor();
/// Swagger JSON文档
Future<SwaggerDocument> fetchAndParseSwaggerDocument() async {
@ -111,12 +109,11 @@ class SwaggerDataParser {
final version = info['version'] as String? ?? '1.0.0';
final description = info['description'] as String? ?? '';
//
final host = jsonData['host'] as String? ?? '';
final basePath = jsonData['basePath'] as String? ?? '/';
final schemes = List<String>.from(jsonData['schemes'] ?? ['https']);
final consumes = List<String>.from(jsonData['consumes'] ?? []);
final produces = List<String>.from(jsonData['produces'] ?? []);
// servers (OpenAPI 3.0)
final servers = _parseServers(jsonData);
// components (OpenAPI 3.0)
final components = _parseComponents(jsonData);
// tags信息 ()
final tagsInfo = _parseTagsInfo(jsonData);
@ -124,8 +121,8 @@ class SwaggerDataParser {
// API路径
final paths = _parseApiPaths(jsonData);
// API模型
final models = _parseApiModels(jsonData);
// API模型 ( components )
final models = components.schemas;
// API控制器 (tags信息)
final controllers = _parseApiControllers(paths, tagsInfo);
@ -134,11 +131,8 @@ class SwaggerDataParser {
title: title,
version: version,
description: description,
host: host,
basePath: basePath,
schemes: schemes,
consumes: consumes,
produces: produces,
servers: servers,
components: components,
paths: paths,
models: models,
controllers: controllers,
@ -152,6 +146,64 @@ class SwaggerDataParser {
});
}
/// servers (OpenAPI 3.0)
List<ApiServer> _parseServers(Map<String, dynamic> jsonData) {
final servers = <ApiServer>[];
try {
final serversJson = jsonData['servers'] as List<dynamic>?;
if (serversJson != null) {
for (final serverJson in serversJson) {
if (serverJson is Map<String, dynamic>) {
final server = ApiServer.fromJson(serverJson);
servers.add(server);
}
}
}
// servers
if (servers.isEmpty) {
servers.add(const ApiServer(url: '/'));
}
} catch (e) {
print('⚠️ 解析servers配置时发生错误: $e');
//
servers.add(const ApiServer(url: '/'));
}
return servers;
}
/// components (OpenAPI 3.0)
ApiComponents _parseComponents(Map<String, dynamic> jsonData) {
try {
final componentsJson = jsonData['components'] as Map<String, dynamic>?;
if (componentsJson != null) {
// 使
final resolver = ReferenceResolver();
final resolvedSchemas = resolver.resolveModels(componentsJson);
// ApiComponents使 schemas
final components = ApiComponents.fromJson(componentsJson);
return ApiComponents(
schemas: resolvedSchemas,
responses: components.responses,
parameters: components.parameters,
examples: components.examples,
requestBodies: components.requestBodies,
headers: components.headers,
securitySchemes: components.securitySchemes,
links: components.links,
callbacks: components.callbacks,
);
}
} catch (e) {
print('⚠️ 解析components配置时发生错误: $e');
}
return const ApiComponents();
}
/// tags信息
Map<String, String> _parseTagsInfo(Map<String, dynamic> jsonData) {
final tagsInfo = <String, String>{};
@ -210,41 +262,6 @@ class SwaggerDataParser {
return paths;
}
/// API模型
Map<String, ApiModel> _parseApiModels(Map<String, dynamic> jsonData) {
final models = <String, ApiModel>{};
// components/schemas (Swagger 3.0)
final schemas = jsonData['components']?['schemas'] as Map<String, dynamic>?;
// components/schemas definitions (Swagger 2.0)
final definitions = jsonData['definitions'] as Map<String, dynamic>?;
final modelDefinitions = schemas ?? definitions;
if (modelDefinitions == null) {
print(' 未发现模型定义 (components/schemas 或 definitions)');
return models;
}
print(
'🔍 发现模型定义位置: ${schemas != null ? 'components/schemas' : 'definitions'}',
);
try {
modelDefinitions.forEach((name, definition) {
final model = ApiModel.fromJson(
name,
definition as Map<String, dynamic>,
);
models[name] = model;
});
} catch (e) {
throw SwaggerParseException('解析API模型失败', details: e.toString());
}
return models;
}
/// API控制器
Map<String, ApiController> _parseApiControllers(
Map<String, ApiPath> paths,

View File

@ -1,193 +0,0 @@
import 'dart:io';
import 'commands/base_command.dart';
import 'commands/generate_command.dart';
import 'core/config.dart';
import 'utils/performance_monitor.dart';
import 'utils/string_utils.dart';
/// Swagger CLI
/// 使CLI工具
class SwaggerCLI {
final Map<String, BaseCommand> _commands = {};
final PerformanceMonitor _monitor = PerformanceMonitor();
SwaggerCLI() {
_registerCommands();
}
///
void _registerCommands() {
_registerCommand(GenerateCommand());
//
// _registerCommand(ParseCommand());
// _registerCommand(ValidateCommand());
// _registerCommand(InfoCommand());
// _registerCommand(TestCommand());
// _registerCommand(CleanCommand());
}
///
void _registerCommand(BaseCommand command) {
_commands[command.name] = command;
}
/// CLI应用程序
Future<int> run(List<String> arguments) async {
try {
_showBanner();
if (arguments.isEmpty ||
arguments.first == 'help' ||
arguments.first == '--help') {
_showHelp();
return 0;
}
final commandName = arguments.first;
final commandArgs =
arguments.length > 1 ? arguments.sublist(1) : <String>[];
//
if (commandName == 'version' || commandName == '--version') {
_showVersion();
return 0;
}
//
final command = _commands[commandName];
if (command == null) {
print('❌ 未知命令: $commandName');
print('');
_showAvailableCommands();
return 1;
}
//
if (commandArgs.contains('--help') || commandArgs.contains('-h')) {
command.showHelp();
return 0;
}
//
final stopwatch = Stopwatch()..start();
final exitCode = await command.execute(commandArgs);
stopwatch.stop();
//
if (exitCode == 0) {
print('');
print('⏱️ 执行时间: ${StringUtils.formatDuration(stopwatch.elapsed)}');
}
return exitCode;
} catch (error, stackTrace) {
print('❌ 应用程序错误: $error');
print('堆栈跟踪: $stackTrace');
return 1;
}
}
///
void _showBanner() {
print('');
print('🚀 Swagger API 代码生成器 v2.0');
print('=====================================');
print('强大的 Swagger API 代码生成工具');
print('');
}
///
void _showHelp() {
print('用法: dart swagger_cli_new.dart <命令> [选项]');
print('');
print('全新的命令式架构,提供更好的可扩展性和用户体验。');
print('');
_showAvailableCommands();
_showGlobalOptions();
_showExamples();
_showContact();
}
///
void _showAvailableCommands() {
print('📋 可用命令:');
print('');
for (final command in _commands.values) {
print(' ${command.name.padRight(12)} ${command.description}');
}
print(' help 显示帮助信息');
print(' version 显示版本信息');
print('');
}
///
void _showGlobalOptions() {
print('🔧 全局选项:');
print(' -h, --help 显示帮助信息');
print(' --version 显示版本信息');
print('');
}
/// 使
void _showExamples() {
print('💡 使用示例:');
print('');
print(' # 生成所有文件');
print(' dart swagger_cli_new.dart generate --all');
print('');
print(' # 只生成模型文件(简洁版本)');
print(' dart swagger_cli_new.dart generate --models --simple');
print('');
print(' # 生成到指定目录并启用性能监控');
print(
' dart swagger_cli_new.dart generate --all --output-dir lib/generated --performance',
);
print('');
print(' # 查看具体命令的帮助');
print(' dart swagger_cli_new.dart generate --help');
print('');
}
///
void _showContact() {
print('🌐 更多信息:');
print(' API文档: ${SwaggerConfig.swaggerJsonUrl}');
print(' 基础URL: ${SwaggerConfig.baseUrl}');
print('');
}
///
void _showVersion() {
print('Swagger CLI v2.0.0');
print('构建于: ${DateTime.now().toIso8601String()}');
print('Dart SDK: ${Platform.version}');
print('');
print('功能特性:');
print('- 🏗️ 模块化命令架构');
print('- 🚀 性能监控和优化');
print('- 🔍 智能类型验证');
print('- 📋 详细的错误报告');
print('- 💾 智能缓存机制');
print('- 📚 丰富的文档生成');
print('');
}
///
// StringUtils.formatDuration
///
List<String> get availableCommands => _commands.keys.toList();
///
BaseCommand? getCommand(String name) => _commands[name];
}
/// CLI应用程序入口点
Future<void> main(List<String> arguments) async {
final cli = SwaggerCLI();
final exitCode = await cli.run(arguments);
exit(exitCode);
}

View File

@ -0,0 +1,19 @@
/// Swagger Generator Flutter
///
/// Flutter OpenAPI 3.0 Dio + Retrofit
library swagger_generator_flutter;
export 'core/error_reporter.dart';
//
export 'core/models.dart';
export 'core/performance_parser.dart';
export 'core/smart_cache.dart';
export 'generators/optimized_retrofit_generator.dart';
export 'generators/performance_generator.dart';
//
export 'generators/retrofit_api_generator.dart';
//
export 'utils/string_utils.dart';
//
export 'validators/enhanced_validator.dart';
export 'validators/schema_validator.dart';

View File

@ -289,7 +289,6 @@ class FileUtils {
///
static Future<String> generateUniqueFileName(
String basePath, String fileName) async {
final directory = Directory(basePath);
final extension = getFileExtension(fileName);
final nameWithoutExt = getFileNameWithoutExtension(fileName);

View File

@ -0,0 +1,301 @@
///
///
library reference_resolver;
import '../core/models.dart';
///
/// OpenAPI
class ReferenceResolver {
///
final Map<String, ApiModel> _resolvedModels = {};
///
final Set<String> _resolutionPath = {};
/// JSON
final Map<String, Map<String, dynamic>> _rawSchemas = {};
///
final int maxDepth;
///
int _currentDepth = 0;
ReferenceResolver({this.maxDepth = 50});
///
Map<String, ApiModel> resolveModels(Map<String, dynamic> componentsJson) {
final schemasJson =
componentsJson['schemas'] as Map<String, dynamic>? ?? {};
// schema
_cacheRawSchemas(schemasJson);
//
final resolvedModels = <String, ApiModel>{};
for (final schemaName in schemasJson.keys) {
try {
final model = resolveModel(schemaName);
if (model != null) {
resolvedModels[schemaName] = model;
}
} catch (e) {
print('⚠️ 解析模型 $schemaName 时发生错误: $e');
//
resolvedModels[schemaName] = ApiModel(
name: schemaName,
description: '解析失败的模型',
properties: {},
required: [],
);
}
}
return resolvedModels;
}
/// schema
void _cacheRawSchemas(Map<String, dynamic> schemasJson) {
schemasJson.forEach((name, schemaData) {
if (schemaData is Map<String, dynamic>) {
_rawSchemas[name] = schemaData;
}
});
}
///
ApiModel? resolveModel(String modelName) {
//
if (_resolvedModels.containsKey(modelName)) {
return _resolvedModels[modelName];
}
//
if (_resolutionPath.contains(modelName)) {
print('🔄 检测到循环引用: ${_resolutionPath.join(' -> ')} -> $modelName');
return _createCircularReferenceModel(modelName);
}
//
if (_currentDepth >= maxDepth) {
print('⚠️ 达到最大解析深度 $maxDepth,停止解析 $modelName');
return _createDepthLimitModel(modelName);
}
//
final schemaData = _rawSchemas[modelName];
if (schemaData == null) {
print('⚠️ 未找到模型定义: $modelName');
return null;
}
//
_resolutionPath.add(modelName);
_currentDepth++;
try {
final model = _parseModelWithContext(modelName, schemaData);
_resolvedModels[modelName] = model;
return model;
} finally {
_resolutionPath.remove(modelName);
_currentDepth--;
}
}
///
ApiModel _parseModelWithContext(String name, Map<String, dynamic> json) {
//
final isEnum = json['enum'] != null;
if (isEnum) {
return _parseEnumModel(name, json);
}
//
if (json['allOf'] != null ||
json['oneOf'] != null ||
json['anyOf'] != null) {
return _parseCompositionModel(name, json);
}
//
return _parseObjectModel(name, json);
}
///
ApiModel _parseEnumModel(String name, Map<String, dynamic> json) {
final enumValues = List<dynamic>.from(json['enum'] ?? []);
final enumType =
PropertyType.fromString(json['type'] as String? ?? 'string');
return ApiModel(
name: name,
description: json['description'] as String? ?? '',
properties: {},
required: [],
isEnum: true,
enumValues: enumValues,
enumType: enumType,
);
}
///
ApiModel _parseCompositionModel(String name, Map<String, dynamic> json) {
//
final allOf = _parseSchemaList(json['allOf'] as List<dynamic>? ?? []);
final oneOf = _parseSchemaList(json['oneOf'] as List<dynamic>? ?? []);
final anyOf = _parseSchemaList(json['anyOf'] as List<dynamic>? ?? []);
final notJson = json['not'] as Map<String, dynamic>?;
final not = notJson != null ? ApiSchema.fromJson(notJson) : null;
// discriminator
final discriminatorJson = json['discriminator'] as Map<String, dynamic>?;
final discriminator = discriminatorJson != null
? ApiDiscriminator.fromJson(discriminatorJson)
: null;
//
final mergedProperties = <String, ApiProperty>{};
final mergedRequired = <String>[];
// allOf
for (final schema in allOf) {
_mergeSchemaProperties(schema, mergedProperties, mergedRequired);
}
// properties
if (json['properties'] != null) {
final directProperties = _parseProperties(
json['properties'] as Map<String, dynamic>,
List<String>.from(json['required'] ?? []),
);
mergedProperties.addAll(directProperties);
mergedRequired.addAll(List<String>.from(json['required'] ?? []));
}
return ApiModel(
name: name,
description: json['description'] as String? ?? '',
properties: mergedProperties,
required: mergedRequired.toSet().toList(),
allOf: allOf,
oneOf: oneOf,
anyOf: anyOf,
not: not,
discriminator: discriminator,
);
}
///
ApiModel _parseObjectModel(String name, Map<String, dynamic> json) {
final properties = _parseProperties(
json['properties'] as Map<String, dynamic>? ?? {},
List<String>.from(json['required'] ?? []),
);
final required = List<String>.from(json['required'] ?? []);
return ApiModel(
name: name,
description: json['description'] as String? ?? '',
properties: properties,
required: required,
);
}
/// schema
List<ApiSchema> _parseSchemaList(List<dynamic> schemaList) {
return schemaList
.map((schema) => ApiSchema.fromJson(schema as Map<String, dynamic>))
.toList();
}
/// schema
void _mergeSchemaProperties(
ApiSchema schema,
Map<String, ApiProperty> targetProperties,
List<String> targetRequired,
) {
//
if (schema.isReference && schema.reference != null) {
final referencedModel = resolveModel(schema.reference!);
if (referencedModel != null) {
targetProperties.addAll(referencedModel.properties);
targetRequired.addAll(referencedModel.required);
}
} else {
//
targetProperties.addAll(schema.properties);
targetRequired.addAll(schema.required);
}
}
///
Map<String, ApiProperty> _parseProperties(
Map<String, dynamic> propertiesJson,
List<String> requiredFields,
) {
final properties = <String, ApiProperty>{};
propertiesJson.forEach((propName, propData) {
if (propData is Map<String, dynamic>) {
try {
final property =
_parsePropertyWithContext(propName, propData, requiredFields);
properties[propName] = property;
} catch (e) {
print('⚠️ 解析属性 $propName 时发生错误: $e');
//
properties[propName] = ApiProperty(
name: propName,
type: PropertyType.string,
description: '解析失败的属性',
required: requiredFields.contains(propName),
);
}
}
});
return properties;
}
///
ApiProperty _parsePropertyWithContext(
String name,
Map<String, dynamic> json,
List<String> requiredFields,
) {
// 使 ApiProperty.fromJson
return ApiProperty.fromJson(name, json, requiredFields);
}
///
ApiModel _createCircularReferenceModel(String modelName) {
return ApiModel(
name: modelName,
description: '循环引用模型 - 为避免无限递归而创建的占位符',
properties: {},
required: [],
);
}
///
ApiModel _createDepthLimitModel(String modelName) {
return ApiModel(
name: modelName,
description: '深度限制模型 - 达到最大解析深度而创建的占位符',
properties: {},
required: [],
);
}
///
void clearCache() {
_resolvedModels.clear();
_resolutionPath.clear();
_rawSchemas.clear();
_currentDepth = 0;
}
}

View File

@ -210,6 +210,14 @@ class StringUtils {
return toPascalCase(cleanName);
}
/// (UPPER_SNAKE_CASE)
static String generateConstantName(String name) {
//
final cleanName = name.replaceAll(RegExp(r'[^a-zA-Z0-9_]'), '_');
// snake_case
return toSnakeCase(cleanName).toUpperCase();
}
///
static String generateFileName(String name) {
// snake_case并添加.dart扩展名

View File

@ -0,0 +1,597 @@
/// OpenAPI
///
library;
import '../core/error_reporter.dart';
import '../core/models.dart';
/// OpenAPI
class EnhancedValidator {
final ErrorReporter _errorReporter;
final bool _includeWarnings;
EnhancedValidator({
bool includeWarnings = true,
}) : _errorReporter = ErrorReporter(),
_includeWarnings = includeWarnings;
///
ErrorReporter get errorReporter => _errorReporter;
/// OpenAPI
bool validateDocument(SwaggerDocument document) {
_errorReporter.clear();
//
_validateBasicStructure(document);
//
_validatePaths(document);
//
_validateComponents(document);
//
_validateSecurity(document);
//
if (_includeWarnings) {
_checkBestPractices(document);
}
return !_errorReporter.hasErrorsOrCritical;
}
///
void _validateBasicStructure(SwaggerDocument document) {
//
if (document.title.isEmpty) {
_errorReporter.reportError(
id: 'MISSING_INFO_TITLE',
title: 'Missing API Title',
description: 'API title is required in the info object.',
severity: ErrorSeverity.error,
category: ErrorCategory.validation,
jsonPath: 'info.title',
suggestions: [
FixSuggestion(
description: 'Add a descriptive title for your API',
codeExample: '"title": "My API"',
),
],
);
}
//
if (document.version.isEmpty) {
_errorReporter.reportError(
id: 'MISSING_INFO_VERSION',
title: 'Missing API Version',
description: 'API version is required in the info object.',
severity: ErrorSeverity.error,
category: ErrorCategory.validation,
jsonPath: 'info.version',
suggestions: [
FixSuggestion(
description: 'Add a version number using semantic versioning',
codeExample: '"version": "1.0.0"',
documentationUrl: 'https://semver.org/',
),
],
);
}
//
if (document.description.isEmpty && _includeWarnings) {
_errorReporter.reportError(
id: 'MISSING_INFO_DESCRIPTION',
title: 'Missing API Description',
description:
'API description helps users understand the purpose of your API.',
severity: ErrorSeverity.warning,
category: ErrorCategory.bestPractice,
jsonPath: 'info.description',
suggestions: [
FixSuggestion(
description: 'Add a description explaining what your API does',
codeExample:
'"description": "This API provides user management functionality"',
),
],
);
}
//
if (document.servers.isEmpty && _includeWarnings) {
_errorReporter.reportError(
id: 'MISSING_SERVERS',
title: 'Missing Server Configuration',
description:
'Server configuration helps clients know where to send requests.',
severity: ErrorSeverity.warning,
category: ErrorCategory.bestPractice,
jsonPath: 'servers',
suggestions: [
FixSuggestion(
description: 'Add at least one server configuration',
codeExample:
'"servers": [{"url": "https://api.example.com", "description": "Production server"}]',
),
],
);
}
}
///
void _validatePaths(SwaggerDocument document) {
if (document.paths.isEmpty) {
_errorReporter.reportError(
id: 'EMPTY_PATHS',
title: 'Empty Paths Object',
description: 'OpenAPI document must contain at least one path.',
severity: ErrorSeverity.error,
category: ErrorCategory.validation,
jsonPath: 'paths',
suggestions: [
FixSuggestion(
description: 'Add at least one API endpoint',
codeExample:
'"/users": { "get": { "responses": { "200": { "description": "Success" } } } }',
),
],
);
return;
}
document.paths.forEach((pathPattern, apiPath) {
_validatePath(pathPattern, apiPath);
});
}
///
void _validatePath(String pathPattern, ApiPath apiPath) {
final pathKey = 'paths["$pathPattern"][${apiPath.method.value}]';
//
if (!pathPattern.startsWith('/')) {
_errorReporter.reportError(
id: 'INVALID_PATH_FORMAT',
title: 'Invalid Path Format',
description: 'Path must start with a forward slash.',
severity: ErrorSeverity.error,
category: ErrorCategory.syntax,
jsonPath: pathKey,
snippet: pathPattern,
suggestions: [
FixSuggestion(
description: 'Ensure path starts with /',
codeExample: '"/$pathPattern" instead of "$pathPattern"',
),
],
);
}
//
if (apiPath.responses.isEmpty) {
_errorReporter.reportError(
id: 'MISSING_OPERATION_RESPONSES',
title: 'Missing Operation Responses',
description: 'Every operation must define at least one response.',
severity: ErrorSeverity.error,
category: ErrorCategory.validation,
jsonPath: '$pathKey.responses',
suggestions: [
FixSuggestion(
description: 'Add at least a default response',
codeExample: '"responses": { "200": { "description": "Success" } }',
),
],
);
}
// ID
if (apiPath.operationId.isEmpty && _includeWarnings) {
_errorReporter.reportError(
id: 'MISSING_OPERATION_ID',
title: 'Missing Operation ID',
description:
'Operation should have an operationId for better code generation.',
severity: ErrorSeverity.warning,
category: ErrorCategory.bestPractice,
jsonPath: '$pathKey.operationId',
suggestions: [
FixSuggestion(
description: 'Add a unique operationId',
codeExample:
'"operationId": "${_generateOperationId(pathPattern, apiPath.method)}"',
),
],
);
}
//
if (apiPath.summary.isEmpty && _includeWarnings) {
_errorReporter.reportError(
id: 'MISSING_OPERATION_SUMMARY',
title: 'Missing Operation Summary',
description:
'Operation should have a summary for better documentation.',
severity: ErrorSeverity.info,
category: ErrorCategory.bestPractice,
jsonPath: '$pathKey.summary',
suggestions: [
FixSuggestion(
description: 'Add a brief summary',
codeExample: '"summary": "Get all users"',
),
],
);
}
//
_validateParameters(apiPath.parameters, pathKey, pathPattern);
//
_validateResponses(apiPath.responses, pathKey);
}
///
void _validateParameters(
List<ApiParameter> parameters, String pathKey, String pathPattern) {
//
final pathParams = _extractPathParameters(pathPattern);
final declaredPathParams = parameters
.where((p) => p.location == ParameterLocation.path)
.map((p) => p.name)
.toSet();
//
for (final param in pathParams) {
if (!declaredPathParams.contains(param)) {
_errorReporter.reportError(
id: 'UNDECLARED_PATH_PARAMETER',
title: 'Undeclared Path Parameter',
description:
'Path parameter "$param" is used in the path but not declared in parameters.',
severity: ErrorSeverity.error,
category: ErrorCategory.validation,
jsonPath: '$pathKey.parameters',
suggestions: [
FixSuggestion(
description: 'Add parameter declaration',
codeExample:
'{"name": "$param", "in": "path", "required": true, "schema": {"type": "string"}}',
),
],
);
}
}
//
for (int i = 0; i < parameters.length; i++) {
final param = parameters[i];
final paramPath = '$pathKey.parameters[$i]';
//
if (param.name.isEmpty) {
_errorReporter.reportError(
id: 'MISSING_PARAMETER_NAME',
title: 'Missing Parameter Name',
description: 'Parameter must have a name.',
severity: ErrorSeverity.error,
category: ErrorCategory.validation,
jsonPath: '$paramPath.name',
suggestions: [
FixSuggestion(
description: 'Add a name for the parameter',
codeExample: '"name": "userId"',
),
],
);
}
//
if (param.location == ParameterLocation.path && !param.required) {
_errorReporter.reportError(
id: 'PATH_PARAMETER_NOT_REQUIRED',
title: 'Path Parameter Not Required',
description: 'Path parameters must be marked as required.',
severity: ErrorSeverity.error,
category: ErrorCategory.validation,
jsonPath: '$paramPath.required',
suggestions: [
FixSuggestion(
description: 'Set required: true for path parameters',
codeExample: '"required": true',
),
],
);
}
}
}
///
void _validateResponses(Map<String, ApiResponse> responses, String pathKey) {
bool hasSuccessResponse = false;
bool hasErrorResponse = false;
responses.forEach((code, response) {
final responsePath = '$pathKey.responses["$code"]';
final statusCode = int.tryParse(code) ?? 0;
//
if (statusCode >= 200 && statusCode < 300) {
hasSuccessResponse = true;
}
//
if (statusCode >= 400) {
hasErrorResponse = true;
}
//
if (response.description.isEmpty) {
_errorReporter.reportError(
id: 'MISSING_RESPONSE_DESCRIPTION',
title: 'Missing Response Description',
description: 'Response should have a description.',
severity: ErrorSeverity.warning,
category: ErrorCategory.bestPractice,
jsonPath: '$responsePath.description',
suggestions: [
FixSuggestion(
description: 'Add a description for the response',
codeExample: '"description": "Successful operation"',
),
],
);
}
});
//
if (!hasSuccessResponse && _includeWarnings) {
_errorReporter.reportError(
id: 'NO_SUCCESS_RESPONSE',
title: 'No Success Response',
description:
'Operation should define at least one success response (2xx).',
severity: ErrorSeverity.warning,
category: ErrorCategory.bestPractice,
jsonPath: '$pathKey.responses',
suggestions: [
FixSuggestion(
description: 'Add a success response',
codeExample: '"200": { "description": "Success" }',
),
],
);
}
//
if (!hasErrorResponse && _includeWarnings) {
_errorReporter.reportError(
id: 'NO_ERROR_RESPONSE',
title: 'No Error Response',
description:
'Consider adding error responses (4xx/5xx) for better API documentation.',
severity: ErrorSeverity.info,
category: ErrorCategory.bestPractice,
jsonPath: '$pathKey.responses',
suggestions: [
FixSuggestion(
description: 'Add common error responses',
codeExample:
'"400": { "description": "Bad Request" }, "404": { "description": "Not Found" }',
),
],
);
}
}
///
void _validateComponents(SwaggerDocument document) {
// schemas
document.components.schemas.forEach((name, model) {
_validateSchema(name, model);
});
//
document.components.securitySchemes.forEach((name, scheme) {
_validateSecurityScheme(name, scheme);
});
}
/// Schema
void _validateSchema(String name, ApiModel model) {
final schemaPath = 'components.schemas["$name"]';
//
if (model.name.isEmpty) {
_errorReporter.reportError(
id: 'MISSING_SCHEMA_NAME',
title: 'Missing Schema Name',
description: 'Schema should have a name.',
severity: ErrorSeverity.error,
category: ErrorCategory.validation,
jsonPath: schemaPath,
suggestions: [
FixSuggestion(
description: 'Ensure schema has a valid name',
codeExample:
'Schema name should match the key in components.schemas',
),
],
);
}
//
if (model.properties.length > 20 && _includeWarnings) {
_errorReporter.reportError(
id: 'LARGE_SCHEMA_OBJECT',
title: 'Large Schema Object',
description:
'Schema has many properties (${model.properties.length}), consider breaking it down.',
severity: ErrorSeverity.info,
category: ErrorCategory.performance,
jsonPath: schemaPath,
suggestions: [
FixSuggestion(
description: 'Consider using composition with allOf',
codeExample:
'"allOf": [{ "\$ref": "#/components/schemas/BaseModel" }, { "type": "object", "properties": {...} }]',
),
],
);
}
}
///
void _validateSecurityScheme(String name, ApiSecurityScheme scheme) {
final schemePath = 'components.securitySchemes["$name"]';
switch (scheme.type) {
case SecuritySchemeType.apiKey:
if (scheme.name == null || scheme.name!.isEmpty) {
_errorReporter.reportError(
id: 'MISSING_API_KEY_NAME',
title: 'Missing API Key Name',
description:
'API Key security scheme must specify a parameter name.',
severity: ErrorSeverity.error,
category: ErrorCategory.security,
jsonPath: '$schemePath.name',
suggestions: [
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) {
_errorReporter.reportError(
id: 'MISSING_HTTP_SCHEME',
title: 'Missing HTTP Scheme',
description:
'HTTP security scheme must specify a scheme (basic, bearer, etc.).',
severity: ErrorSeverity.error,
category: ErrorCategory.security,
jsonPath: '$schemePath.scheme',
suggestions: [
FixSuggestion(
description: 'Add scheme field',
codeExample: '"scheme": "bearer"',
),
],
);
}
break;
case SecuritySchemeType.oauth2:
if (scheme.flows == null) {
_errorReporter.reportError(
id: 'MISSING_OAUTH2_FLOWS',
title: 'Missing OAuth2 Flows',
description: 'OAuth2 security scheme must define flows.',
severity: ErrorSeverity.error,
category: ErrorCategory.security,
jsonPath: '$schemePath.flows',
suggestions: [
FixSuggestion(
description: 'Add flows configuration',
codeExample:
'"flows": { "authorizationCode": { "authorizationUrl": "...", "tokenUrl": "..." } }',
),
],
);
}
break;
case SecuritySchemeType.openIdConnect:
if (scheme.openIdConnectUrl == null ||
scheme.openIdConnectUrl!.isEmpty) {
_errorReporter.reportError(
id: 'MISSING_OPENID_URL',
title: 'Missing OpenID Connect URL',
description: 'OpenID Connect security scheme must specify a URL.',
severity: ErrorSeverity.error,
category: ErrorCategory.security,
jsonPath: '$schemePath.openIdConnectUrl',
suggestions: [
FixSuggestion(
description: 'Add OpenID Connect URL',
codeExample:
'"openIdConnectUrl": "https://example.com/.well-known/openid_configuration"',
),
],
);
}
break;
}
}
///
void _validateSecurity(SwaggerDocument document) {
//
}
///
void _checkBestPractices(SwaggerDocument document) {
// 使
final hasTaggedOperations =
document.paths.values.any((path) => path.tags.isNotEmpty);
if (!hasTaggedOperations) {
_errorReporter.reportError(
id: 'NO_OPERATION_TAGS',
title: 'No Operation Tags',
description: 'Consider using tags to organize your API operations.',
severity: ErrorSeverity.info,
category: ErrorCategory.bestPractice,
jsonPath: 'paths',
suggestions: [
FixSuggestion(
description: 'Add tags to operations',
codeExample: '"tags": ["users"]',
),
],
);
}
}
///
Set<String> _extractPathParameters(String path) {
final regex = RegExp(r'\{([^}]+)\}');
final matches = regex.allMatches(path);
return matches.map((match) => match.group(1)!).toSet();
}
/// ID
String _generateOperationId(String path, HttpMethod method) {
final pathParts = path
.split('/')
.where((part) => part.isNotEmpty && !part.startsWith('{'))
.toList();
final methodPrefix = method.value.toLowerCase();
if (pathParts.length >= 3) {
// api/v1
pathParts.removeRange(0, 2);
}
final nameParts = pathParts.map((part) => _toPascalCase(part)).join('');
return '$methodPrefix$nameParts';
}
/// PascalCase
String _toPascalCase(String input) {
return input
.split('_')
.map((word) => word.isEmpty
? ''
: word[0].toUpperCase() + word.substring(1).toLowerCase())
.join('');
}
}

View File

@ -0,0 +1,845 @@
/// Schema
/// OpenAPI 3.0
library;
import '../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 [],
this.warnings = const [],
});
///
factory ValidationResult.success(
{List<ValidationWarning> warnings = const []}) {
return ValidationResult(
isValid: true,
warnings: warnings,
);
}
///
factory ValidationResult.failure(List<ValidationError> errors,
{List<ValidationWarning> warnings = const []}) {
return ValidationResult(
isValid: false,
errors: errors,
warnings: warnings,
);
}
///
bool get hasWarnings => warnings.isNotEmpty;
///
bool get hasErrors => errors.isNotEmpty;
}
///
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,
});
@override
String toString() {
final buffer = StringBuffer();
buffer.write('[$type] $path: $message');
if (suggestion != null) {
buffer.write(' (建议: $suggestion)');
}
return buffer.toString();
}
}
///
class ValidationWarning {
final String path;
final String message;
final String? suggestion;
const ValidationWarning({
required this.path,
required this.message,
this.suggestion,
});
@override
String toString() {
final buffer = StringBuffer();
buffer.write('[WARNING] $path: $message');
if (suggestion != null) {
buffer.write(' (建议: $suggestion)');
}
return buffer.toString();
}
}
///
enum ValidationErrorType {
required,
format,
type,
reference,
constraint,
compatibility,
security,
}
/// Schema
class SchemaValidator {
final List<ValidationError> _errors = [];
final List<ValidationWarning> _warnings = [];
/// OpenAPI
ValidationResult validateDocument(SwaggerDocument document) {
_errors.clear();
_warnings.clear();
//
_validateInfo(document);
//
_validateServers(document.servers);
//
_validatePaths(document.paths);
//
_validateComponents(document.components);
//
_validateSecurity(document.security, document.components.securitySchemes);
return ValidationResult(
isValid: _errors.isEmpty,
errors: List.from(_errors),
warnings: List.from(_warnings),
);
}
///
void _validateInfo(SwaggerDocument document) {
if (document.title.isEmpty) {
_errors.add(const ValidationError(
path: 'info.title',
message: 'API 标题不能为空',
type: ValidationErrorType.required,
suggestion: '请提供有意义的 API 标题',
));
}
if (document.version.isEmpty) {
_errors.add(const ValidationError(
path: 'info.version',
message: 'API 版本不能为空',
type: ValidationErrorType.required,
suggestion: '请使用语义化版本号,如 "1.0.0"',
));
}
if (document.description.isEmpty) {
_warnings.add(const ValidationWarning(
path: 'info.description',
message: 'API 描述为空',
suggestion: '建议添加 API 的详细描述',
));
}
}
///
void _validateServers(List<ApiServer> servers) {
if (servers.isEmpty) {
_warnings.add(const ValidationWarning(
path: 'servers',
message: '未定义服务器配置',
suggestion: '建议添加至少一个服务器配置',
));
return;
}
for (int i = 0; i < servers.length; i++) {
final server = servers[i];
final path = 'servers[$i]';
if (server.url.isEmpty) {
_errors.add(ValidationError(
path: '$path.url',
message: '服务器 URL 不能为空',
type: ValidationErrorType.required,
));
} else if (!_isValidUrl(server.url)) {
_errors.add(ValidationError(
path: '$path.url',
message: '服务器 URL 格式无效: ${server.url}',
type: ValidationErrorType.format,
suggestion: '请使用有效的 URL 格式,如 "https://api.example.com"',
));
}
//
server.variables.forEach((name, variable) {
if (variable.defaultValue.isEmpty) {
_errors.add(ValidationError(
path: '$path.variables.$name.default',
message: '服务器变量必须有默认值',
type: ValidationErrorType.required,
));
}
});
}
}
///
void _validatePaths(Map<String, ApiPath> paths) {
if (paths.isEmpty) {
_errors.add(const ValidationError(
path: 'paths',
message: 'API 文档必须包含至少一个路径',
type: ValidationErrorType.required,
));
return;
}
paths.forEach((pathPattern, path) {
final pathKey = 'paths["$pathPattern"][${path.method.value}]';
_validatePath(path, pathKey);
});
}
///
void _validatePath(ApiPath path, String pathKey) {
// ID
if (path.operationId.isEmpty) {
_warnings.add(ValidationWarning(
path: '$pathKey.operationId',
message: '缺少操作 ID',
suggestion: '建议为每个操作添加唯一的 operationId',
));
}
//
if (path.summary.isEmpty) {
_warnings.add(ValidationWarning(
path: '$pathKey.summary',
message: '缺少操作摘要',
suggestion: '建议添加简短的操作描述',
));
}
//
for (int i = 0; i < path.parameters.length; i++) {
_validateParameter(path.parameters[i], '$pathKey.parameters[$i]');
}
//
if (path.requestBody != null) {
_validateRequestBody(path.requestBody!, '$pathKey.requestBody');
}
//
if (path.responses.isEmpty) {
_errors.add(ValidationError(
path: '$pathKey.responses',
message: '操作必须定义至少一个响应',
type: ValidationErrorType.required,
));
} else {
path.responses.forEach((code, response) {
_validateResponse(response, '$pathKey.responses["$code"]');
});
}
//
for (int i = 0; i < path.security.length; i++) {
_validateSecurityRequirement(path.security[i], '$pathKey.security[$i]');
}
}
///
void _validateParameter(ApiParameter parameter, String path) {
if (parameter.name.isEmpty) {
_errors.add(ValidationError(
path: '$path.name',
message: '参数名称不能为空',
type: ValidationErrorType.required,
));
}
//
if (parameter.location == ParameterLocation.path && !parameter.required) {
_errors.add(ValidationError(
path: '$path.required',
message: '路径参数必须是必需的',
type: ValidationErrorType.constraint,
));
}
//
if (parameter.type == PropertyType.unknown) {
_warnings.add(ValidationWarning(
path: '$path.type',
message: '参数类型未知',
suggestion: '建议明确指定参数类型',
));
}
}
///
void _validateRequestBody(ApiRequestBody requestBody, String path) {
if (requestBody.content.isEmpty) {
_errors.add(ValidationError(
path: '$path.content',
message: '请求体必须定义至少一种内容类型',
type: ValidationErrorType.required,
));
}
requestBody.content.forEach((mediaType, content) {
_validateMediaType(content, '$path.content["$mediaType"]', mediaType);
});
}
///
void _validateResponse(ApiResponse response, String path) {
if (response.description.isEmpty) {
_warnings.add(ValidationWarning(
path: '$path.description',
message: '响应缺少描述',
suggestion: '建议为响应添加描述',
));
}
response.content.forEach((mediaType, content) {
_validateMediaType(content, '$path.content["$mediaType"]', mediaType);
});
}
///
void _validateMediaType(
ApiMediaType mediaType, String path, String contentType) {
// schema
if (mediaType.schema == null) {
_warnings.add(ValidationWarning(
path: '$path.schema',
message: '媒体类型缺少 schema 定义',
suggestion: '建议为媒体类型添加 schema',
));
}
// multipart form data
if (contentType.startsWith('multipart/') || contentType.contains('form')) {
if (mediaType.encoding.isEmpty) {
_warnings.add(ValidationWarning(
path: '$path.encoding',
message: '表单数据建议定义编码信息',
suggestion: '为文件上传字段添加 contentType 等编码信息',
));
}
}
}
///
void _validateComponents(ApiComponents? components) {
if (components == null) return;
// schemas
components.schemas.forEach((name, model) {
_validateModel(model, 'components.schemas["$name"]');
});
//
components.securitySchemes.forEach((name, scheme) {
_validateSecurityScheme(scheme, 'components.securitySchemes["$name"]');
});
}
///
void _validateModel(ApiModel model, String path) {
if (model.name.isEmpty) {
_errors.add(ValidationError(
path: '$path.name',
message: '模型名称不能为空',
type: ValidationErrorType.required,
));
}
//
model.properties.forEach((name, property) {
_validateProperty(property, '$path.properties["$name"]');
});
//
for (final requiredField in model.required) {
if (!model.properties.containsKey(requiredField)) {
_errors.add(ValidationError(
path: '$path.required',
message: '必需字段 "$requiredField" 在属性中未定义',
type: ValidationErrorType.reference,
));
}
}
}
///
void _validateProperty(ApiProperty property, String path) {
if (property.name.isEmpty) {
_errors.add(ValidationError(
path: '$path.name',
message: '属性名称不能为空',
type: ValidationErrorType.required,
));
}
if (property.type == PropertyType.unknown) {
_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++) {
_validateSecurityRequirement(security[i], 'security[$i]');
}
}
///
void _validateSecurityRequirement(
ApiSecurityRequirement requirement, String path) {
for (final schemeName in requirement.schemeNames) {
// components.securitySchemes
//
if (schemeName.isEmpty) {
_warnings.add(ValidationWarning(
path: path,
message: '安全方案名称为空',
suggestion: '请确保安全方案名称有效',
));
}
}
}
///
void _validateSecurityScheme(ApiSecurityScheme scheme, String path) {
switch (scheme.type) {
case SecuritySchemeType.apiKey:
if (scheme.name == null || scheme.name!.isEmpty) {
_errors.add(ValidationError(
path: '$path.name',
message: 'API Key 安全方案必须指定参数名称',
type: ValidationErrorType.required,
));
}
break;
case SecuritySchemeType.http:
if (scheme.scheme == null || scheme.scheme!.isEmpty) {
_errors.add(ValidationError(
path: '$path.scheme',
message: 'HTTP 安全方案必须指定认证方案',
type: ValidationErrorType.required,
));
}
break;
case SecuritySchemeType.oauth2:
if (scheme.flows == null) {
_errors.add(ValidationError(
path: '$path.flows',
message: 'OAuth2 安全方案必须定义流程',
type: ValidationErrorType.required,
));
}
break;
case SecuritySchemeType.openIdConnect:
if (scheme.openIdConnectUrl == null ||
scheme.openIdConnectUrl!.isEmpty) {
_errors.add(ValidationError(
path: '$path.openIdConnectUrl',
message: 'OpenID Connect 安全方案必须指定 URL',
type: ValidationErrorType.required,
));
}
break;
}
}
/// URL
bool _isValidUrl(String url) {
try {
final uri = Uri.parse(url);
return uri.hasScheme && (uri.scheme == 'http' || uri.scheme == 'https');
} catch (e) {
return false;
}
}
///
void validateDocumentStructure(SwaggerDocument document) {
_validateOpenApiVersion(document);
_validatePathStructure(document);
_validateComponentReferences(document);
_validateSecurityReferences(document);
_validateExampleConsistency(document);
_validateResponseStructure(document);
_validateParameterConsistency(document);
}
/// OpenAPI
void _validateOpenApiVersion(SwaggerDocument document) {
// SwaggerDocument openApiVersion
// OpenAPI 3.0
_warnings.add(const ValidationWarning(
path: 'openapi',
message: '无法验证 OpenAPI 版本',
suggestion: '确保使用 OpenAPI 3.0.x 或 3.1.x 版本',
));
}
///
void _validatePathStructure(SwaggerDocument document) {
final pathPatterns = document.paths.keys.toList();
//
for (int i = 0; i < pathPatterns.length; i++) {
for (int j = i + 1; j < pathPatterns.length; j++) {
if (_pathsConflict(pathPatterns[i], pathPatterns[j])) {
_errors.add(ValidationError(
path: 'paths',
message: '路径冲突: "${pathPatterns[i]}" 与 "${pathPatterns[j]}"',
type: ValidationErrorType.constraint,
suggestion: '确保路径模式不会产生歧义',
));
}
}
}
//
document.paths.forEach((pathPattern, path) {
final pathParams = _extractPathParameters(pathPattern);
final declaredParams = path.parameters
.where((p) => p.location == ParameterLocation.path)
.map((p) => p.name)
.toSet();
//
for (final param in pathParams) {
if (!declaredParams.contains(param)) {
_errors.add(ValidationError(
path: 'paths["$pathPattern"][${path.method.value}].parameters',
message: '路径参数 "$param" 未在参数列表中声明',
type: ValidationErrorType.reference,
suggestion: '添加路径参数的声明',
));
}
}
// 使
for (final param in declaredParams) {
if (!pathParams.contains(param)) {
_warnings.add(ValidationWarning(
path: 'paths["$pathPattern"][${path.method.value}].parameters',
message: '声明的路径参数 "$param" 未在路径中使用',
suggestion: '移除未使用的参数声明或修正路径',
));
}
}
});
}
///
void _validateComponentReferences(SwaggerDocument document) {
final schemas = document.components.schemas.keys.toSet();
final securitySchemes = document.components.securitySchemes.keys.toSet();
//
final schemaRefs = <String>{};
final securityRefs = <String>{};
_collectReferences(document, schemaRefs, securityRefs);
//
for (final ref in schemaRefs) {
if (!schemas.contains(ref)) {
_errors.add(ValidationError(
path: 'components.schemas',
message: '引用的 schema "$ref" 未定义',
type: ValidationErrorType.reference,
suggestion: '定义缺失的 schema 或修正引用',
));
}
}
for (final ref in securityRefs) {
if (!securitySchemes.contains(ref)) {
_errors.add(ValidationError(
path: 'components.securitySchemes',
message: '引用的安全方案 "$ref" 未定义',
type: ValidationErrorType.reference,
suggestion: '定义缺失的安全方案或修正引用',
));
}
}
// 使
for (final schema in schemas) {
if (!schemaRefs.contains(schema)) {
_warnings.add(ValidationWarning(
path: 'components.schemas["$schema"]',
message: 'Schema "$schema" 已定义但未被使用',
suggestion: '移除未使用的 schema 或添加引用',
));
}
}
}
///
void _validateSecurityReferences(SwaggerDocument document) {
final definedSchemes = document.components.securitySchemes.keys.toSet();
//
for (int i = 0; i < document.security.length; i++) {
final requirement = document.security[i];
for (final schemeName in requirement.schemeNames) {
if (!definedSchemes.contains(schemeName)) {
_errors.add(ValidationError(
path: 'security[$i]',
message: '引用的安全方案 "$schemeName" 未定义',
type: ValidationErrorType.reference,
suggestion: '在 components.securitySchemes 中定义该安全方案',
));
}
}
}
//
document.paths.forEach((pathPattern, path) {
for (int i = 0; i < path.security.length; i++) {
final requirement = path.security[i];
for (final schemeName in requirement.schemeNames) {
if (!definedSchemes.contains(schemeName)) {
_errors.add(ValidationError(
path: 'paths["$pathPattern"][${path.method.value}].security[$i]',
message: '引用的安全方案 "$schemeName" 未定义',
type: ValidationErrorType.reference,
suggestion: '在 components.securitySchemes 中定义该安全方案',
));
}
}
}
});
}
///
void _validateExampleConsistency(SwaggerDocument document) {
document.paths.forEach((pathPattern, path) {
//
if (path.requestBody != null) {
path.requestBody!.content.forEach((mediaType, content) {
_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"]');
});
});
});
}
///
void _validateMediaTypeExamples(ApiMediaType mediaType, String path) {
// example examples
if (mediaType.example != null && mediaType.examples.isNotEmpty) {
_warnings.add(ValidationWarning(
path: path,
message: 'example 和 examples 不应同时存在',
suggestion: '使用 examples 对象来提供多个示例',
));
}
//
if (mediaType.example != null && mediaType.schema != null) {
// TODO: schema example
}
}
///
void _validateResponseStructure(SwaggerDocument document) {
document.paths.forEach((pathPattern, path) {
//
final hasSuccessResponse = path.responses.keys.any((code) {
final statusCode = int.tryParse(code) ?? 0;
return statusCode >= 200 && statusCode < 300;
});
if (!hasSuccessResponse) {
_warnings.add(ValidationWarning(
path: 'paths["$pathPattern"][${path.method.value}].responses',
message: '缺少成功响应 (2xx)',
suggestion: '添加至少一个成功响应',
));
}
//
final hasErrorResponse = path.responses.keys.any((code) {
final statusCode = int.tryParse(code) ?? 0;
return statusCode >= 400;
});
if (!hasErrorResponse) {
_warnings.add(ValidationWarning(
path: 'paths["$pathPattern"][${path.method.value}].responses',
message: '建议添加错误响应 (4xx/5xx)',
suggestion: '添加常见的错误响应,如 400、401、404、500',
));
}
});
}
///
void _validateParameterConsistency(SwaggerDocument document) {
final parameterNames = <String, Set<String>>{};
document.paths.forEach((pathPattern, path) {
for (final param in path.parameters) {
final key = '${param.location.name}:${param.name}';
parameterNames.putIfAbsent(pathPattern, () => <String>{});
if (parameterNames[pathPattern]!.contains(key)) {
_errors.add(ValidationError(
path: 'paths["$pathPattern"][${path.method.value}].parameters',
message: '重复的参数: ${param.name} (${param.location.name})',
type: ValidationErrorType.constraint,
suggestion: '确保参数名称在同一位置类型中唯一',
));
} else {
parameterNames[pathPattern]!.add(key);
}
}
});
}
///
bool _pathsConflict(String path1, String path2) {
if (path1 == path2) return true;
//
final normalized1 = path1.replaceAll(RegExp(r'\{[^}]+\}'), '*');
final normalized2 = path2.replaceAll(RegExp(r'\{[^}]+\}'), '*');
return normalized1 == normalized2;
}
///
Set<String> _extractPathParameters(String path) {
final regex = RegExp(r'\{([^}]+)\}');
final matches = regex.allMatches(path);
return matches.map((match) => match.group(1)!).toSet();
}
///
void _collectReferences(SwaggerDocument document, Set<String> schemaRefs,
Set<String> securityRefs) {
//
document.paths.forEach((pathPattern, path) {
//
for (final _ in path.parameters) {
// TODO: schema
}
//
if (path.requestBody != null) {
path.requestBody!.content.forEach((mediaType, content) {
_collectSchemaReferences(content.schema, schemaRefs);
});
}
//
path.responses.forEach((code, response) {
response.content.forEach((mediaType, content) {
_collectSchemaReferences(content.schema, schemaRefs);
});
});
//
for (final requirement in path.security) {
securityRefs.addAll(requirement.schemeNames);
}
});
//
for (final requirement in document.security) {
securityRefs.addAll(requirement.schemeNames);
}
//
document.components.schemas.forEach((name, model) {
for (final _ in model.properties.values) {
// TODO: schema
}
});
}
/// Schema
void _collectSchemaReferences(
Map<String, dynamic>? schema, Set<String> refs) {
if (schema == null) return;
// $ref
final ref = schema['\$ref'] as String?;
if (ref != null && ref.startsWith('#/components/schemas/')) {
final refName = ref.substring('#/components/schemas/'.length);
refs.add(refName);
}
// schema
if (schema['items'] is Map<String, dynamic>) {
_collectSchemaReferences(schema['items'] as Map<String, dynamic>, refs);
}
if (schema['properties'] is Map<String, dynamic>) {
final properties = schema['properties'] as Map<String, dynamic>;
properties.forEach((key, value) {
if (value is Map<String, dynamic>) {
_collectSchemaReferences(value, refs);
}
});
}
// schema
for (final key in ['allOf', 'oneOf', 'anyOf']) {
if (schema[key] is List) {
final list = schema[key] as List;
for (final item in list) {
if (item is Map<String, dynamic>) {
_collectSchemaReferences(item, refs);
}
}
}
}
}
}

View File

@ -185,6 +185,22 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.0"
dio:
dependency: "direct dev"
description:
name: dio
sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.8.0+1"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.1"
fake_async:
dependency: transitive
description:
@ -324,7 +340,7 @@ packages:
source: hosted
version: "3.0.1"
logging:
dependency: transitive
dependency: "direct main"
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
@ -380,7 +396,7 @@ packages:
source: hosted
version: "2.2.0"
path:
dependency: transitive
dependency: "direct main"
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
@ -411,6 +427,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.5.0"
retrofit:
dependency: "direct dev"
description:
name: retrofit
sha256: "84d70114a5b6bae5f4c1302335f9cb610ebeb1b02023d5e7e87697aaff52926a"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.6.0"
shelf:
dependency: transitive
description:

View File

@ -20,16 +20,53 @@ show_help() {
echo -e "${YELLOW}用法: $0 [命令] [选项]${NC}"
echo ""
echo -e "${GREEN}快速命令:${NC}"
echo -e " $0 all # 生成所有文件"
echo -e " $0 all # 生成所有文件(推荐)"
echo -e " $0 models # 生成数据模型"
echo -e " $0 docs # 生成API文档"
echo -e " $0 api # 生成Retrofit API"
echo ""
echo -e "${GREEN}工具命令:${NC}"
echo -e " $0 clean # 清理生成的文件"
echo -e " $0 validate # 验证生成的代码"
echo -e " $0 format # 格式化代码"
echo ""
echo -e "${GREEN}直接使用:${NC}"
echo -e " dart run bin/main.dart generate --help"
echo ""
}
# 检查必要的工具
check_prerequisites() {
if ! command -v dart &> /dev/null; then
echo -e "${YELLOW}❌ Dart SDK 未安装或不在 PATH 中${NC}"
exit 1
fi
if [ ! -f "$CLI_DART_FILE" ]; then
echo -e "${YELLOW}❌ CLI 文件不存在: $CLI_DART_FILE${NC}"
exit 1
fi
}
# 执行生成并格式化
generate_and_format() {
local cmd="$1"
echo -e "${CYAN}🚀 执行: $cmd${NC}"
if eval "$cmd"; then
echo -e "${CYAN}🔧 修复和排序 imports...${NC}"
dart fix --apply
echo -e "${CYAN}🎨 格式化代码...${NC}"
dart format .
echo -e "${GREEN}✅ 生成完成!${NC}"
else
echo -e "${YELLOW}❌ 生成失败${NC}"
exit 1
fi
}
# 主函数
main() {
if [ $# -eq 0 ] || [ "$1" = "help" ] || [ "$1" = "--help" ]; then
@ -37,26 +74,50 @@ main() {
exit 0
fi
# 检查必要工具(除了 clean 命令)
if [ "$1" != "clean" ]; then
check_prerequisites
fi
case "$1" in
all)
dart run "$CLI_DART_FILE" generate --models --api --split-by-tags
dart format .
dart fix --apply
dart run "$CLI_DART_FILE" generate --models --api
dart fix --apply # 先修复和排序 imports
dart format . # 再格式化代码
;;
models)
dart run "$CLI_DART_FILE" generate --models
dart format .
dart fix --apply
dart fix --apply # 先修复和排序 imports
dart format . # 再格式化代码
;;
docs)
dart run "$CLI_DART_FILE" generate --docs
dart format .
dart fix --apply
dart fix --apply # 先修复和排序 imports
dart format . # 再格式化代码
;;
api)
dart run "$CLI_DART_FILE" generate --api
dart format .
dart fix --apply
dart fix --apply # 先修复和排序 imports
dart format . # 再格式化代码
;;
clean)
echo -e "${CYAN}🧹 清理生成的文件...${NC}"
rm -rf generator/
echo -e "${GREEN}✅ 清理完成${NC}"
;;
validate)
echo -e "${CYAN}🔍 验证生成的代码...${NC}"
if [ -f "validate.sh" ]; then
./validate.sh
else
echo -e "${YELLOW}⚠️ 验证脚本不存在,请先运行: chmod +x validate.sh${NC}"
fi
;;
format)
echo -e "${CYAN}🎨 格式化代码...${NC}"
dart fix --apply # 先修复和排序 imports
dart format . # 再格式化代码
echo -e "${GREEN}✅ 格式化完成${NC}"
;;
*)
echo -e "${YELLOW}未知命令: $1${NC}"

12902
swagger.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,613 @@
import 'package:swagger_generator_flutter/core/models.dart';
import 'package:swagger_generator_flutter/generators/optimized_retrofit_generator.dart';
import 'package:swagger_generator_flutter/generators/performance_generator.dart';
import 'package:swagger_generator_flutter/generators/retrofit_api_generator.dart';
import 'package:test/test.dart';
void main() {
group('Comprehensive Generator Tests', () {
late SwaggerDocument testDocument;
setUp(() {
testDocument = const SwaggerDocument(
title: 'Test API',
version: '1.0.0',
description: 'A comprehensive test API',
servers: [
ApiServer(
url: 'https://api.example.com',
description: 'Production server',
),
],
components: ApiComponents(
schemas: {},
securitySchemes: {
'bearerAuth': const ApiSecurityScheme(
type: SecuritySchemeType.http,
description: 'Bearer token',
scheme: 'bearer',
bearerFormat: 'JWT',
),
'apiKey': const ApiSecurityScheme(
type: SecuritySchemeType.apiKey,
description: 'API Key',
name: 'X-API-Key',
location: ApiKeyLocation.header,
),
},
),
paths: {
'/users': const ApiPath(
path: '/users',
method: HttpMethod.get,
summary: 'Get all users',
description: 'Retrieve a list of all users',
operationId: 'getUsers',
tags: ['users'],
parameters: [
ApiParameter(
name: 'page',
location: ParameterLocation.query,
required: false,
type: PropertyType.integer,
description: 'Page number',
),
ApiParameter(
name: 'limit',
location: ParameterLocation.query,
required: false,
type: PropertyType.integer,
description: 'Items per page',
),
],
responses: {
'200': const ApiResponse(
code: '200',
description: 'Successful response',
content: {
'application/json': const ApiMediaType(
schema: {
'type': 'array',
'items': {
'\$ref': '#/components/schemas/User',
},
},
),
},
),
'400': const ApiResponse(
code: '400',
description: 'Bad request',
),
'401': const ApiResponse(
code: '401',
description: 'Unauthorized',
),
},
security: [
ApiSecurityRequirement(
requirements: {'bearerAuth': []},
),
],
),
'/users/{id}': const ApiPath(
path: '/users/{id}',
method: HttpMethod.get,
summary: 'Get user by ID',
description: 'Retrieve a specific user by their ID',
operationId: 'getUserById',
tags: ['users'],
parameters: [
ApiParameter(
name: 'id',
location: ParameterLocation.path,
required: true,
type: PropertyType.integer,
description: 'User ID',
),
],
responses: {
'200': const ApiResponse(
code: '200',
description: 'User found',
content: {
'application/json': const ApiMediaType(
schema: {
'\$ref': '#/components/schemas/User',
},
),
},
),
'404': const ApiResponse(
code: '404',
description: 'User not found',
),
},
),
'/users/create': const ApiPath(
path: '/users/create',
method: HttpMethod.post,
summary: 'Create user',
description: 'Create a new user',
operationId: 'createUser',
tags: ['users'],
parameters: [],
requestBody: ApiRequestBody(
description: 'User data',
required: true,
content: {
'application/json': const ApiMediaType(
schema: {
'\$ref': '#/components/schemas/CreateUserRequest',
},
),
},
),
responses: {
'201': const ApiResponse(
code: '201',
description: 'User created',
content: {
'application/json': const ApiMediaType(
schema: {
'\$ref': '#/components/schemas/User',
},
),
},
),
'400': const ApiResponse(
code: '400',
description: 'Invalid input',
),
},
),
'/files/upload': const ApiPath(
path: '/files/upload',
method: HttpMethod.post,
summary: 'Upload file',
description: 'Upload a file to the server',
operationId: 'uploadFile',
tags: ['files'],
parameters: [],
requestBody: ApiRequestBody(
description: 'File to upload',
required: true,
content: {
'multipart/form-data': const ApiMediaType(
schema: {
'type': 'object',
'properties': {
'file': {
'type': 'string',
'format': 'binary',
},
'description': {
'type': 'string',
},
},
},
),
},
),
responses: {
'200': const ApiResponse(
code: '200',
description: 'File uploaded successfully',
content: {
'application/json': const ApiMediaType(
schema: {
'\$ref': '#/components/schemas/FileUploadResult',
},
),
},
),
},
),
},
models: {
'User': const ApiModel(
name: 'User',
description: 'User model',
properties: {
'id': const ApiProperty(
name: 'id',
type: PropertyType.integer,
description: 'User ID',
required: true,
),
'name': const ApiProperty(
name: 'name',
type: PropertyType.string,
description: 'User name',
required: true,
),
'email': const ApiProperty(
name: 'email',
type: PropertyType.string,
description: 'User email',
required: true,
),
'createdAt': const ApiProperty(
name: 'createdAt',
type: PropertyType.string,
description: 'Creation timestamp',
required: false,
),
},
required: ['id', 'name', 'email'],
),
'CreateUserRequest': const ApiModel(
name: 'CreateUserRequest',
description: 'Request model for creating a user',
properties: {
'name': const ApiProperty(
name: 'name',
type: PropertyType.string,
description: 'User name',
required: true,
),
'email': const ApiProperty(
name: 'email',
type: PropertyType.string,
description: 'User email',
required: true,
),
},
required: ['name', 'email'],
),
'FileUploadResult': const ApiModel(
name: 'FileUploadResult',
description: 'Result of file upload',
properties: {
'url': const ApiProperty(
name: 'url',
type: PropertyType.string,
description: 'File URL',
required: true,
),
'filename': const ApiProperty(
name: 'filename',
type: PropertyType.string,
description: 'Original filename',
required: true,
),
'size': const ApiProperty(
name: 'size',
type: PropertyType.integer,
description: 'File size in bytes',
required: true,
),
},
required: ['url', 'filename', 'size'],
),
},
controllers: {},
security: [
ApiSecurityRequirement(
requirements: {'bearerAuth': []},
),
],
);
});
group('RetrofitApiGenerator', () {
test('generates basic Retrofit API', () {
final generator = RetrofitApiGenerator(
className: 'TestApiService',
splitByTags: false,
);
final result = generator.generateFromDocument(testDocument);
expect(result, isNotEmpty);
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('@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'));
expect(result, contains('class ApiService'));
expect(result, contains('late final UsersApi users'));
expect(result, contains('late final FilesApi files'));
});
test('handles file upload endpoints', () {
final generator = RetrofitApiGenerator();
final result = generator.generateFromDocument(testDocument);
expect(result, contains('@POST(\'/files/upload\')'));
expect(result, contains('@MultiPart()'));
expect(result, contains('MultipartFile'));
});
test('generates proper parameter annotations', () {
final generator = RetrofitApiGenerator();
final result = generator.generateFromDocument(testDocument);
// Path parameters
expect(result, contains('@Path(\'id\') int id'));
// Query parameters
expect(result, contains('@Query(\'page\') int? page'));
expect(result, contains('@Query(\'limit\') int? limit'));
// Body parameters
expect(result, contains('@Body() CreateUserRequest body'));
});
test('generates security annotations', () {
final generator = RetrofitApiGenerator();
final result = generator.generateFromDocument(testDocument);
// Should include headers for authentication
expect(
result, anyOf([contains('Authorization'), contains('X-API-Key')]));
});
});
group('OptimizedRetrofitGenerator', () {
test('generates optimized code with base types', () {
final generator = OptimizedRetrofitGenerator(
className: 'OptimizedApiService',
generateModularApis: true,
generateBaseResult: true,
generatePagination: true,
generateFileUpload: true,
);
final result = generator.generateFromDocument(testDocument);
expect(result, isNotEmpty);
expect(result, contains('class BaseResult<T>'));
expect(result, contains('class BasePageResult<T>'));
expect(result, contains('class FileUploadRequest'));
expect(result, contains('class FileUploadResult'));
expect(result, contains('class ApiUtils'));
});
test('generates modular APIs', () {
final generator = OptimizedRetrofitGenerator(
generateModularApis: true,
);
final result = generator.generateFromDocument(testDocument);
expect(result, contains('class UsersApi'));
expect(result, contains('class FilesApi'));
expect(result, contains('class ApiService'));
});
test('generates single API when modular is disabled', () {
final generator = OptimizedRetrofitGenerator(
generateModularApis: false,
className: 'SingleApiService',
);
final result = generator.generateFromDocument(testDocument);
expect(result, contains('abstract class SingleApiService'));
expect(result, isNot(contains('class UsersApi')));
expect(result, isNot(contains('class FilesApi')));
});
test('includes utility methods', () {
final generator = OptimizedRetrofitGenerator(
generateFileUpload: true,
generatePagination: true,
);
final result = generator.generateFromDocument(testDocument);
expect(result, contains('class ApiUtils'));
expect(result, contains('createFileUpload'));
expect(result, contains('createPageParam'));
});
});
group('PerformanceGenerator', () {
test('generates code with performance tracking', () async {
final generator = PerformanceGenerator(
maxConcurrency: 2,
enableCaching: true,
enableParallel: true,
);
final result = await generator.generateFromDocument(testDocument);
expect(result, isNotEmpty);
expect(result, contains('Generated API for Test API'));
expect(result, contains('class User'));
expect(result, contains('CommonApi'));
final stats = generator.getStats();
expect(stats.totalTasks, greaterThan(0));
expect(stats.completedTasks, greaterThan(0));
});
test('caching improves performance', () async {
final generator = PerformanceGenerator(
enableCaching: true,
);
// First generation
final stopwatch1 = Stopwatch()..start();
await generator.generateFromDocument(testDocument);
stopwatch1.stop();
// Second generation (should use cache)
final stopwatch2 = Stopwatch()..start();
await generator.generateFromDocument(testDocument);
stopwatch2.stop();
// Second should be faster due to caching
expect(stopwatch2.elapsedMicroseconds,
lessThan(stopwatch1.elapsedMicroseconds));
final cacheStats = generator.getCacheStats();
expect(cacheStats.hits, greaterThan(0));
});
});
group('Code Quality', () {
test('generated code is valid Dart syntax', () {
final generator = RetrofitApiGenerator();
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\';'));
// 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));
});
test('handles special characters in names', () {
const specialDocument = SwaggerDocument(
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(
path: '/special-endpoint_with.dots',
method: HttpMethod.get,
summary: 'Special endpoint',
description: 'Endpoint with special characters',
operationId: 'getSpecialEndpoint',
tags: ['special-tag_with.dots'],
parameters: [],
responses: {
'200': const ApiResponse(
code: '200',
description: 'Success',
),
},
),
},
models: {},
controllers: {},
security: [],
);
final generator = RetrofitApiGenerator();
final result = generator.generateFromDocument(specialDocument);
expect(result, isNotEmpty);
// Should handle special characters in class names
expect(result, contains('class'));
});
test('generates proper JSON annotations', () {
final generator = OptimizedRetrofitGenerator(
generateBaseResult: true,
);
final result = generator.generateFromDocument(testDocument);
expect(result, contains('@JsonSerializable()'));
expect(result, contains('fromJson'));
expect(result, contains('toJson'));
expect(result, contains('_\$'));
});
test('handles nullable and required fields correctly', () {
final generator = RetrofitApiGenerator();
final result = generator.generateFromDocument(testDocument);
// Required path parameters should not be nullable
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'));
});
});
group('Error Handling', () {
test('handles empty document gracefully', () {
const emptyDocument = SwaggerDocument(
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
});
test('handles missing operation IDs', () {
const documentWithoutOperationIds = SwaggerDocument(
title: 'Test API',
version: '1.0.0',
description: 'Test',
servers: [],
components: ApiComponents(schemas: {}, securitySchemes: {}),
paths: {
'/test': const ApiPath(
path: '/test',
method: HttpMethod.get,
summary: 'Test endpoint',
description: 'Test',
operationId: '', // Empty operation ID
tags: [],
parameters: [],
responses: {
'200': const ApiResponse(
code: '200',
description: 'Success',
),
},
),
},
models: {},
controllers: {},
security: [],
);
final generator = RetrofitApiGenerator();
expect(
() => generator.generateFromDocument(documentWithoutOperationIds),
returnsNormally);
});
});
});
}

View File

@ -0,0 +1,623 @@
import 'package:swagger_generator_flutter/core/models.dart';
import 'package:test/test.dart';
void main() {
group('Comprehensive Parser Tests', () {
group('OpenAPI 3.0 Core Features', () {
test('parses basic OpenAPI 3.0 document', () {
final json = {
'openapi': '3.0.3',
'info': {
'title': 'Test API',
'version': '1.0.0',
'description': 'A test API',
},
'servers': [
{
'url': 'https://api.example.com',
'description': 'Production server',
},
],
'paths': {
'/users': {
'get': {
'summary': 'Get users',
'operationId': 'getUsers',
'responses': {
'200': {
'description': 'Success',
'content': {
'application/json': {
'schema': {
'type': 'array',
'items': {
'\$ref': '#/components/schemas/User',
},
},
},
},
},
},
},
},
},
'components': {
'schemas': {
'User': {
'type': 'object',
'properties': {
'id': {
'type': 'integer',
'format': 'int64',
},
'name': {
'type': 'string',
},
},
'required': ['id', 'name'],
},
},
},
};
final document = SwaggerDocument.fromJson(json);
expect(document.title, equals('Test API'));
expect(document.version, equals('1.0.0'));
expect(document.description, equals('A test API'));
expect(document.servers, hasLength(1));
expect(document.servers.first.url, equals('https://api.example.com'));
expect(document.paths, hasLength(1));
expect(document.models, hasLength(1));
expect(document.models.containsKey('User'), isTrue);
});
test('parses servers with variables', () {
final json = {
'openapi': '3.0.3',
'info': {'title': 'Test', 'version': '1.0.0'},
'servers': [
{
'url': 'https://{environment}.example.com/{basePath}',
'description': 'Configurable server',
'variables': {
'environment': {
'default': 'api',
'enum': ['api', 'staging', 'dev'],
'description': 'Environment name',
},
'basePath': {
'default': 'v1',
'description': 'API version',
},
},
},
],
'paths': {},
};
final document = SwaggerDocument.fromJson(json);
final server = document.servers.first;
expect(
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'));
expect(server.variables['environment']!.enumValues, hasLength(3));
});
test('parses complex request body', () {
final json = {
'openapi': '3.0.3',
'info': {'title': 'Test', 'version': '1.0.0'},
'paths': {
'/users': {
'post': {
'summary': 'Create user',
'requestBody': {
'description': 'User to create',
'required': true,
'content': {
'application/json': {
'schema': {
'\$ref': '#/components/schemas/User',
},
'examples': {
'user1': {
'summary': 'Example user',
'value': {
'name': 'John Doe',
'email': 'john@example.com',
},
},
},
},
'application/xml': {
'schema': {
'\$ref': '#/components/schemas/User',
},
},
},
},
'responses': {
'201': {'description': 'Created'},
},
},
},
},
'components': {
'schemas': {
'User': {
'type': 'object',
'properties': {
'name': {'type': 'string'},
'email': {'type': 'string'},
},
},
},
},
};
final document = SwaggerDocument.fromJson(json);
final path = document.paths['/users']!;
expect(path.requestBody, isNotNull);
expect(path.requestBody!.required, isTrue);
expect(path.requestBody!.content, hasLength(2));
expect(
path.requestBody!.content.containsKey('application/json'), isTrue);
expect(
path.requestBody!.content.containsKey('application/xml'), isTrue);
final jsonContent = path.requestBody!.content['application/json']!;
expect(jsonContent.examples, hasLength(1));
expect(jsonContent.examples.containsKey('user1'), isTrue);
});
test('parses complex responses with headers and links', () {
final json = {
'openapi': '3.0.3',
'info': {'title': 'Test', 'version': '1.0.0'},
'paths': {
'/users/{id}': {
'get': {
'summary': 'Get user',
'parameters': [
{
'name': 'id',
'in': 'path',
'required': true,
'schema': {'type': 'integer'},
},
],
'responses': {
'200': {
'description': 'User found',
'headers': {
'X-Rate-Limit': {
'description': 'Rate limit',
'schema': {'type': 'integer'},
},
},
'content': {
'application/json': {
'schema': {
'\$ref': '#/components/schemas/User',
},
},
},
'links': {
'getUserPosts': {
'operationId': 'getUserPosts',
'parameters': {
'userId': '\$response.body#/id',
},
},
},
},
'404': {
'description': 'User not found',
},
},
},
},
},
'components': {
'schemas': {
'User': {
'type': 'object',
'properties': {
'id': {'type': 'integer'},
'name': {'type': 'string'},
},
},
},
},
};
final document = SwaggerDocument.fromJson(json);
final path = document.paths['/users/{id}']!;
expect(path.parameters, hasLength(1));
expect(path.parameters.first.name, equals('id'));
expect(path.parameters.first.location, equals(ParameterLocation.path));
expect(path.parameters.first.required, isTrue);
expect(path.responses, hasLength(2));
final successResponse = path.responses['200']!;
expect(successResponse.headers, hasLength(1));
expect(successResponse.headers.containsKey('X-Rate-Limit'), isTrue);
expect(successResponse.links, hasLength(1));
expect(successResponse.links.containsKey('getUserPosts'), isTrue);
});
test('parses security schemes and requirements', () {
final json = {
'openapi': '3.0.3',
'info': {'title': 'Test', 'version': '1.0.0'},
'security': [
{'bearerAuth': []},
{'apiKey': []},
],
'paths': {
'/protected': {
'get': {
'summary': 'Protected endpoint',
'security': [
{
'bearerAuth': ['read:users']
},
],
'responses': {
'200': {'description': 'Success'},
},
},
},
},
'components': {
'securitySchemes': {
'bearerAuth': {
'type': 'http',
'scheme': 'bearer',
'bearerFormat': 'JWT',
},
'apiKey': {
'type': 'apiKey',
'in': 'header',
'name': 'X-API-Key',
},
'oauth2': {
'type': 'oauth2',
'flows': {
'authorizationCode': {
'authorizationUrl': 'https://example.com/oauth/authorize',
'tokenUrl': 'https://example.com/oauth/token',
'scopes': {
'read:users': 'Read user data',
'write:users': 'Write user data',
},
},
},
},
},
},
};
final document = SwaggerDocument.fromJson(json);
expect(document.security, hasLength(2));
expect(document.components.securitySchemes, hasLength(3));
final bearerAuth = document.components.securitySchemes['bearerAuth']!;
expect(bearerAuth.type, equals(SecuritySchemeType.http));
expect(bearerAuth.scheme, equals('bearer'));
expect(bearerAuth.bearerFormat, equals('JWT'));
final apiKey = document.components.securitySchemes['apiKey']!;
expect(apiKey.type, equals(SecuritySchemeType.apiKey));
expect(apiKey.location, equals(ApiKeyLocation.header));
expect(apiKey.name, equals('X-API-Key'));
final oauth2 = document.components.securitySchemes['oauth2']!;
expect(oauth2.type, equals(SecuritySchemeType.oauth2));
expect(oauth2.flows, isNotNull);
expect(oauth2.flows!.authorizationCode, isNotNull);
expect(oauth2.flows!.authorizationCode!.scopes, hasLength(2));
final path = document.paths['/protected']!;
expect(path.security, hasLength(1));
expect(path.security.first.schemeNames, contains('bearerAuth'));
});
});
group('Schema Validation', () {
test('parses allOf composition', () {
final json = {
'openapi': '3.0.3',
'info': {'title': 'Test', 'version': '1.0.0'},
'paths': {},
'components': {
'schemas': {
'Pet': {
'type': 'object',
'properties': {
'name': {'type': 'string'},
},
'required': ['name'],
},
'Dog': {
'allOf': [
{'\$ref': '#/components/schemas/Pet'},
{
'type': 'object',
'properties': {
'breed': {'type': 'string'},
},
'required': ['breed'],
},
],
},
},
},
};
final document = SwaggerDocument.fromJson(json);
expect(document.models, hasLength(2));
expect(document.models.containsKey('Pet'), isTrue);
expect(document.models.containsKey('Dog'), isTrue);
final dog = document.models['Dog']!;
expect(dog.name, equals('Dog'));
// allOf
});
test('parses oneOf and anyOf', () {
final json = {
'openapi': '3.0.3',
'info': {'title': 'Test', 'version': '1.0.0'},
'paths': {},
'components': {
'schemas': {
'StringOrNumber': {
'oneOf': [
{'type': 'string'},
{'type': 'number'},
],
},
'FlexibleType': {
'anyOf': [
{'type': 'string'},
{'type': 'integer'},
{'type': 'boolean'},
],
},
},
},
};
final document = SwaggerDocument.fromJson(json);
expect(document.models, hasLength(2));
expect(document.models.containsKey('StringOrNumber'), isTrue);
expect(document.models.containsKey('FlexibleType'), isTrue);
});
test('parses discriminator', () {
final json = {
'openapi': '3.0.3',
'info': {'title': 'Test', 'version': '1.0.0'},
'paths': {},
'components': {
'schemas': {
'Pet': {
'type': 'object',
'discriminator': {
'propertyName': 'petType',
'mapping': {
'dog': '#/components/schemas/Dog',
'cat': '#/components/schemas/Cat',
},
},
'properties': {
'petType': {'type': 'string'},
'name': {'type': 'string'},
},
'required': ['petType', 'name'],
},
'Dog': {
'allOf': [
{'\$ref': '#/components/schemas/Pet'},
{
'type': 'object',
'properties': {
'breed': {'type': 'string'},
},
},
],
},
'Cat': {
'allOf': [
{'\$ref': '#/components/schemas/Pet'},
{
'type': 'object',
'properties': {
'color': {'type': 'string'},
},
},
],
},
},
},
};
final document = SwaggerDocument.fromJson(json);
expect(document.models, hasLength(3));
final pet = document.models['Pet']!;
expect(pet.name, equals('Pet'));
// discriminator
});
});
group('Error Handling', () {
test('handles missing required fields gracefully', () {
final json = {
'openapi': '3.0.3',
// Missing info object
'paths': {},
};
expect(() => SwaggerDocument.fromJson(json),
throwsA(isA<FormatException>()));
});
test('handles invalid OpenAPI version', () {
final json = {
'openapi': '2.0', // Invalid version
'info': {'title': 'Test', 'version': '1.0.0'},
'paths': {},
};
// Should still parse but might have warnings
expect(() => SwaggerDocument.fromJson(json), returnsNormally);
});
test('handles malformed paths', () {
final json = {
'openapi': '3.0.3',
'info': {'title': 'Test', 'version': '1.0.0'},
'paths': {
'/valid': {
'get': {
'responses': {
'200': {'description': 'OK'}
},
},
},
'/invalid': {
'invalidMethod': {
'responses': {
'200': {'description': 'OK'}
},
},
},
},
};
final document = SwaggerDocument.fromJson(json);
// Should parse valid paths and skip invalid ones
expect(document.paths.length, greaterThanOrEqualTo(1));
});
test('handles circular references', () {
final json = {
'openapi': '3.0.3',
'info': {'title': 'Test', 'version': '1.0.0'},
'paths': {},
'components': {
'schemas': {
'Node': {
'type': 'object',
'properties': {
'value': {'type': 'string'},
'children': {
'type': 'array',
'items': {'\$ref': '#/components/schemas/Node'},
},
},
},
},
},
};
// Should handle circular references without infinite recursion
expect(() => SwaggerDocument.fromJson(json), returnsNormally);
final document = SwaggerDocument.fromJson(json);
expect(document.models.containsKey('Node'), isTrue);
});
});
group('Edge Cases', () {
test('handles empty document', () {
final json = {
'openapi': '3.0.3',
'info': {'title': 'Empty API', 'version': '1.0.0'},
'paths': {},
};
final document = SwaggerDocument.fromJson(json);
expect(document.title, equals('Empty API'));
expect(document.paths, isEmpty);
expect(document.models, isEmpty);
});
test('handles very large documents', () {
final paths = <String, dynamic>{};
final schemas = <String, dynamic>{};
// Create a large number of paths and schemas
for (int i = 0; i < 1000; i++) {
paths['/resource$i'] = {
'get': {
'summary': 'Get resource $i',
'responses': {
'200': {'description': 'Success'}
},
},
};
schemas['Model$i'] = {
'type': 'object',
'properties': {
'id': {'type': 'integer'},
'name': {'type': 'string'},
},
};
}
final json = {
'openapi': '3.0.3',
'info': {'title': 'Large API', 'version': '1.0.0'},
'paths': paths,
'components': {'schemas': schemas},
};
final stopwatch = Stopwatch()..start();
final document = SwaggerDocument.fromJson(json);
stopwatch.stop();
expect(document.paths.length, greaterThan(500));
expect(document.models.length, greaterThan(500));
expect(stopwatch.elapsedMilliseconds,
lessThan(10000)); // Should complete within 10 seconds
});
test('handles unicode and special characters', () {
final json = {
'openapi': '3.0.3',
'info': {
'title': 'API with 中文 and émojis 🚀',
'version': '1.0.0',
'description': 'Supports unicode: αβγ, 日本語, العربية',
},
'paths': {
'/测试': {
'get': {
'summary': 'Test with unicode path',
'responses': {
'200': {'description': 'Success'}
},
},
},
},
};
final document = SwaggerDocument.fromJson(json);
expect(document.title, contains('中文'));
expect(document.title, contains('🚀'));
expect(document.description, contains('日本語'));
expect(document.paths.containsKey('/测试'), isTrue);
});
});
});
}

217
tests/encoding_test.dart Normal file
View File

@ -0,0 +1,217 @@
import 'dart:convert';
import 'dart:io';
import 'package:test/test.dart';
void main() {
group('Encoding Support', () {
test('handles UTF-8 encoding', () {
const testString = 'Hello, 世界! 🌍';
final encoded = utf8.encode(testString);
final decoded = utf8.decode(encoded);
expect(decoded, testString);
expect(
encoded.length,
greaterThan(
testString.length)); // UTF-8 uses multiple bytes for non-ASCII
});
test('handles ASCII encoding', () {
const testString = 'Hello, World!';
final encoded = ascii.encode(testString);
final decoded = ascii.decode(encoded);
expect(decoded, testString);
expect(
encoded.length, testString.length); // ASCII is 1 byte per character
});
test('handles Latin1 encoding', () {
const testString = 'Café';
final encoded = latin1.encode(testString);
final decoded = latin1.decode(encoded);
expect(decoded, testString);
});
test('handles Base64 encoding and decoding', () {
const testString = 'Hello, World!';
final bytes = utf8.encode(testString);
final encoded = base64Encode(bytes);
final decoded = base64Decode(encoded);
final result = utf8.decode(decoded);
expect(result, testString);
expect(encoded, 'SGVsbG8sIFdvcmxkIQ==');
});
test('handles URL encoding and decoding', () {
const testString = 'Hello World & Special Characters!@#\$%^&*()';
final encoded = Uri.encodeComponent(testString);
final decoded = Uri.decodeComponent(encoded);
expect(decoded, testString);
expect(encoded, contains('%20')); // Space should be encoded as %20
expect(encoded, contains('%26')); // & should be encoded as %26
});
test('detects BOM for UTF-8', () {
final utf8Bom = [0xEF, 0xBB, 0xBF];
final testBytes = utf8Bom + utf8.encode('Hello');
// BOM
final bool hasUtf8Bom = testBytes.length >= 3 &&
testBytes[0] == 0xEF &&
testBytes[1] == 0xBB &&
testBytes[2] == 0xBF;
expect(hasUtf8Bom, true);
});
test('detects BOM for UTF-16LE', () {
final utf16leBom = [0xFF, 0xFE];
final bool hasUtf16LeBom = utf16leBom.length >= 2 &&
utf16leBom[0] == 0xFF &&
utf16leBom[1] == 0xFE;
expect(hasUtf16LeBom, true);
});
test('detects BOM for UTF-16BE', () {
final utf16beBom = [0xFE, 0xFF];
final bool hasUtf16BeBom = utf16beBom.length >= 2 &&
utf16beBom[0] == 0xFE &&
utf16beBom[1] == 0xFF;
expect(hasUtf16BeBom, true);
});
test('handles chunked transfer encoding format', () {
//
const chunkData = '5\r\nHello\r\n5\r\nWorld\r\n0\r\n\r\n';
final bytes = ascii.encode(chunkData);
//
final result = <int>[];
var offset = 0;
while (offset < bytes.length) {
//
var lineEnd = offset;
while (lineEnd < bytes.length - 1) {
if (bytes[lineEnd] == 13 && bytes[lineEnd + 1] == 10) break; // \r\n
lineEnd++;
}
if (lineEnd >= bytes.length - 1) break;
//
final sizeHex = String.fromCharCodes(bytes.sublist(offset, lineEnd));
final chunkSize = int.tryParse(sizeHex, radix: 16) ?? 0;
if (chunkSize == 0) break; //
// \r\n
offset = lineEnd + 2;
//
if (offset + chunkSize <= bytes.length) {
result.addAll(bytes.sublist(offset, offset + chunkSize));
offset += chunkSize + 2; // \r\n
} else {
break;
}
}
final decodedString = ascii.decode(result);
expect(decodedString, 'HelloWorld');
});
test('validates encoding compatibility', () {
const testString = 'Hello, World!';
// UTF-8
expect(() => utf8.encode(testString), returnsNormally);
// ASCII ASCII
expect(() => ascii.encode(testString), returnsNormally);
// ASCII
const nonAsciiString = 'Hello, 世界!';
expect(() => utf8.encode(nonAsciiString), returnsNormally);
expect(() => ascii.encode(nonAsciiString), throwsA(isA<ArgumentError>()));
});
test('handles different content encodings', () {
const testData = 'Hello, World!';
final originalBytes = utf8.encode(testData);
// gzip
final gzipCompressed = gzip.encode(originalBytes);
final gzipDecompressed = gzip.decode(gzipCompressed);
expect(utf8.decode(gzipDecompressed), testData);
// zlib
final zlibCompressed = zlib.encode(originalBytes);
final zlibDecompressed = zlib.decode(zlibCompressed);
expect(utf8.decode(zlibDecompressed), testData);
});
test('handles form URL encoding', () {
final formData = {
'name': 'John Doe',
'email': 'john@example.com',
'message': 'Hello & welcome!',
};
final encodedPairs = <String>[];
formData.forEach((key, value) {
final encodedKey = Uri.encodeComponent(key);
final encodedValue = Uri.encodeComponent(value.toString());
encodedPairs.add('$encodedKey=$encodedValue');
});
final encoded = encodedPairs.join('&');
expect(encoded, contains('name=John%20Doe'));
expect(encoded, contains('email=john%40example.com'));
expect(encoded, contains('message=Hello%20%26%20welcome!'));
});
test('handles binary data encoding', () {
//
final binaryData = List.generate(256, (i) => i);
// Base64
final base64Encoded = base64Encode(binaryData);
final base64Decoded = base64Decode(base64Encoded);
expect(base64Decoded, binaryData);
// Base64
expect(base64Encoded.length % 4, 0); // Base64 4
expect(RegExp(r'^[A-Za-z0-9+/]*={0,2}$').hasMatch(base64Encoded), true);
});
test('handles mixed encoding scenarios', () {
// JSON
final jsonData = {
'name': 'José María',
'description': 'Café & Restaurant 🍽️',
'price': 29.99,
'tags': ['food', 'café', '美食'],
};
final jsonString = jsonEncode(jsonData);
final utf8Bytes = utf8.encode(jsonString);
final base64Encoded = base64Encode(utf8Bytes);
//
final decodedBytes = base64Decode(base64Encoded);
final decodedString = utf8.decode(decodedBytes);
final decodedJson = jsonDecode(decodedString) as Map<String, dynamic>;
expect(decodedJson['name'], 'José María');
expect(decodedJson['description'], 'Café & Restaurant 🍽️');
expect(decodedJson['tags'], ['food', 'café', '美食']);
});
});
}

View File

@ -0,0 +1,476 @@
import 'package:swagger_generator_flutter/core/error_reporter.dart';
import 'package:swagger_generator_flutter/core/models.dart';
import 'package:swagger_generator_flutter/validators/enhanced_validator.dart';
import 'package:test/test.dart';
void main() {
group('EnhancedValidator', () {
late EnhancedValidator validator;
setUp(() {
validator = EnhancedValidator(
includeWarnings: true,
);
});
test('validates valid document successfully', () {
const document = SwaggerDocument(
title: 'Test API',
version: '1.0.0',
description: 'A test API',
servers: [
ApiServer(
url: 'https://api.example.com',
description: 'Production server',
),
],
components: ApiComponents(
schemas: {},
securitySchemes: {},
),
paths: {
'/users': const ApiPath(
path: '/users',
method: HttpMethod.get,
summary: 'Get users',
description: 'Retrieve all users',
operationId: 'getUsers',
tags: ['users'],
parameters: [],
responses: {
'200': const ApiResponse(
code: '200',
description: 'Success',
content: {
'application/json': const ApiMediaType(
schema: {'type': 'array'},
),
},
),
},
),
},
models: {},
controllers: {},
security: [],
);
final isValid = validator.validateDocument(document);
expect(isValid, true);
expect(validator.errorReporter.hasErrorsOrCritical, false);
});
test('detects missing required fields', () {
const document = SwaggerDocument(
title: '', // Missing title
version: '', // Missing version
description: '',
servers: [],
components: ApiComponents(schemas: {}, securitySchemes: {}),
paths: {}, // Empty paths
models: {},
controllers: {},
security: [],
);
final isValid = validator.validateDocument(document);
expect(isValid, false);
expect(validator.errorReporter.hasErrorsOrCritical, true);
final errors = validator.errorReporter.errors;
expect(errors.any((e) => e.id == 'MISSING_INFO_TITLE'), true);
expect(errors.any((e) => e.id == 'MISSING_INFO_VERSION'), true);
expect(errors.any((e) => e.id == 'EMPTY_PATHS'), true);
});
test('validates path parameters correctly', () {
const document = SwaggerDocument(
title: 'Test API',
version: '1.0.0',
description: 'Test',
servers: [],
components: ApiComponents(schemas: {}, securitySchemes: {}),
paths: {
'/users/{id}': const ApiPath(
path: '/users/{id}',
method: HttpMethod.get,
summary: 'Get user',
description: 'Get user by ID',
operationId: 'getUser',
tags: ['users'],
parameters: [
// Missing path parameter declaration for 'id'
],
responses: {
'200': const ApiResponse(
code: '200',
description: 'Success',
),
},
),
},
models: {},
controllers: {},
security: [],
);
final isValid = validator.validateDocument(document);
expect(isValid, false);
final errors = validator.errorReporter.errors;
expect(errors.any((e) => e.id == 'UNDECLARED_PATH_PARAMETER'), true);
});
test('validates path parameter requirements', () {
const document = SwaggerDocument(
title: 'Test API',
version: '1.0.0',
description: 'Test',
servers: [],
components: ApiComponents(schemas: {}, securitySchemes: {}),
paths: {
'/users/{id}': const ApiPath(
path: '/users/{id}',
method: HttpMethod.get,
summary: 'Get user',
description: 'Get user by ID',
operationId: 'getUser',
tags: ['users'],
parameters: [
ApiParameter(
name: 'id',
location: ParameterLocation.path,
required: false, // Path parameters must be required
type: PropertyType.integer,
description: 'User ID',
),
],
responses: {
'200': const ApiResponse(
code: '200',
description: 'Success',
),
},
),
},
models: {},
controllers: {},
security: [],
);
final isValid = validator.validateDocument(document);
expect(isValid, false);
final errors = validator.errorReporter.errors;
expect(errors.any((e) => e.id == 'PATH_PARAMETER_NOT_REQUIRED'), true);
});
test('validates security schemes', () {
const document = SwaggerDocument(
title: 'Test API',
version: '1.0.0',
description: 'Test',
servers: [],
components: ApiComponents(
schemas: {},
securitySchemes: {
'apiKey': const ApiSecurityScheme(
type: SecuritySchemeType.apiKey,
description: 'API Key',
name: '', // Missing name
location: ApiKeyLocation.header,
),
'bearer': const ApiSecurityScheme(
type: SecuritySchemeType.http,
description: 'Bearer token',
scheme: '', // Missing scheme
),
},
),
paths: {
'/test': const ApiPath(
path: '/test',
method: HttpMethod.get,
summary: 'Test',
description: 'Test endpoint',
operationId: 'test',
tags: [],
parameters: [],
responses: {
'200': const ApiResponse(
code: '200',
description: 'Success',
),
},
),
},
models: {},
controllers: {},
security: [],
);
final isValid = validator.validateDocument(document);
expect(isValid, false);
final errors = validator.errorReporter.errors;
expect(errors.any((e) => e.id == 'MISSING_API_KEY_NAME'), true);
expect(errors.any((e) => e.id == 'MISSING_HTTP_SCHEME'), true);
});
test('generates warnings for missing optional fields', () {
const document = SwaggerDocument(
title: 'Test API',
version: '1.0.0',
description: '', // Missing description
servers: [], // Missing servers
components: ApiComponents(schemas: {}, securitySchemes: {}),
paths: {
'/test': const ApiPath(
path: '/test',
method: HttpMethod.get,
summary: '', // Missing summary
description: 'Test endpoint',
operationId: '', // Missing operationId
tags: [],
parameters: [],
responses: {
'200': const ApiResponse(
code: '200',
description: '', // Missing response description
),
},
),
},
models: {},
controllers: {},
security: [],
);
final isValid = validator.validateDocument(document);
expect(isValid, true); // Should be valid but with warnings
final warnings =
validator.errorReporter.getErrorsBySeverity(ErrorSeverity.warning);
expect(warnings.isNotEmpty, true);
expect(warnings.any((w) => w.id == 'MISSING_INFO_DESCRIPTION'), true);
expect(warnings.any((w) => w.id == 'MISSING_SERVERS'), true);
expect(warnings.any((w) => w.id == 'MISSING_OPERATION_ID'), true);
expect(warnings.any((w) => w.id == 'MISSING_RESPONSE_DESCRIPTION'), true);
});
test('validates responses correctly', () {
const document = SwaggerDocument(
title: 'Test API',
version: '1.0.0',
description: 'Test',
servers: [],
components: ApiComponents(schemas: {}, securitySchemes: {}),
paths: {
'/test': const ApiPath(
path: '/test',
method: HttpMethod.get,
summary: 'Test',
description: 'Test endpoint',
operationId: 'test',
tags: [],
parameters: [],
responses: {}, // Missing responses
),
},
models: {},
controllers: {},
security: [],
);
final isValid = validator.validateDocument(document);
expect(isValid, false);
final errors = validator.errorReporter.errors;
expect(errors.any((e) => e.id == 'MISSING_OPERATION_RESPONSES'), true);
});
test('checks for best practices', () {
const document = SwaggerDocument(
title: 'Test API',
version: '1.0.0',
description: 'Test API',
servers: [ApiServer(url: 'https://api.example.com')],
components: ApiComponents(schemas: {}, securitySchemes: {}),
paths: {
'/test': const ApiPath(
path: '/test',
method: HttpMethod.get,
summary: 'Test',
description: 'Test endpoint',
operationId: 'test',
tags: [], // No tags
parameters: [],
responses: {
'200': const ApiResponse(
code: '200',
description: 'Success',
),
// No error responses
},
),
},
models: {},
controllers: {},
security: [],
);
final isValid = validator.validateDocument(document);
expect(isValid, true);
final infos =
validator.errorReporter.getErrorsBySeverity(ErrorSeverity.info);
expect(infos.any((i) => i.id == 'NO_OPERATION_TAGS'), true);
expect(infos.any((i) => i.id == 'NO_ERROR_RESPONSE'), true);
});
test('validates large schemas', () {
// Create a model with many properties
final properties = <String, ApiProperty>{};
for (int i = 0; i < 25; i++) {
properties['property$i'] = ApiProperty(
name: 'property$i',
type: PropertyType.string,
description: 'Property $i',
required: false,
);
}
final largeModel = ApiModel(
name: 'LargeModel',
description: 'A model with many properties',
properties: properties,
required: [],
);
final document = SwaggerDocument(
title: 'Test API',
version: '1.0.0',
description: 'Test API',
servers: [const ApiServer(url: 'https://api.example.com')],
components: ApiComponents(
schemas: {'LargeModel': largeModel},
securitySchemes: {},
),
paths: {
'/test': const ApiPath(
path: '/test',
method: HttpMethod.get,
summary: 'Test',
description: 'Test endpoint',
operationId: 'test',
tags: ['test'],
parameters: [],
responses: {
'200': const ApiResponse(
code: '200',
description: 'Success',
),
},
),
},
models: {'LargeModel': largeModel},
controllers: {},
security: [],
);
final isValid = validator.validateDocument(document);
expect(isValid, true);
final infos =
validator.errorReporter.getErrorsBySeverity(ErrorSeverity.info);
expect(infos.any((i) => i.id == 'LARGE_SCHEMA_OBJECT'), true);
});
test('generates detailed error report', () {
const document = SwaggerDocument(
title: '',
version: '',
description: '',
servers: [],
components: ApiComponents(schemas: {}, securitySchemes: {}),
paths: {},
models: {},
controllers: {},
security: [],
);
validator.validateDocument(document);
final report = validator.errorReporter.generateReport();
expect(report, isNotEmpty);
expect(report, contains('Error Summary'));
expect(report, contains('Missing API Title'));
expect(report, contains('Missing API Version'));
expect(report, contains('Empty Paths Object'));
});
test('generates JSON error report', () {
const document = SwaggerDocument(
title: '',
version: '',
description: '',
servers: [],
components: ApiComponents(schemas: {}, securitySchemes: {}),
paths: {},
models: {},
controllers: {},
security: [],
);
validator.validateDocument(document);
final jsonReport = validator.errorReporter.generateJsonReport();
expect(jsonReport, isNotEmpty);
expect(jsonReport, contains('"timestamp"'));
expect(jsonReport, contains('"summary"'));
expect(jsonReport, contains('"errors"'));
});
test('strict mode validation', () {
final strictValidator = EnhancedValidator(
includeWarnings: false,
);
const document = SwaggerDocument(
title: 'Test API',
version: '1.0.0',
description: '', // Missing description
servers: [], // Missing servers
components: ApiComponents(schemas: {}, securitySchemes: {}),
paths: {
'/test': const ApiPath(
path: '/test',
method: HttpMethod.get,
summary: 'Test',
description: 'Test endpoint',
operationId: 'test',
tags: ['test'],
parameters: [],
responses: {
'200': const ApiResponse(
code: '200',
description: 'Success',
),
},
),
},
models: {},
controllers: {},
security: [],
);
final isValid = strictValidator.validateDocument(document);
expect(isValid, true);
// Should have no warnings in strict mode with includeWarnings: false
final warnings = strictValidator.errorReporter
.getErrorsBySeverity(ErrorSeverity.warning);
expect(warnings.length, equals(0));
});
});
}

589
tests/integration_test.dart Normal file
View File

@ -0,0 +1,589 @@
import 'dart:convert';
import 'dart:io';
import 'package:swagger_generator_flutter/core/error_reporter.dart';
import 'package:swagger_generator_flutter/core/performance_parser.dart';
import 'package:swagger_generator_flutter/generators/optimized_retrofit_generator.dart';
import 'package:swagger_generator_flutter/generators/retrofit_api_generator.dart';
import 'package:swagger_generator_flutter/validators/enhanced_validator.dart';
import 'package:test/test.dart';
void main() {
group('Integration Tests', () {
group('End-to-End Workflow', () {
test('complete workflow from JSON to generated code', () async {
// 1.
final testApiJson = {
'openapi': '3.0.3',
'info': {
'title': 'Integration Test API',
'version': '1.0.0',
'description': 'API for integration testing',
},
'servers': [
{
'url': 'https://api.example.com',
'description': 'Production server',
},
],
'paths': {
'/users': {
'get': {
'summary': 'Get users',
'operationId': 'getUsers',
'tags': ['users'],
'parameters': [
{
'name': 'page',
'in': 'query',
'required': false,
'schema': {'type': 'integer', 'default': 1},
'description': 'Page number',
},
{
'name': 'limit',
'in': 'query',
'required': false,
'schema': {'type': 'integer', 'default': 20},
'description': 'Items per page',
},
],
'responses': {
'200': {
'description': 'Success',
'content': {
'application/json': {
'schema': {
'type': 'object',
'properties': {
'data': {
'type': 'array',
'items': {
'\$ref': '#/components/schemas/User',
},
},
'total': {'type': 'integer'},
'page': {'type': 'integer'},
'limit': {'type': 'integer'},
},
},
},
},
},
'400': {
'description': 'Bad request',
},
},
},
'post': {
'summary': 'Create user',
'operationId': 'createUser',
'tags': ['users'],
'requestBody': {
'required': true,
'content': {
'application/json': {
'schema': {
'\$ref': '#/components/schemas/CreateUserRequest',
},
},
},
},
'responses': {
'201': {
'description': 'User created',
'content': {
'application/json': {
'schema': {
'\$ref': '#/components/schemas/User',
},
},
},
},
'400': {
'description': 'Invalid input',
},
},
},
},
'/users/{id}': {
'get': {
'summary': 'Get user by ID',
'operationId': 'getUserById',
'tags': ['users'],
'parameters': [
{
'name': 'id',
'in': 'path',
'required': true,
'schema': {'type': 'integer'},
'description': 'User ID',
},
],
'responses': {
'200': {
'description': 'User found',
'content': {
'application/json': {
'schema': {
'\$ref': '#/components/schemas/User',
},
},
},
},
'404': {
'description': 'User not found',
},
},
},
},
'/files/upload': {
'post': {
'summary': 'Upload file',
'operationId': 'uploadFile',
'tags': ['files'],
'requestBody': {
'required': true,
'content': {
'multipart/form-data': {
'schema': {
'type': 'object',
'properties': {
'file': {
'type': 'string',
'format': 'binary',
},
'description': {
'type': 'string',
},
},
'required': ['file'],
},
},
},
},
'responses': {
'200': {
'description': 'File uploaded',
'content': {
'application/json': {
'schema': {
'\$ref': '#/components/schemas/FileUploadResult',
},
},
},
},
},
},
},
},
'components': {
'schemas': {
'User': {
'type': 'object',
'properties': {
'id': {
'type': 'integer',
'format': 'int64',
},
'name': {
'type': 'string',
'maxLength': 100,
},
'email': {
'type': 'string',
'format': 'email',
},
'createdAt': {
'type': 'string',
'format': 'date-time',
},
},
'required': ['id', 'name', 'email'],
},
'CreateUserRequest': {
'type': 'object',
'properties': {
'name': {
'type': 'string',
'maxLength': 100,
},
'email': {
'type': 'string',
'format': 'email',
},
},
'required': ['name', 'email'],
},
'FileUploadResult': {
'type': 'object',
'properties': {
'url': {
'type': 'string',
'format': 'uri',
},
'filename': {
'type': 'string',
},
'size': {
'type': 'integer',
},
'contentType': {
'type': 'string',
},
},
'required': ['url', 'filename', 'size'],
},
},
'securitySchemes': {
'bearerAuth': {
'type': 'http',
'scheme': 'bearer',
'bearerFormat': 'JWT',
},
},
},
'security': [
{
'bearerAuth': [],
},
],
};
final jsonString = jsonEncode(testApiJson);
// 2. JSON SwaggerDocument
final parser = PerformanceParser(
config: const ParseConfig(
enablePerformanceStats: true,
enableParallelParsing: false, //
),
);
final document = await parser.parseDocument(jsonString);
//
expect(document.title, equals('Integration Test API'));
expect(document.version, equals('1.0.0'));
expect(document.paths.length, equals(3));
expect(document.models.length, equals(3));
expect(document.servers.length, equals(1));
//
final parseStats = parser.lastStats;
expect(parseStats, isNotNull);
expect(parseStats!.totalTime.inMilliseconds, lessThan(5000));
// 3.
final validator = EnhancedValidator(
includeWarnings: true,
);
final isValid = validator.validateDocument(document);
expect(isValid, isTrue);
final errors = validator.errorReporter.errors;
final criticalErrors = errors
.where((e) =>
e.severity == ErrorSeverity.error ||
e.severity == ErrorSeverity.critical)
.toList();
expect(criticalErrors, isEmpty,
reason:
'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);
//
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('@MultiPart()'));
// 5. API
final optimizedGenerator = OptimizedRetrofitGenerator(
className: 'OptimizedIntegrationApi',
generateModularApis: true,
generateBaseResult: true,
generatePagination: true,
generateFileUpload: true,
);
final optimizedCode = optimizedGenerator.generateFromDocument(document);
//
expect(optimizedCode, isNotEmpty);
expect(optimizedCode, contains('class BaseResult'));
expect(optimizedCode, contains('class BasePageResult'));
expect(optimizedCode, contains('class FileUploadRequest'));
expect(optimizedCode, contains('class ApiUtils'));
expect(optimizedCode, contains('UsersApi'));
expect(optimizedCode, contains('FilesApi'));
// 6.
print('Integration Test Performance Summary:');
print(' Parse Time: ${parseStats.totalTime.inMilliseconds}ms');
print(
' 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');
print(
' Optimized Code Size: ${(optimizedCode.length / 1024).toStringAsFixed(2)}KB');
//
expect(
parseStats.totalTime.inMilliseconds, lessThan(2000)); // 2
expect(retrofitCode.length, greaterThan(1000)); //
expect(optimizedCode.length,
greaterThan(retrofitCode.length)); //
});
test('handles real project swagger.json', () async {
final file = File('swagger.json');
if (!file.existsSync()) {
print(
'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');
//
final parser = PerformanceParser(
config: const ParseConfig(
enablePerformanceStats: true,
enableParallelParsing: false, //
maxConcurrency: 4,
),
);
final parseStopwatch = Stopwatch()..start();
final document = await parser.parseDocument(jsonString);
parseStopwatch.stop();
expect(document, isNotNull);
expect(document.paths.isNotEmpty, isTrue);
print('Real project parsing results:');
print(' Parse Time: ${parseStopwatch.elapsedMilliseconds}ms');
print(' Paths: ${document.paths.length}');
print(' Models: ${document.models.length}');
print(' Servers: ${document.servers.length}');
//
final validator = EnhancedValidator(includeWarnings: false);
final isValid = validator.validateDocument(document);
final errors =
validator.errorReporter.getErrorsBySeverity(ErrorSeverity.error);
final criticalErrors =
validator.errorReporter.getErrorsBySeverity(ErrorSeverity.critical);
print('Validation results:');
print(' Valid: $isValid');
print(' Errors: ${errors.length}');
print(' Critical: ${criticalErrors.length}');
//
final generator = OptimizedRetrofitGenerator(
className: 'OAMobileApiService',
generateModularApis: true,
generateBaseResult: true,
generatePagination: true,
generateFileUpload: true,
);
final genStopwatch = Stopwatch()..start();
final generatedCode = generator.generateFromDocument(document);
genStopwatch.stop();
expect(generatedCode, isNotEmpty);
expect(generatedCode, contains('OAMobileApiService'));
print('Code generation results:');
print(' Generation Time: ${genStopwatch.elapsedMilliseconds}ms');
print(
' Generated Code Size: ${(generatedCode.length / 1024).toStringAsFixed(2)}KB');
print(' Generated Lines: ${generatedCode.split('\n').length}');
//
expect(parseStopwatch.elapsedMilliseconds, lessThan(15000)); // 15
expect(genStopwatch.elapsedMilliseconds, lessThan(10000)); // 10
expect(generatedCode.length, greaterThan(5000)); // 5KB代码
});
});
group('Error Handling Integration', () {
test('handles malformed JSON gracefully', () async {
const malformedJson =
'{"openapi": "3.0.3", "info": {"title": "Test"'; //
final parser = PerformanceParser();
expect(() => parser.parseDocument(malformedJson),
throwsA(isA<FormatException>()));
});
test('handles invalid OpenAPI document', () async {
final invalidDoc = {
'openapi': '3.0.3',
'info': {
'title': 'Invalid API',
// version
},
'paths': {}, //
};
final jsonString = jsonEncode(invalidDoc);
final parser = PerformanceParser();
expect(() => parser.parseDocument(jsonString),
throwsA(isA<FormatException>()));
});
test('validation catches common errors', () async {
final problematicDoc = {
'openapi': '3.0.3',
'info': {
'title': 'Problematic API',
'version': '1.0.0',
},
'paths': {
'/users/{id}': {
'get': {
'summary': 'Get user',
'responses': {
'200': {
'description': 'Success',
},
},
//
},
},
},
};
final jsonString = jsonEncode(problematicDoc);
final parser = PerformanceParser();
final document = await parser.parseDocument(jsonString);
final validator = EnhancedValidator();
final isValid = validator.validateDocument(document);
expect(isValid, isFalse);
final errors = validator.errorReporter.errors;
expect(errors.any((e) => e.id == 'UNDECLARED_PATH_PARAMETER'), isTrue);
});
});
group('Performance Integration', () {
test('handles large documents efficiently', () async {
//
final paths = <String, dynamic>{};
final schemas = <String, dynamic>{};
for (int i = 0; i < 200; i++) {
paths['/resource$i'] = {
'get': {
'summary': 'Get resource $i',
'operationId': 'getResource$i',
'tags': ['resources'],
'responses': {
'200': {
'description': 'Success',
'content': {
'application/json': {
'schema': {
'\$ref': '#/components/schemas/Resource$i',
},
},
},
},
},
},
};
schemas['Resource$i'] = {
'type': 'object',
'properties': {
'id': {'type': 'integer'},
'name': {'type': 'string'},
'value$i': {'type': 'string'},
},
'required': ['id', 'name'],
};
}
final largeDoc = {
'openapi': '3.0.3',
'info': {
'title': 'Large API',
'version': '1.0.0',
},
'paths': paths,
'components': {
'schemas': schemas,
},
};
final jsonString = jsonEncode(largeDoc);
print(
'Large document size: ${(jsonString.length / 1024).toStringAsFixed(2)}KB');
//
final parser = PerformanceParser(
config: const ParseConfig(
enablePerformanceStats: true,
enableParallelParsing: false, //
maxConcurrency: 4,
),
);
final parseStopwatch = Stopwatch()..start();
final document = await parser.parseDocument(jsonString);
parseStopwatch.stop();
expect(document.paths.length, greaterThan(100));
expect(document.models.length, greaterThan(100));
expect(parseStopwatch.elapsedMilliseconds, lessThan(10000)); // 10
//
final generator = OptimizedRetrofitGenerator(
generateModularApis: true,
);
final genStopwatch = Stopwatch()..start();
final generatedCode = generator.generateFromDocument(document);
genStopwatch.stop();
expect(generatedCode.length, greaterThan(10000)); // 10KB代码
expect(genStopwatch.elapsedMilliseconds, lessThan(15000)); // 15
print('Large document performance:');
print(' Parse Time: ${parseStopwatch.elapsedMilliseconds}ms');
print(' Generation Time: ${genStopwatch.elapsedMilliseconds}ms');
print(' Paths: ${document.paths.length}');
print(' Models: ${document.models.length}');
print(
' Generated Code: ${(generatedCode.length / 1024).toStringAsFixed(2)}KB');
});
});
});
}

451
tests/media_type_test.dart Normal file
View File

@ -0,0 +1,451 @@
import 'package:swagger_generator_flutter/core/models.dart';
import 'package:test/test.dart';
void main() {
group('MediaType Enum', () {
test('converts media type to string', () {
expect(MediaType.json.value, 'application/json');
expect(MediaType.xml.value, 'application/xml');
expect(MediaType.multipartFormData.value, 'multipart/form-data');
expect(
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');
expect(MediaType.applicationPdf.value, 'application/pdf');
expect(MediaType.imagePng.value, 'image/png');
expect(MediaType.imageJpeg.value, 'image/jpeg');
expect(MediaType.imageGif.value, 'image/gif');
expect(MediaType.imageSvg.value, 'image/svg+xml');
expect(MediaType.audioMp3.value, 'audio/mpeg');
expect(MediaType.videoMp4.value, 'video/mp4');
});
test('converts string to media type', () {
expect(MediaTypeExtension.fromString('application/json'), MediaType.json);
expect(MediaTypeExtension.fromString('application/xml'), MediaType.xml);
expect(MediaTypeExtension.fromString('text/xml'), MediaType.xml);
expect(MediaTypeExtension.fromString('multipart/form-data'),
MediaType.multipartFormData);
expect(MediaTypeExtension.fromString('application/x-www-form-urlencoded'),
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);
expect(MediaTypeExtension.fromString('application/pdf'),
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);
expect(MediaTypeExtension.fromString('audio/mpeg'), MediaType.audioMp3);
expect(MediaTypeExtension.fromString('audio/mp3'), MediaType.audioMp3);
expect(MediaTypeExtension.fromString('video/mp4'), MediaType.videoMp4);
expect(MediaTypeExtension.fromString('unknown/type'), MediaType.custom);
});
test('checks text types', () {
expect(MediaType.json.isText, true);
expect(MediaType.xml.isText, true);
expect(MediaType.textPlain.isText, true);
expect(MediaType.textHtml.isText, true);
expect(MediaType.textCsv.isText, true);
expect(MediaType.imagePng.isText, false);
expect(MediaType.applicationOctetStream.isText, false);
});
test('checks binary types', () {
expect(MediaType.applicationOctetStream.isBinary, true);
expect(MediaType.applicationPdf.isBinary, true);
expect(MediaType.imagePng.isBinary, true);
expect(MediaType.imageJpeg.isBinary, true);
expect(MediaType.imageGif.isBinary, true);
expect(MediaType.audioMp3.isBinary, true);
expect(MediaType.videoMp4.isBinary, true);
expect(MediaType.json.isBinary, false);
expect(MediaType.textPlain.isBinary, false);
});
test('checks form types', () {
expect(MediaType.formData.isForm, true);
expect(MediaType.formUrlEncoded.isForm, true);
expect(MediaType.multipartFormData.isForm, true);
expect(MediaType.json.isForm, false);
expect(MediaType.xml.isForm, false);
});
test('checks image types', () {
expect(MediaType.imagePng.isImage, true);
expect(MediaType.imageJpeg.isImage, true);
expect(MediaType.imageGif.isImage, true);
expect(MediaType.imageSvg.isImage, true);
expect(MediaType.json.isImage, false);
expect(MediaType.applicationPdf.isImage, false);
});
test('checks audio types', () {
expect(MediaType.audioMp3.isAudio, true);
expect(MediaType.json.isAudio, false);
expect(MediaType.videoMp4.isAudio, false);
});
test('checks video types', () {
expect(MediaType.videoMp4.isVideo, true);
expect(MediaType.json.isVideo, false);
expect(MediaType.audioMp3.isVideo, false);
});
test('gets correct Dart types', () {
expect(MediaType.json.dartType, 'Map<String, dynamic>');
expect(MediaType.xml.dartType, 'String');
expect(MediaType.multipartFormData.dartType, 'FormData');
expect(MediaType.formUrlEncoded.dartType, 'FormData');
expect(MediaType.textPlain.dartType, 'String');
expect(MediaType.textHtml.dartType, 'String');
expect(MediaType.textCsv.dartType, 'String');
expect(MediaType.applicationOctetStream.dartType, 'List<int>');
expect(MediaType.applicationPdf.dartType, 'List<int>');
expect(MediaType.imagePng.dartType, 'List<int>');
expect(MediaType.imageJpeg.dartType, 'List<int>');
expect(MediaType.imageGif.dartType, 'List<int>');
expect(MediaType.imageSvg.dartType, 'String');
expect(MediaType.audioMp3.dartType, 'List<int>');
expect(MediaType.videoMp4.dartType, 'List<int>');
expect(MediaType.custom.dartType, 'dynamic');
});
});
group('ApiMediaType', () {
test('creates ApiMediaType with JSON content type', () {
final json = {
'schema': {
'type': 'object',
'properties': {
'id': {'type': 'integer'},
'name': {'type': 'string'},
},
},
'example': {
'id': 1,
'name': 'Test',
},
};
final mediaType = ApiMediaType.fromJson(json, 'application/json');
expect(mediaType.mediaType, MediaType.json);
expect(mediaType.rawMediaType, 'application/json');
expect(mediaType.isJson, true);
expect(mediaType.isXml, false);
expect(mediaType.isForm, false);
expect(mediaType.isBinary, false);
expect(mediaType.isText, true);
expect(mediaType.dartType, 'Map<String, dynamic>');
});
test('creates ApiMediaType with XML content type', () {
final json = {
'schema': {
'type': 'string',
},
'example': '<user><id>1</id><name>Test</name></user>',
};
final mediaType = ApiMediaType.fromJson(json, 'application/xml');
expect(mediaType.mediaType, MediaType.xml);
expect(mediaType.rawMediaType, 'application/xml');
expect(mediaType.isJson, false);
expect(mediaType.isXml, true);
expect(mediaType.isForm, false);
expect(mediaType.isBinary, false);
expect(mediaType.isText, true);
expect(mediaType.dartType, 'String');
});
test('creates ApiMediaType with form data content type', () {
final json = {
'schema': {
'type': 'object',
'properties': {
'file': {'type': 'string', 'format': 'binary'},
'name': {'type': 'string'},
},
},
'encoding': {
'file': {
'contentType': 'image/png',
},
},
};
final mediaType = ApiMediaType.fromJson(json, 'multipart/form-data');
expect(mediaType.mediaType, MediaType.multipartFormData);
expect(mediaType.rawMediaType, 'multipart/form-data');
expect(mediaType.isJson, false);
expect(mediaType.isXml, false);
expect(mediaType.isForm, true);
expect(mediaType.isFileUpload, true);
expect(mediaType.isBinary, false);
expect(mediaType.isText, false);
expect(mediaType.dartType, 'FormData');
expect(mediaType.encoding.length, 1);
expect(mediaType.encoding['file'], isNotNull);
});
test('creates ApiMediaType with binary content type', () {
final json = {
'schema': {
'type': 'string',
'format': 'binary',
},
};
final mediaType = ApiMediaType.fromJson(json, 'application/octet-stream');
expect(mediaType.mediaType, MediaType.applicationOctetStream);
expect(mediaType.rawMediaType, 'application/octet-stream');
expect(mediaType.isJson, false);
expect(mediaType.isXml, false);
expect(mediaType.isForm, false);
expect(mediaType.isBinary, true);
expect(mediaType.isText, false);
expect(mediaType.dartType, 'List<int>');
});
test('creates ApiMediaType with image content type', () {
final json = {
'schema': {
'type': 'string',
'format': 'binary',
},
};
final mediaType = ApiMediaType.fromJson(json, 'image/png');
expect(mediaType.mediaType, MediaType.imagePng);
expect(mediaType.rawMediaType, 'image/png');
expect(mediaType.isImage, true);
expect(mediaType.isBinary, true);
expect(mediaType.dartType, 'List<int>');
});
test('creates ApiMediaType with custom content type', () {
final json = {
'schema': {
'type': 'string',
},
};
final mediaType = ApiMediaType.fromJson(json, 'application/vnd.api+json');
expect(mediaType.mediaType, MediaType.custom);
expect(mediaType.rawMediaType, 'application/vnd.api+json');
expect(mediaType.dartType, 'dynamic');
});
test('creates ApiMediaType with default content type when none provided',
() {
final json = {
'schema': {
'type': 'object',
},
};
final mediaType = ApiMediaType.fromJson(json);
expect(mediaType.mediaType, MediaType.json);
expect(mediaType.rawMediaType, 'application/json');
expect(mediaType.isJson, true);
});
test('creates ApiMediaType with examples', () {
final json = {
'schema': {
'type': 'object',
},
'examples': {
'user1': {
'summary': 'User example 1',
'value': {'id': 1, 'name': 'John'},
},
'user2': {
'summary': 'User example 2',
'value': {'id': 2, 'name': 'Jane'},
},
},
};
final mediaType = ApiMediaType.fromJson(json, 'application/json');
expect(mediaType.examples.length, 2);
expect(mediaType.examples['user1'], isNotNull);
expect(mediaType.examples['user2'], isNotNull);
expect(mediaType.examples['user1']?.summary, 'User example 1');
});
});
group('File Upload Support', () {
test('creates ApiEncoding with file content type', () {
final json = {
'contentType': 'image/png',
'headers': {
'X-Custom-Header': {
'description': 'Custom header for file upload',
'required': true,
},
},
};
final encoding = ApiEncoding.fromJson(json);
expect(encoding.contentType, 'image/png');
expect(encoding.isFile, true);
expect(encoding.isImage, true);
expect(encoding.isAudio, false);
expect(encoding.isVideo, false);
expect(encoding.hasHeaders, true);
expect(encoding.headers.length, 1);
expect(encoding.headers['X-Custom-Header'], isNotNull);
});
test('detects different file types in ApiEncoding', () {
final imageEncoding = ApiEncoding.fromJson({'contentType': 'image/jpeg'});
expect(imageEncoding.isFile, true);
expect(imageEncoding.isImage, true);
expect(imageEncoding.isAudio, false);
expect(imageEncoding.isVideo, false);
final audioEncoding = ApiEncoding.fromJson({'contentType': 'audio/mpeg'});
expect(audioEncoding.isFile, true);
expect(audioEncoding.isImage, false);
expect(audioEncoding.isAudio, true);
expect(audioEncoding.isVideo, false);
final videoEncoding = ApiEncoding.fromJson({'contentType': 'video/mp4'});
expect(videoEncoding.isFile, true);
expect(videoEncoding.isImage, false);
expect(videoEncoding.isAudio, false);
expect(videoEncoding.isVideo, true);
final binaryEncoding =
ApiEncoding.fromJson({'contentType': 'application/octet-stream'});
expect(binaryEncoding.isFile, true);
expect(binaryEncoding.isImage, false);
expect(binaryEncoding.isAudio, false);
expect(binaryEncoding.isVideo, false);
final textEncoding = ApiEncoding.fromJson({'contentType': 'text/plain'});
expect(textEncoding.isFile, false);
expect(textEncoding.isImage, false);
expect(textEncoding.isAudio, false);
expect(textEncoding.isVideo, false);
});
test('creates ApiMediaType with file upload encoding', () {
final json = {
'schema': {
'type': 'object',
'properties': {
'avatar': {'type': 'string', 'format': 'binary'},
'name': {'type': 'string'},
'documents': {
'type': 'array',
'items': {'type': 'string', 'format': 'binary'},
},
},
},
'encoding': {
'avatar': {
'contentType': 'image/*',
'headers': {
'X-File-Type': {
'description': 'File type validation',
'required': false,
},
},
},
'documents': {
'contentType': 'application/pdf',
'explode': true,
},
},
};
final mediaType = ApiMediaType.fromJson(json, 'multipart/form-data');
expect(mediaType.isFileUpload, true);
expect(mediaType.encoding.length, 2);
expect(mediaType.encoding['avatar'], isNotNull);
expect(mediaType.encoding['documents'], isNotNull);
expect(mediaType.encoding['avatar']?.contentType, 'image/*');
expect(mediaType.encoding['documents']?.contentType, 'application/pdf');
expect(mediaType.encoding['documents']?.explode, true);
});
test('handles complex file upload scenarios', () {
final json = {
'schema': {
'type': 'object',
'properties': {
'profileImage': {'type': 'string', 'format': 'binary'},
'thumbnails': {
'type': 'array',
'items': {'type': 'string', 'format': 'binary'},
},
'metadata': {
'type': 'object',
'properties': {
'title': {'type': 'string'},
'description': {'type': 'string'},
},
},
},
'required': ['profileImage', 'metadata'],
},
'encoding': {
'profileImage': {
'contentType': 'image/png, image/jpeg',
'headers': {
'X-Image-Quality': {
'description': 'Image quality setting',
'schema': {'type': 'integer', 'minimum': 1, 'maximum': 100},
},
},
},
'thumbnails': {
'contentType': 'image/png',
'style': 'form',
'explode': true,
},
'metadata': {
'contentType': 'application/json',
},
},
};
final mediaType = ApiMediaType.fromJson(json, 'multipart/form-data');
expect(mediaType.isFileUpload, true);
expect(mediaType.encoding.length, 3);
final profileImageEncoding = mediaType.encoding['profileImage']!;
expect(profileImageEncoding.contentType, 'image/png, image/jpeg');
expect(profileImageEncoding.hasHeaders, true);
final thumbnailsEncoding = mediaType.encoding['thumbnails']!;
expect(thumbnailsEncoding.explode, true);
expect(thumbnailsEncoding.style, 'form');
final metadataEncoding = mediaType.encoding['metadata']!;
expect(metadataEncoding.contentType, 'application/json');
});
});
}

View File

@ -1,923 +0,0 @@
import 'package:test/test.dart';
import 'package:swagger_generator_flutter/core/models.dart';
void main() {
group('ApiPath', () {
test('creates ApiPath with required fields', () {
const path = ApiPath(
path: '/api/users',
method: HttpMethod.get,
summary: 'Get users',
description: 'Retrieve all users',
operationId: 'getUsers',
tags: ['User'],
parameters: [],
responses: {},
requestBody: null,
);
expect(path.path, '/api/users');
expect(path.method, HttpMethod.get);
expect(path.summary, 'Get users');
expect(path.description, 'Retrieve all users');
expect(path.tags, ['User']);
expect(path.parameters, isEmpty);
expect(path.responses, isEmpty);
expect(path.requestBody, isNull);
});
test('creates ApiPath with all fields', () {
final parameters = [
const ApiParameter(
name: 'id',
location: ParameterLocation.path,
required: true,
type: PropertyType.integer,
description: 'User ID',
),
];
final responses = {
'200': const ApiResponse(
code: '200',
description: 'Success',
schema: {'type': 'object'},
content: null,
),
};
const requestBody = ApiRequestBody(
description: 'User data',
required: true,
content: {
'application/json': {
'schema': {'type': 'object'}
}
},
);
final path = ApiPath(
path: '/api/users/{id}',
method: HttpMethod.put,
summary: 'Update user',
description: 'Update user by ID',
operationId: 'updateUser',
tags: ['User'],
parameters: parameters,
responses: responses,
requestBody: requestBody,
);
expect(path.parameters, hasLength(1));
expect(path.responses, hasLength(1));
expect(path.requestBody, isNotNull);
});
});
group('ApiParameter', () {
test('creates ApiParameter with required fields', () {
const param = ApiParameter(
name: 'id',
location: ParameterLocation.path,
required: true,
type: PropertyType.integer,
description: 'User ID',
);
expect(param.name, 'id');
expect(param.location, ParameterLocation.path);
expect(param.required, true);
expect(param.type, PropertyType.integer);
expect(param.description, 'User ID');
});
test('creates ApiParameter with optional fields', () {
const param = ApiParameter(
name: 'page',
location: ParameterLocation.query,
required: false,
type: PropertyType.integer,
description: 'Page number',
format: 'int32',
example: 1,
defaultValue: 1,
);
expect(param.format, 'int32');
expect(param.example, 1);
expect(param.defaultValue, 1);
});
test('creates ApiParameter from JSON', () {
final json = {
'name': 'id',
'in': 'path',
'required': true,
'type': 'integer',
'description': 'User ID',
'format': 'int64',
'example': 123,
'default': 1,
};
final param = ApiParameter.fromJson(json);
expect(param.name, 'id');
expect(param.location, ParameterLocation.path);
expect(param.required, true);
expect(param.type, PropertyType.integer);
expect(param.description, 'User ID');
expect(param.format, 'int64');
expect(param.example, 123);
expect(param.defaultValue, 1);
});
});
group('ApiResponse', () {
test('creates ApiResponse with required fields', () {
const response = ApiResponse(
code: '200',
description: 'Success',
schema: null,
content: null,
);
expect(response.code, '200');
expect(response.description, 'Success');
expect(response.schema, isNull);
expect(response.content, isNull);
});
test('creates ApiResponse with schema', () {
final schema = {
'type': 'object',
'properties': {
'id': {'type': 'integer'},
'name': {'type': 'string'},
},
};
final response = ApiResponse(
code: '200',
description: 'Success',
schema: schema,
content: null,
);
expect(response.schema, equals(schema));
});
test('creates ApiResponse from JSON', () {
final json = {
'description': 'Success',
'schema': {'type': 'object'},
'content': {
'application/json': {
'schema': {'type': 'object'},
},
},
};
final response = ApiResponse.fromJson('200', json);
expect(response.code, '200');
expect(response.description, 'Success');
expect(response.schema, isNotNull);
expect(response.content, isNotNull);
});
});
group('ApiRequestBody', () {
test('creates ApiRequestBody with required fields', () {
const requestBody = ApiRequestBody(
description: 'User data',
required: true,
content: null,
);
expect(requestBody.description, 'User data');
expect(requestBody.required, true);
expect(requestBody.content, isNull);
});
test('creates ApiRequestBody with content', () {
final content = {
'application/json': {
'schema': {
'type': 'object',
'properties': {
'name': {'type': 'string'},
'email': {'type': 'string'},
},
},
},
};
final requestBody = ApiRequestBody(
description: 'User data',
required: true,
content: content,
);
expect(requestBody.content, equals(content));
});
test('creates ApiRequestBody from JSON', () {
final json = {
'description': 'User data',
'required': true,
'content': {
'application/json': {
'schema': {'type': 'object'},
},
},
};
final requestBody = ApiRequestBody.fromJson(json);
expect(requestBody.description, 'User data');
expect(requestBody.required, true);
expect(requestBody.content, isNotNull);
});
});
group('ApiModel', () {
test('creates ApiModel with required fields', () {
const model = ApiModel(
name: 'User',
description: 'User model',
properties: {},
required: [],
);
expect(model.name, 'User');
expect(model.description, 'User model');
expect(model.properties, isEmpty);
expect(model.required, isEmpty);
expect(model.isEnum, false);
expect(model.enumValues, isEmpty);
expect(model.enumType, isNull);
});
test('creates ApiModel with properties', () {
final properties = {
'id': const ApiProperty(
name: 'id',
type: PropertyType.integer,
description: 'User ID',
required: true,
),
'name': const ApiProperty(
name: 'name',
type: PropertyType.string,
description: 'User name',
required: true,
),
};
final model = ApiModel(
name: 'User',
description: 'User model',
properties: properties,
required: ['id', 'name'],
);
expect(model.properties, hasLength(2));
expect(model.required, ['id', 'name']);
});
test('creates enum ApiModel', () {
const model = ApiModel(
name: 'UserStatus',
description: 'User status enum',
properties: {},
required: [],
isEnum: true,
enumValues: ['active', 'inactive', 'pending'],
enumType: PropertyType.string,
);
expect(model.isEnum, true);
expect(model.enumValues, ['active', 'inactive', 'pending']);
expect(model.enumType, PropertyType.string);
});
test('creates ApiModel from JSON', () {
final json = {
'description': 'User model',
'properties': {
'id': {
'type': 'integer',
'description': 'User ID',
},
'name': {
'type': 'string',
'description': 'User name',
},
},
'required': ['id', 'name'],
};
final model = ApiModel.fromJson('User', json);
expect(model.name, 'User');
expect(model.description, 'User model');
expect(model.properties, hasLength(2));
expect(model.required, ['id', 'name']);
});
test('creates enum ApiModel from JSON', () {
final json = {
'description': 'User status enum',
'enum': ['active', 'inactive', 'pending'],
'type': 'string',
};
final model = ApiModel.fromJson('UserStatus', json);
expect(model.isEnum, true);
expect(model.enumValues, ['active', 'inactive', 'pending']);
expect(model.enumType, PropertyType.string);
});
});
group('ApiProperty', () {
test('creates ApiProperty with required fields', () {
const property = ApiProperty(
name: 'id',
type: PropertyType.integer,
description: 'User ID',
required: true,
);
expect(property.name, 'id');
expect(property.type, PropertyType.integer);
expect(property.description, 'User ID');
expect(property.required, true);
expect(property.nullable, false);
});
test('creates ApiProperty with optional fields', () {
const property = ApiProperty(
name: 'name',
type: PropertyType.string,
description: 'User name',
required: false,
nullable: true,
format: 'string',
example: 'John Doe',
defaultValue: 'Unknown',
reference: 'User',
);
expect(property.nullable, true);
expect(property.format, 'string');
expect(property.example, 'John Doe');
expect(property.defaultValue, 'Unknown');
expect(property.reference, 'User');
});
test('creates ApiProperty from JSON', () {
final json = {
'type': 'string',
'description': 'User name',
'nullable': true,
'format': 'string',
'example': 'John Doe',
'default': 'Unknown',
};
final property = ApiProperty.fromJson('name', json, []);
expect(property.name, 'name');
expect(property.type, PropertyType.string);
expect(property.description, 'User name');
expect(property.nullable, true);
expect(property.format, 'string');
expect(property.example, 'John Doe');
expect(property.defaultValue, 'Unknown');
});
test('creates ApiProperty with reference', () {
final json = {
'type': 'object',
'description': 'User object',
'\$ref': '#/components/schemas/User',
};
final property = ApiProperty.fromJson('user', json, []);
expect(property.type, PropertyType.reference);
expect(property.reference, 'User');
});
test('creates ApiProperty with array items', () {
final json = {
'type': 'array',
'description': 'User list',
'items': {
'type': 'object',
'\$ref': '#/components/schemas/User',
},
};
final property = ApiProperty.fromJson('users', json, []);
expect(property.type, PropertyType.array);
expect(property.items, isNotNull);
expect(property.items!.name, 'User');
});
});
group('PropertyType', () {
test('converts string to PropertyType', () {
expect(PropertyType.fromString('string'), PropertyType.string);
expect(PropertyType.fromString('integer'), PropertyType.integer);
expect(PropertyType.fromString('number'), PropertyType.number);
expect(PropertyType.fromString('boolean'), PropertyType.boolean);
expect(PropertyType.fromString('array'), PropertyType.array);
expect(PropertyType.fromString('object'), PropertyType.object);
expect(PropertyType.fromString('unknown'), PropertyType.string);
});
test('gets PropertyType value', () {
expect(PropertyType.string.value, 'string');
expect(PropertyType.integer.value, 'integer');
expect(PropertyType.number.value, 'number');
expect(PropertyType.boolean.value, 'boolean');
expect(PropertyType.array.value, 'array');
expect(PropertyType.object.value, 'object');
expect(PropertyType.reference.value, 'reference');
});
});
group('ParameterLocation', () {
test('converts string to ParameterLocation', () {
expect(ParameterLocation.fromString('query'), ParameterLocation.query);
expect(ParameterLocation.fromString('path'), ParameterLocation.path);
expect(ParameterLocation.fromString('header'), ParameterLocation.header);
expect(ParameterLocation.fromString('cookie'), ParameterLocation.cookie);
expect(ParameterLocation.fromString('unknown'), ParameterLocation.query);
});
});
group('HttpMethod', () {
test('converts string to HttpMethod', () {
expect(HttpMethod.fromString('GET'), HttpMethod.get);
expect(HttpMethod.fromString('POST'), HttpMethod.post);
expect(HttpMethod.fromString('PUT'), HttpMethod.put);
expect(HttpMethod.fromString('DELETE'), HttpMethod.delete);
expect(HttpMethod.fromString('PATCH'), HttpMethod.patch);
expect(HttpMethod.fromString('HEAD'), HttpMethod.head);
expect(HttpMethod.fromString('OPTIONS'), HttpMethod.options);
});
test('handles case insensitive input', () {
expect(HttpMethod.fromString('get'), HttpMethod.get);
expect(HttpMethod.fromString('Post'), HttpMethod.post);
expect(HttpMethod.fromString('put'), HttpMethod.put);
});
test('returns default for unknown method', () {
expect(HttpMethod.fromString('UNKNOWN'), HttpMethod.get);
expect(HttpMethod.fromString(''), HttpMethod.get);
});
test('gets HttpMethod value', () {
expect(HttpMethod.get.value, 'GET');
expect(HttpMethod.post.value, 'POST');
expect(HttpMethod.put.value, 'PUT');
expect(HttpMethod.delete.value, 'DELETE');
expect(HttpMethod.patch.value, 'PATCH');
expect(HttpMethod.head.value, 'HEAD');
expect(HttpMethod.options.value, 'OPTIONS');
});
});
group('SwaggerDocument', () {
test('creates SwaggerDocument with required fields', () {
const document = SwaggerDocument(
title: 'Test API',
version: '1.0.0',
description: 'Test API description',
host: 'api.example.com',
basePath: '/api',
schemes: ['https'],
consumes: ['application/json'],
produces: ['application/json'],
paths: {},
models: {},
controllers: {},
);
expect(document.title, 'Test API');
expect(document.version, '1.0.0');
expect(document.description, 'Test API description');
expect(document.host, 'api.example.com');
expect(document.basePath, '/api');
expect(document.schemes, ['https']);
expect(document.consumes, ['application/json']);
expect(document.produces, ['application/json']);
expect(document.paths, isEmpty);
expect(document.models, isEmpty);
expect(document.controllers, isEmpty);
});
test('creates SwaggerDocument from JSON with all fields', () {
final json = {
'info': {
'title': 'Test API',
'version': '1.0.0',
'description': 'Test API description',
},
'host': 'api.example.com',
'basePath': '/api',
'schemes': ['https', 'http'],
'consumes': ['application/json', 'application/xml'],
'produces': ['application/json', 'text/plain'],
};
final document = SwaggerDocument.fromJson(json);
expect(document.title, 'Test API');
expect(document.version, '1.0.0');
expect(document.description, 'Test API description');
expect(document.host, 'api.example.com');
expect(document.basePath, '/api');
expect(document.schemes, ['https', 'http']);
expect(document.consumes, ['application/json', 'application/xml']);
expect(document.produces, ['application/json', 'text/plain']);
});
test('creates SwaggerDocument from JSON with minimal fields', () {
final json = <String, dynamic>{};
final document = SwaggerDocument.fromJson(json);
expect(document.title, 'API');
expect(document.version, '1.0.0');
expect(document.description, '');
expect(document.host, '');
expect(document.basePath, '');
expect(document.schemes, ['https']);
expect(document.consumes, ['application/json']);
expect(document.produces, ['application/json']);
});
test('creates SwaggerDocument from JSON with null info', () {
final json = {'info': null};
final document = SwaggerDocument.fromJson(json);
expect(document.title, 'API');
expect(document.version, '1.0.0');
expect(document.description, '');
});
});
group('ApiController', () {
test('creates ApiController with required fields', () {
const controller = ApiController(
name: 'UserController',
description: 'User management controller',
paths: [],
);
expect(controller.name, 'UserController');
expect(controller.description, 'User management controller');
expect(controller.paths, isEmpty);
});
test('creates ApiController with paths', () {
final paths = [
const ApiPath(
path: '/api/users',
method: HttpMethod.get,
summary: 'Get users',
description: 'Retrieve all users',
operationId: 'getUsers',
tags: ['User'],
parameters: [],
responses: {},
requestBody: null,
),
const ApiPath(
path: '/api/users/{id}',
method: HttpMethod.post,
summary: 'Create user',
description: 'Create a new user',
operationId: 'createUser',
tags: ['User'],
parameters: [],
responses: {},
requestBody: null,
),
];
final controller = ApiController(
name: 'UserController',
description: 'User management controller',
paths: paths,
);
expect(controller.paths, hasLength(2));
expect(controller.paths[0].path, '/api/users');
expect(controller.paths[1].path, '/api/users/{id}');
});
test('creates ApiController from paths', () {
final paths = [
const ApiPath(
path: '/api/users',
method: HttpMethod.get,
summary: 'Get users',
description: 'Retrieve all users',
operationId: 'getUsers',
tags: ['User'],
parameters: [],
responses: {},
requestBody: null,
),
];
final controller = ApiController.fromPaths('UserController', paths);
expect(controller.name, 'UserController');
expect(controller.description, 'UserController');
expect(controller.paths, hasLength(1));
});
});
group('ApiPath fromJson', () {
test('creates ApiPath from JSON with all fields', () {
final json = {
'summary': 'Get users',
'description': 'Retrieve all users',
'operationId': 'getUsers',
'tags': ['User'],
'parameters': [
<String, dynamic>{
'name': 'id',
'in': 'path',
'required': true,
'type': 'integer',
'description': 'User ID',
}
],
'responses': <String, dynamic>{
'200': <String, dynamic>{
'description': 'Success',
'schema': <String, dynamic>{'type': 'object'},
}
},
'requestBody': <String, dynamic>{
'description': 'User data',
'required': true,
'content': <String, dynamic>{
'application/json': <String, dynamic>{
'schema': <String, dynamic>{'type': 'object'},
},
},
},
'deprecated': true,
};
final path = ApiPath.fromJson('/api/users/{id}', 'PUT', json);
expect(path.path, '/api/users/{id}');
expect(path.method, HttpMethod.put);
expect(path.summary, 'Get users');
expect(path.description, 'Retrieve all users');
expect(path.operationId, 'getUsers');
expect(path.tags, ['User']);
expect(path.parameters, hasLength(1));
expect(path.responses, hasLength(1));
expect(path.requestBody, isNotNull);
expect(path.deprecated, true);
});
test('creates ApiPath from JSON with minimal fields', () {
final json = <String, dynamic>{};
final path = ApiPath.fromJson('/api/users', 'GET', json);
expect(path.path, '/api/users');
expect(path.method, HttpMethod.get);
expect(path.summary, '');
expect(path.description, '');
expect(path.operationId, '');
expect(path.tags, isEmpty);
expect(path.parameters, isEmpty);
expect(path.responses, isEmpty);
expect(path.requestBody, isNull);
expect(path.deprecated, false);
});
});
group('ApiModel fromJson edge cases', () {
test('creates ApiModel with nullable properties', () {
final json = {
'description': 'User model',
'properties': {
'id': {
'type': 'integer',
'description': 'User ID',
'nullable': true,
},
'name': {
'type': 'string',
'description': 'User name',
'nullable': false,
},
},
};
final model = ApiModel.fromJson('User', json);
expect(model.properties['id']!.nullable, true);
expect(model.properties['id']!.required, false);
expect(model.properties['name']!.nullable, false);
expect(model.properties['name']!.required, true);
expect(model.required, ['name']);
});
test('creates ApiModel with explicit required fields', () {
final json = {
'description': 'User model',
'properties': {
'id': {
'type': 'integer',
'description': 'User ID',
'nullable': true,
},
'name': {
'type': 'string',
'description': 'User name',
'nullable': false,
},
},
'required': ['id', 'name'],
};
final model = ApiModel.fromJson('User', json);
expect(model.properties['id']!.required, true);
expect(model.properties['name']!.required, true);
expect(model.required, ['id', 'name']);
});
});
group('ApiProperty complex types', () {
test('creates ApiProperty with nested object', () {
final json = {
'type': 'object',
'description': 'User address',
'properties': {
'street': {'type': 'string'},
'city': {'type': 'string'},
},
};
final property = ApiProperty.fromJson('address', json, []);
expect(property.type, PropertyType.object);
expect(property.description, 'User address');
expect(property.required, false);
});
test('creates ApiProperty with array of primitives', () {
final json = {
'type': 'array',
'description': 'User tags',
'items': {
'type': 'string',
},
};
final property = ApiProperty.fromJson('tags', json, []);
expect(property.type, PropertyType.array);
expect(property.description, 'User tags');
expect(property.items, isNotNull);
expect(property.items!.name, 'string');
});
test('creates ApiProperty with array of objects', () {
final json = {
'type': 'array',
'description': 'User list',
'items': {
'type': 'object',
'\$ref': '#/components/schemas/User',
},
};
final property = ApiProperty.fromJson('users', json, []);
expect(property.type, PropertyType.array);
expect(property.description, 'User list');
expect(property.items, isNotNull);
expect(property.items!.name, 'User');
});
test('creates ApiProperty with file type', () {
final json = {
'type': 'file',
'description': 'User avatar',
'format': 'binary',
};
final property = ApiProperty.fromJson('avatar', json, []);
expect(property.type, PropertyType.file);
expect(property.description, 'User avatar');
expect(property.format, 'binary');
});
test('creates ApiProperty with date type', () {
final json = {
'type': 'string',
'format': 'date',
'description': 'User birth date',
};
final property = ApiProperty.fromJson('birthDate', json, []);
expect(property.type, PropertyType.string);
expect(property.format, 'date');
expect(property.description, 'User birth date');
});
test('creates ApiProperty with date-time type', () {
final json = {
'type': 'string',
'format': 'date-time',
'description': 'User created at',
};
final property = ApiProperty.fromJson('createdAt', json, []);
expect(property.type, PropertyType.string);
expect(property.format, 'date-time');
expect(property.description, 'User created at');
});
});
group('Error handling and edge cases', () {
test('ApiParameter fromJson handles missing fields gracefully', () {
final json = <String, dynamic>{};
final param = ApiParameter.fromJson(json);
expect(param.name, '');
expect(param.location, ParameterLocation.query);
expect(param.required, false);
expect(param.type, PropertyType.string);
expect(param.description, '');
expect(param.format, isNull);
expect(param.example, isNull);
expect(param.defaultValue, isNull);
});
test('ApiResponse fromJson handles missing fields gracefully', () {
final json = <String, dynamic>{};
final response = ApiResponse.fromJson('200', json);
expect(response.code, '200');
expect(response.description, '');
expect(response.schema, isNull);
expect(response.content, isNull);
});
test('ApiRequestBody fromJson handles missing fields gracefully', () {
final json = <String, dynamic>{};
final requestBody = ApiRequestBody.fromJson(json);
expect(requestBody.description, '');
expect(requestBody.required, false);
expect(requestBody.content, isNull);
});
test('PropertyType fromString handles unknown types', () {
expect(PropertyType.fromString('unknown'), PropertyType.string);
expect(PropertyType.fromString(''), PropertyType.string);
expect(PropertyType.fromString('CUSTOM_TYPE'), PropertyType.string);
});
test('ParameterLocation fromString handles unknown locations', () {
expect(ParameterLocation.fromString('unknown'), ParameterLocation.query);
expect(ParameterLocation.fromString(''), ParameterLocation.query);
expect(ParameterLocation.fromString('CUSTOM_LOCATION'),
ParameterLocation.query);
});
test('HttpMethod fromString handles unknown methods', () {
expect(HttpMethod.fromString('unknown'), HttpMethod.get);
expect(HttpMethod.fromString(''), HttpMethod.get);
expect(HttpMethod.fromString('CUSTOM_METHOD'), HttpMethod.get);
});
});
}

View File

@ -0,0 +1,392 @@
import 'dart:convert';
import 'dart:io';
import 'package:swagger_generator_flutter/core/models.dart';
import 'package:swagger_generator_flutter/generators/optimized_retrofit_generator.dart';
import 'package:test/test.dart';
void main() {
group('OptimizedRetrofitGenerator', () {
late OptimizedRetrofitGenerator generator;
late SwaggerDocument testDocument;
setUp(() {
generator = OptimizedRetrofitGenerator(
className: 'TestApiService',
generateModularApis: true,
generateBaseResult: true,
generatePagination: true,
generateFileUpload: true,
);
//
testDocument = const SwaggerDocument(
title: 'Test API',
version: '1.0.0',
description: 'Test API for generator',
servers: [
ApiServer(
url: 'https://api.example.com',
description: 'Test server',
),
],
components: ApiComponents(
schemas: {},
securitySchemes: {},
),
paths: {
'/api/v1/users/{id}': const ApiPath(
path: '/api/v1/users/{id}',
method: HttpMethod.get,
summary: 'Get user by ID',
description: 'Retrieve user information by ID',
operationId: 'getUser',
tags: ['users'],
parameters: [
ApiParameter(
name: 'id',
location: ParameterLocation.path,
required: true,
type: PropertyType.integer,
description: 'User ID',
),
],
responses: {
'200': const ApiResponse(
code: '200',
description: 'Success',
content: {
'application/json': const ApiMediaType(
schema: {'type': 'object'},
),
},
),
},
),
'/api/v1/users': const ApiPath(
path: '/api/v1/users',
method: HttpMethod.post,
summary: 'Create user',
description: 'Create a new user',
operationId: 'createUser',
tags: ['users'],
parameters: [],
requestBody: ApiRequestBody(
description: 'User data',
required: true,
content: {
'application/json': const ApiMediaType(
schema: {'type': 'object'},
),
},
),
responses: {
'201': const ApiResponse(
code: '201',
description: 'Created',
content: {
'application/json': const ApiMediaType(
schema: {'type': 'object'},
),
},
),
},
),
'/api/v1/files/upload': const ApiPath(
path: '/api/v1/files/upload',
method: HttpMethod.post,
summary: 'Upload file',
description: 'Upload a file',
operationId: 'uploadFile',
tags: ['files'],
parameters: [],
requestBody: ApiRequestBody(
description: 'File to upload',
required: true,
content: {
'multipart/form-data': const ApiMediaType(
schema: {'type': 'object'},
),
},
),
responses: {
'200': const ApiResponse(
code: '200',
description: 'Success',
content: {
'application/json': const ApiMediaType(
schema: {'type': 'object'},
),
},
),
},
),
},
models: {},
controllers: {},
security: [],
);
});
test('generates optimized code successfully', () {
expect(
() => generator.generateFromDocument(testDocument), returnsNormally);
final generatedCode = generator.generateFromDocument(testDocument);
expect(generatedCode, isNotEmpty);
print('Generated code length: ${generatedCode.length} characters');
});
test('includes required imports', () {
final generatedCode = generator.generateFromDocument(testDocument);
expect(generatedCode, contains('import \'package:dio/dio.dart\';'));
expect(generatedCode,
contains('import \'package:retrofit/retrofit.dart\';'));
expect(generatedCode,
contains('import \'package:json_annotation/json_annotation.dart\';'));
expect(generatedCode, contains('part \'test_api_service_api.g.dart\';'));
});
test('generates BaseResult type', () {
final generatedCode = generator.generateFromDocument(testDocument);
expect(generatedCode, contains('class BaseResult<T>'));
expect(generatedCode, contains('final int code;'));
expect(generatedCode, contains('final String message;'));
expect(generatedCode, contains('final T? data;'));
expect(generatedCode, contains('bool get isSuccess => code == 200;'));
});
test('generates pagination types', () {
final generatedCode = generator.generateFromDocument(testDocument);
expect(generatedCode, contains('class BasePageParameter'));
expect(generatedCode, contains('class BasePageResult<T>'));
expect(generatedCode, contains('final int page;'));
expect(generatedCode, contains('final int size;'));
expect(generatedCode, contains('final int total;'));
expect(generatedCode, contains('int get totalPages'));
expect(generatedCode, contains('bool get hasNext'));
expect(generatedCode, contains('bool get hasPrevious'));
});
test('generates file upload types', () {
final generatedCode = generator.generateFromDocument(testDocument);
expect(generatedCode, contains('class FileUploadRequest'));
expect(generatedCode, contains('class FileUploadResult'));
expect(generatedCode, contains('final MultipartFile file;'));
expect(generatedCode, contains('final String url;'));
expect(generatedCode, contains('final String filename;'));
expect(generatedCode, contains('final int size;'));
});
test('generates modular APIs', () {
final generatedCode = generator.generateFromDocument(testDocument);
expect(generatedCode, contains('class UsersApi'));
expect(generatedCode, contains('class FilesApi'));
expect(generatedCode, contains('class TestApiService'));
expect(generatedCode, contains('late final UsersApi users;'));
expect(generatedCode, contains('late final FilesApi files;'));
});
test('generates API methods with correct annotations', () {
final generatedCode = generator.generateFromDocument(testDocument);
// GET
expect(generatedCode, contains('@GET(\'/api/v1/users/{id}\')'));
expect(generatedCode, contains('@Path(\'id\') int id'));
// POST
expect(generatedCode, contains('@POST(\'/api/v1/users\')'));
expect(generatedCode, contains('@Body() Map<String, dynamic> body'));
//
expect(generatedCode, contains('@POST(\'/api/v1/files/upload\')'));
expect(generatedCode, contains('@MultiPart()'));
expect(generatedCode, contains('@Part() MultipartFile file'));
});
test('generates utility classes', () {
final generatedCode = generator.generateFromDocument(testDocument);
expect(generatedCode, contains('class ApiUtils'));
expect(generatedCode,
contains('static Future<MultipartFile> createFileUpload'));
expect(
generatedCode, contains('static BasePageParameter createPageParam'));
});
test('handles method name generation correctly', () {
final generatedCode = generator.generateFromDocument(testDocument);
expect(generatedCode, contains('Future<BaseResult<dynamic>> getUsers('));
expect(generatedCode, contains('Future<BaseResult<dynamic>> postUsers('));
expect(generatedCode,
contains('Future<BaseResult<dynamic>> postFilesUpload('));
});
test('generates single API when modular is disabled', () {
final singleApiGenerator = OptimizedRetrofitGenerator(
className: 'SingleApiService',
generateModularApis: false,
);
final generatedCode =
singleApiGenerator.generateFromDocument(testDocument);
expect(generatedCode, contains('abstract class SingleApiService'));
expect(generatedCode, contains('factory SingleApiService(Dio dio'));
expect(generatedCode, isNot(contains('class UsersApi')));
expect(generatedCode, isNot(contains('class FilesApi')));
});
test('handles custom base result type', () {
final customGenerator = OptimizedRetrofitGenerator(
baseResultType: 'CustomResult',
pageResultType: 'CustomPageResult',
);
final generatedCode = customGenerator.generateFromDocument(testDocument);
expect(generatedCode, contains('class CustomResult<T>'));
expect(generatedCode, contains('class CustomPageResult<T>'));
expect(generatedCode, contains('Future<CustomResult<dynamic>>'));
});
test('extracts module names correctly', () {
final generator = OptimizedRetrofitGenerator();
// 使
//
final generatedCode = generator.generateFromDocument(testDocument);
expect(generatedCode, contains('UsersApi')); // /api/v1/users -> Users
expect(generatedCode, contains('FilesApi')); // /api/v1/files -> Files
});
test('handles different parameter types', () {
//
const complexDocument = SwaggerDocument(
title: 'Complex API',
version: '1.0.0',
description: 'API with complex parameters',
servers: [ApiServer(url: 'https://api.example.com')],
components: ApiComponents(schemas: {}, securitySchemes: {}),
paths: {
'/api/v1/search': const ApiPath(
path: '/api/v1/search',
method: HttpMethod.get,
summary: 'Search',
description: 'Search with various parameters',
operationId: 'search',
tags: ['search'],
parameters: [
ApiParameter(
name: 'query',
location: ParameterLocation.query,
required: true,
type: PropertyType.string,
description: 'Search query',
),
ApiParameter(
name: 'page',
location: ParameterLocation.query,
required: false,
type: PropertyType.integer,
description: 'Page number',
),
ApiParameter(
name: 'active',
location: ParameterLocation.query,
required: false,
type: PropertyType.boolean,
description: 'Filter active items',
),
],
responses: {
'200': const ApiResponse(
code: '200',
description: 'Success',
content: {
'application/json':
const ApiMediaType(schema: {'type': 'object'}),
},
),
},
),
},
models: {},
controllers: {},
security: [],
);
final generatedCode = generator.generateFromDocument(complexDocument);
expect(
generatedCode, contains('@Query(\'query\') required String query'));
expect(generatedCode, contains('@Query(\'page\') int? page'));
expect(generatedCode, contains('@Query(\'active\') bool? active'));
});
test('performance test with complex document', () {
final stopwatch = Stopwatch()..start();
final generatedCode = generator.generateFromDocument(testDocument);
final elapsedMs = stopwatch.elapsedMilliseconds;
stopwatch.stop();
print('Generation time: ${elapsedMs}ms');
print('Generated code size: ${generatedCode.length} characters');
expect(elapsedMs, lessThan(1000)); // 1
expect(generatedCode.length, greaterThan(1000)); //
});
});
group('Real Project Integration', () {
test('generates code for real swagger.json', () async {
// swagger.json
final file = File('swagger.json');
if (!file.existsSync()) {
print('swagger.json not found, skipping real project test');
return;
}
final jsonString = await file.readAsString();
final swaggerJson = jsonDecode(jsonString) as Map<String, dynamic>;
final document = SwaggerDocument.fromJson(swaggerJson);
final generator = OptimizedRetrofitGenerator(
className: 'OAMobileApiService',
generateModularApis: true,
generateBaseResult: true,
generatePagination: true,
generateFileUpload: true,
);
final stopwatch = Stopwatch()..start();
final generatedCode = generator.generateFromDocument(document);
final elapsedMs = stopwatch.elapsedMilliseconds;
print('Real project generation:');
print(' Time: ${elapsedMs}ms');
print(' Code size: ${generatedCode.length} characters');
print(' Lines: ${generatedCode.split('\n').length}');
expect(generatedCode, isNotEmpty);
expect(generatedCode, contains('class OAMobileApiService'));
expect(generatedCode, contains('FollowManagerApi'));
expect(generatedCode, contains('LoginApi'));
expect(generatedCode, contains('IndexApi'));
//
// final outputFile = File('generated_oa_mobile_api.dart');
// await outputFile.writeAsString(generatedCode);
// print('Generated code written to: ${outputFile.path}');
});
});
}

488
tests/performance_test.dart Normal file
View File

@ -0,0 +1,488 @@
import 'dart:convert';
import 'dart:io';
import 'package:swagger_generator_flutter/core/models.dart';
import 'package:swagger_generator_flutter/core/performance_parser.dart';
import 'package:swagger_generator_flutter/core/smart_cache.dart';
import 'package:swagger_generator_flutter/generators/performance_generator.dart';
import 'package:test/test.dart';
void main() {
group('Performance Tests', () {
group('PerformanceParser', () {
late PerformanceParser parser;
setUp(() {
parser = PerformanceParser(
config: ParseConfig(
enableParallelParsing: true,
enableStreamParsing: false,
enablePerformanceStats: true,
maxConcurrency: 4,
),
);
});
test('parses document with performance tracking', () async {
final jsonString = jsonEncode({
'openapi': '3.0.3',
'info': {
'title': 'Test API',
'version': '1.0.0',
},
'paths': {
'/users': {
'get': {
'summary': 'Get users',
'responses': {
'200': {
'description': 'Success',
},
},
},
},
},
});
final document = await parser.parseDocument(jsonString);
expect(document.title, equals('Test API'));
expect(document.version, equals('1.0.0'));
expect(document.paths, hasLength(1));
final stats = parser.lastStats;
expect(stats, isNotNull);
expect(stats!.totalTime.inMicroseconds, greaterThan(0));
expect(stats.documentSize, equals(jsonString.length));
expect(stats.pathCount, equals(1));
});
test('handles large documents efficiently', () async {
//
final paths = <String, dynamic>{};
for (int i = 0; i < 100; i++) {
paths['/api/v1/resource$i'] = {
'get': {
'summary': 'Get resource $i',
'responses': {
'200': {'description': 'Success'},
},
},
'post': {
'summary': 'Create resource $i',
'responses': {
'201': {'description': 'Created'},
},
},
};
}
final largeDoc = {
'openapi': '3.0.3',
'info': {
'title': 'Large API',
'version': '1.0.0',
},
'paths': paths,
};
final jsonString = jsonEncode(largeDoc);
final stopwatch = Stopwatch()..start();
final document = await parser.parseDocument(jsonString);
stopwatch.stop();
expect(document.paths.length, greaterThan(50)); //
expect(stopwatch.elapsedMilliseconds, lessThan(5000)); // 5
final stats = parser.lastStats;
expect(stats, isNotNull);
expect(stats!.pathsPerSecond, greaterThan(10)); // 10
});
test('parallel parsing improves performance', () async {
final sequentialParser = PerformanceParser(
config: ParseConfig(
enableParallelParsing: false,
enablePerformanceStats: true,
),
);
final parallelParser = PerformanceParser(
config: ParseConfig(
enableParallelParsing: true,
enablePerformanceStats: true,
maxConcurrency: 4,
),
);
//
final paths = <String, dynamic>{};
for (int i = 0; i < 50; i++) {
paths['/api/v1/resource$i'] = {
'get': {
'summary': 'Get resource $i',
'responses': {
'200': {'description': 'Success'}
},
},
};
}
final doc = {
'openapi': '3.0.3',
'info': {'title': 'Test API', 'version': '1.0.0'},
'paths': paths,
};
final jsonString = jsonEncode(doc);
//
await sequentialParser.parseDocument(jsonString);
final sequentialTime = sequentialParser.lastStats!.totalTime;
//
await parallelParser.parseDocument(jsonString);
final parallelTime = parallelParser.lastStats!.totalTime;
//
print('Sequential: ${sequentialTime.inMilliseconds}ms');
print('Parallel: ${parallelTime.inMilliseconds}ms');
//
expect(parallelTime.inMilliseconds,
lessThan(sequentialTime.inMilliseconds * 2));
});
});
group('SmartCache', () {
late SmartCache<String> cache;
setUp(() {
cache = SmartCache<String>(
maxSize: 10,
strategy: CacheStrategy.smart,
defaultTtl: Duration(seconds: 1),
);
});
test('basic cache operations work correctly', () {
cache.put('key1', 'value1');
expect(cache.get('key1'), equals('value1'));
expect(cache.containsKey('key1'), isTrue);
cache.remove('key1');
expect(cache.get('key1'), isNull);
expect(cache.containsKey('key1'), isFalse);
});
test('cache eviction works when full', () {
//
for (int i = 0; i < 10; i++) {
cache.put('key$i', 'value$i');
}
expect(cache.getStats().size, equals(10));
//
cache.put('key10', 'value10');
expect(cache.getStats().size, equals(10));
expect(cache.getStats().evictions, greaterThan(0));
});
test('TTL expiration works', () async {
cache.put('key1', 'value1', ttl: Duration(milliseconds: 100));
expect(cache.get('key1'), equals('value1'));
await Future.delayed(Duration(milliseconds: 150));
expect(cache.get('key1'), isNull);
});
test('cache statistics are accurate', () {
cache.put('key1', 'value1');
cache.get('key1'); // hit
cache.get('key2'); // miss
final stats = cache.getStats();
expect(stats.totalRequests, equals(2));
expect(stats.hits, equals(1));
expect(stats.misses, equals(1));
expect(stats.hitRate, equals(0.5));
});
test('smart eviction strategy works', () {
// 访
cache.put('frequent', 'value1');
cache.put('recent', 'value2');
cache.put('old', 'value3');
// 访
for (int i = 0; i < 5; i++) {
cache.get('frequent');
}
// 访
cache.get('recent');
// 访
//
for (int i = 0; i < 8; i++) {
cache.put('filler$i', 'value$i');
}
// 访
expect(cache.get('frequent'), equals('value1'));
//
// expect(cache.get('old'), isNull);
});
});
group('PerformanceGenerator', () {
late PerformanceGenerator generator;
late SwaggerDocument testDocument;
setUp(() {
generator = PerformanceGenerator(
maxConcurrency: 4,
enableCaching: true,
enableIncremental: true,
enableParallel: true,
);
testDocument = SwaggerDocument(
title: 'Performance Test API',
version: '1.0.0',
description: 'API for performance testing',
servers: [ApiServer(url: 'https://api.example.com')],
components: ApiComponents(schemas: {}, securitySchemes: {}),
paths: {
'/users': ApiPath(
path: '/users',
method: HttpMethod.get,
summary: 'Get users',
description: 'Get all users',
operationId: 'getUsers',
tags: ['users'],
parameters: [],
responses: {
'200': ApiResponse(
code: '200',
description: 'Success',
),
},
),
'/posts': ApiPath(
path: '/posts',
method: HttpMethod.get,
summary: 'Get posts',
description: 'Get all posts',
operationId: 'getPosts',
tags: ['posts'],
parameters: [],
responses: {
'200': ApiResponse(
code: '200',
description: 'Success',
),
},
),
},
models: {
'User': ApiModel(
name: 'User',
description: 'User model',
properties: {
'id': ApiProperty(
name: 'id',
type: PropertyType.integer,
description: 'User ID',
required: true,
),
'name': ApiProperty(
name: 'name',
type: PropertyType.string,
description: 'User name',
required: true,
),
},
required: ['id', 'name'],
),
},
controllers: {},
security: [],
);
});
test('generates code with performance tracking', () async {
final result = await generator.generateFromDocument(testDocument);
expect(result, isNotEmpty);
expect(result, contains('Performance Test API'));
expect(result, contains('class User'));
expect(result, contains('UsersApi'));
expect(result, contains('PostsApi'));
final stats = generator.getStats();
expect(stats.totalTasks, greaterThan(0));
expect(stats.completedTasks, greaterThan(0));
expect(stats.successRate, equals(1.0));
});
test('caching improves performance on repeated generation', () async {
//
final stopwatch1 = Stopwatch()..start();
await generator.generateFromDocument(testDocument);
stopwatch1.stop();
final firstTime = stopwatch1.elapsedMicroseconds;
// 使
final stopwatch2 = Stopwatch()..start();
await generator.generateFromDocument(testDocument);
stopwatch2.stop();
final secondTime = stopwatch2.elapsedMicroseconds;
print('First generation: ${firstTime}μs');
print('Second generation: ${secondTime}μs');
//
expect(secondTime, lessThan(firstTime));
final cacheStats = generator.getCacheStats();
expect(cacheStats.hits, greaterThan(0));
});
test('parallel generation works with multiple modules', () async {
//
final largePaths = <String, ApiPath>{};
final modules = ['users', 'posts', 'comments', 'tags', 'categories'];
for (final module in modules) {
for (int i = 0; i < 5; i++) {
final path = '/api/v1/$module/$i';
largePaths[path] = ApiPath(
path: path,
method: HttpMethod.get,
summary: 'Get $module $i',
description: 'Get $module item $i',
operationId: 'get${module.toUpperCase()}$i',
tags: [module],
parameters: [],
responses: {
'200': ApiResponse(code: '200', description: 'Success'),
},
);
}
}
final largeDocument = SwaggerDocument(
title: 'Large API',
version: '1.0.0',
description: 'Large API for testing',
servers: [ApiServer(url: 'https://api.example.com')],
components: ApiComponents(schemas: {}, securitySchemes: {}),
paths: largePaths,
models: {},
controllers: {},
security: [],
);
final result = await generator.generateFromDocument(largeDocument);
expect(result, isNotEmpty);
expect(result, contains('Large API'));
// API
for (final module in modules) {
final className =
'${module[0].toUpperCase()}${module.substring(1)}Api';
expect(result, contains(className));
}
final stats = generator.getStats();
expect(stats.totalTasks, greaterThan(modules.length));
expect(stats.parallelEfficiency, greaterThan(0.5));
});
test('incremental generation detects no changes', () async {
//
await generator.generateFromDocument(testDocument);
final firstStats = generator.getStats();
//
await generator.generateFromDocument(testDocument);
final secondStats = generator.getStats();
//
expect(
secondStats.totalTasks, lessThanOrEqualTo(firstStats.totalTasks));
});
});
group('Real Project Performance', () {
test('handles real swagger.json efficiently', () async {
final file = File('swagger.json');
if (!file.existsSync()) {
print(
'swagger.json not found, skipping real project performance test');
return;
}
final jsonString = await file.readAsString();
//
final parser = PerformanceParser(
config: ParseConfig(
enableParallelParsing: true,
enablePerformanceStats: true,
maxConcurrency: 4,
),
);
final parseStopwatch = Stopwatch()..start();
final document = await parser.parseDocument(jsonString);
parseStopwatch.stop();
print('Parse Performance:');
print(' Time: ${parseStopwatch.elapsedMilliseconds}ms');
print(
' Document size: ${(jsonString.length / 1024).toStringAsFixed(2)}KB');
print(' Paths: ${document.paths.length}');
print(' Models: ${document.models.length}');
final parseStats = parser.lastStats!;
print(parseStats.toString());
expect(
parseStopwatch.elapsedMilliseconds, lessThan(10000)); // 10
expect(parseStats.pathsPerSecond, greaterThan(1)); // 1
//
final generator = PerformanceGenerator(
maxConcurrency: 4,
enableCaching: true,
enableParallel: true,
);
final genStopwatch = Stopwatch()..start();
final result = await generator.generateFromDocument(document);
genStopwatch.stop();
print('\nGeneration Performance:');
print(' Time: ${genStopwatch.elapsedMilliseconds}ms');
print(
' Generated size: ${(result.length / 1024).toStringAsFixed(2)}KB');
print(' Lines: ${result.split('\n').length}');
final genStats = generator.getStats();
print(genStats.toString());
expect(genStopwatch.elapsedMilliseconds, lessThan(15000)); // 15
expect(result.length, greaterThan(1000)); //
expect(genStats.successRate, greaterThan(0.8)); // 80%
});
});
});
}

View File

@ -0,0 +1,325 @@
import 'package:swagger_generator_flutter/core/models.dart';
import 'package:swagger_generator_flutter/utils/reference_resolver.dart';
import 'package:test/test.dart';
void main() {
group('ReferenceResolver', () {
late ReferenceResolver resolver;
setUp(() {
resolver = ReferenceResolver();
});
tearDown(() {
resolver.clearCache();
});
test('resolves simple models without references', () {
final componentsJson = {
'schemas': {
'User': {
'type': 'object',
'description': 'User model',
'properties': {
'id': {'type': 'integer'},
'name': {'type': 'string'},
},
'required': ['id', 'name'],
},
'Product': {
'type': 'object',
'description': 'Product model',
'properties': {
'id': {'type': 'integer'},
'title': {'type': 'string'},
'price': {'type': 'number'},
},
'required': ['id', 'title'],
},
},
};
final models = resolver.resolveModels(componentsJson);
expect(models.length, 2);
expect(models['User'], isNotNull);
expect(models['User']!.name, 'User');
expect(models['User']!.properties.length, 2);
expect(models['User']!.required, ['id', 'name']);
expect(models['Product'], isNotNull);
expect(models['Product']!.name, 'Product');
expect(models['Product']!.properties.length, 3);
expect(models['Product']!.required, ['id', 'title']);
});
test('resolves models with allOf composition', () {
final componentsJson = {
'schemas': {
'BaseEntity': {
'type': 'object',
'properties': {
'id': {'type': 'integer'},
'createdAt': {'type': 'string', 'format': 'date-time'},
},
'required': ['id'],
},
'User': {
'allOf': [
{'\$ref': '#/components/schemas/BaseEntity'},
{
'type': 'object',
'properties': {
'name': {'type': 'string'},
'email': {'type': 'string'},
},
'required': ['name'],
},
],
},
},
};
final models = resolver.resolveModels(componentsJson);
expect(models.length, 2);
expect(models['User'], isNotNull);
expect(models['User']!.isAllOf, true);
expect(models['User']!.allOf.length, 2);
//
expect(
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);
expect(models['User']!.properties['createdAt'], isNotNull);
});
test('detects and handles circular references', () {
final componentsJson = {
'schemas': {
'Node': {
'type': 'object',
'properties': {
'id': {'type': 'integer'},
'parent': {'\$ref': '#/components/schemas/Node'},
'children': {
'type': 'array',
'items': {'\$ref': '#/components/schemas/Node'},
},
},
'required': ['id'],
},
},
};
final models = resolver.resolveModels(componentsJson);
expect(models.length, 1);
expect(models['Node'], isNotNull);
expect(models['Node']!.name, 'Node');
expect(models['Node']!.properties.length, 3);
//
expect(models['Node']!.properties['parent'], isNotNull);
expect(models['Node']!.properties['children'], isNotNull);
});
test('handles complex nested structures', () {
final componentsJson = {
'schemas': {
'Address': {
'type': 'object',
'properties': {
'street': {'type': 'string'},
'city': {'type': 'string'},
'coordinates': {
'type': 'object',
'properties': {
'lat': {'type': 'number'},
'lng': {'type': 'number'},
},
'required': ['lat', 'lng'],
},
},
'required': ['street', 'city'],
},
'User': {
'type': 'object',
'properties': {
'id': {'type': 'integer'},
'name': {'type': 'string'},
'addresses': {
'type': 'array',
'items': {'\$ref': '#/components/schemas/Address'},
},
},
'required': ['id', 'name'],
},
},
};
final models = resolver.resolveModels(componentsJson);
expect(models.length, 2);
expect(models['User'], isNotNull);
expect(models['Address'], isNotNull);
final userModel = models['User']!;
expect(userModel.properties['addresses'], isNotNull);
expect(userModel.properties['addresses']!.type, PropertyType.array);
final addressModel = models['Address']!;
expect(addressModel.properties['coordinates'], isNotNull);
expect(addressModel.properties['coordinates']!.type, PropertyType.object);
});
test('handles enum models', () {
final componentsJson = {
'schemas': {
'Status': {
'type': 'string',
'enum': ['active', 'inactive', 'pending'],
'description': 'User status',
},
'Priority': {
'type': 'integer',
'enum': [1, 2, 3, 4, 5],
'description': 'Priority level',
},
},
};
final models = resolver.resolveModels(componentsJson);
expect(models.length, 2);
final statusModel = models['Status']!;
expect(statusModel.isEnum, true);
expect(statusModel.enumValues, ['active', 'inactive', 'pending']);
expect(statusModel.enumType, PropertyType.string);
final priorityModel = models['Priority']!;
expect(priorityModel.isEnum, true);
expect(priorityModel.enumValues, [1, 2, 3, 4, 5]);
expect(priorityModel.enumType, PropertyType.integer);
});
test('handles oneOf with discriminator', () {
final componentsJson = {
'schemas': {
'Pet': {
'oneOf': [
{'\$ref': '#/components/schemas/Cat'},
{'\$ref': '#/components/schemas/Dog'},
],
'discriminator': {
'propertyName': 'petType',
'mapping': {
'cat': '#/components/schemas/Cat',
'dog': '#/components/schemas/Dog',
},
},
},
'Cat': {
'type': 'object',
'properties': {
'petType': {'type': 'string'},
'meowSound': {'type': 'string'},
},
'required': ['petType'],
},
'Dog': {
'type': 'object',
'properties': {
'petType': {'type': 'string'},
'barkSound': {'type': 'string'},
},
'required': ['petType'],
},
},
};
final models = resolver.resolveModels(componentsJson);
expect(models.length, 3);
final petModel = models['Pet']!;
expect(petModel.isOneOf, true);
expect(petModel.hasDiscriminator, true);
expect(petModel.discriminator?.propertyName, 'petType');
expect(petModel.discriminator?.mapping.length, 2);
expect(models['Cat'], isNotNull);
expect(models['Dog'], isNotNull);
});
test('respects maximum depth limit', () {
final resolver = ReferenceResolver(maxDepth: 3);
final componentsJson = {
'schemas': {
'DeepNested': {
'type': 'object',
'properties': {
'level1': {
'type': 'object',
'properties': {
'level2': {
'type': 'object',
'properties': {
'level3': {
'type': 'object',
'properties': {
'level4': {'type': 'string'},
},
},
},
},
},
},
},
},
},
};
final models = resolver.resolveModels(componentsJson);
expect(models.length, 1);
expect(models['DeepNested'], isNotNull);
//
final model = models['DeepNested']!;
expect(model.properties['level1'], isNotNull);
});
test('handles parsing errors gracefully', () {
final componentsJson = {
'schemas': {
'ValidModel': {
'type': 'object',
'properties': {
'id': {'type': 'integer'},
},
},
'InvalidModel': {
//
'properties': 'invalid_properties_value', //
'required': 123, //
},
},
};
final models = resolver.resolveModels(componentsJson);
expect(models.length, 2);
expect(models['ValidModel'], isNotNull);
expect(models['InvalidModel'], isNotNull);
//
final invalidModel = models['InvalidModel']!;
expect(invalidModel.description, '解析失败的模型');
});
});
}

504
tests/security_test.dart Normal file
View File

@ -0,0 +1,504 @@
import 'package:swagger_generator_flutter/core/models.dart';
import 'package:test/test.dart';
void main() {
group('OAuth2 Flow', () {
test('creates OAuth2Flow with all fields', () {
final json = {
'authorizationUrl': 'https://example.com/oauth/authorize',
'tokenUrl': 'https://example.com/oauth/token',
'refreshUrl': 'https://example.com/oauth/refresh',
'scopes': {
'read': 'Read access',
'write': 'Write access',
'admin': 'Admin access',
},
};
final flow = OAuth2Flow.fromJson(json);
expect(flow.authorizationUrl, 'https://example.com/oauth/authorize');
expect(flow.tokenUrl, 'https://example.com/oauth/token');
expect(flow.refreshUrl, 'https://example.com/oauth/refresh');
expect(flow.scopes.length, 3);
expect(flow.scopes['read'], 'Read access');
expect(flow.scopes['write'], 'Write access');
expect(flow.scopes['admin'], 'Admin access');
expect(flow.hasAuthorizationUrl, true);
expect(flow.hasTokenUrl, true);
expect(flow.hasRefreshUrl, true);
expect(flow.hasScopes, true);
});
test('creates OAuth2Flow with minimal fields', () {
final json = {
'tokenUrl': 'https://example.com/oauth/token',
'scopes': {},
};
final flow = OAuth2Flow.fromJson(json);
expect(flow.authorizationUrl, isNull);
expect(flow.tokenUrl, 'https://example.com/oauth/token');
expect(flow.refreshUrl, isNull);
expect(flow.scopes, isEmpty);
expect(flow.hasAuthorizationUrl, false);
expect(flow.hasTokenUrl, true);
expect(flow.hasRefreshUrl, false);
expect(flow.hasScopes, false);
});
});
group('OAuth2 Flows', () {
test('creates OAuth2Flows with multiple flow types', () {
final json = {
'authorizationCode': {
'authorizationUrl': 'https://example.com/oauth/authorize',
'tokenUrl': 'https://example.com/oauth/token',
'scopes': {
'read': 'Read access',
'write': 'Write access',
},
},
'implicit': {
'authorizationUrl': 'https://example.com/oauth/authorize',
'scopes': {
'read': 'Read access',
},
},
'clientCredentials': {
'tokenUrl': 'https://example.com/oauth/token',
'scopes': {
'admin': 'Admin access',
},
},
};
final flows = OAuth2Flows.fromJson(json);
expect(flows.hasAnyFlow, true);
expect(flows.availableFlows.length, 3);
expect(flows.availableFlows.contains(OAuth2FlowType.authorizationCode),
true);
expect(flows.availableFlows.contains(OAuth2FlowType.implicit), true);
expect(flows.availableFlows.contains(OAuth2FlowType.clientCredentials),
true);
expect(flows.availableFlows.contains(OAuth2FlowType.password), false);
expect(flows.authorizationCode, isNotNull);
expect(flows.implicit, isNotNull);
expect(flows.password, isNull);
expect(flows.clientCredentials, isNotNull);
expect(flows.getFlow(OAuth2FlowType.authorizationCode), isNotNull);
expect(flows.getFlow(OAuth2FlowType.password), isNull);
});
test('creates empty OAuth2Flows', () {
final json = <String, dynamic>{};
final flows = OAuth2Flows.fromJson(json);
expect(flows.hasAnyFlow, false);
expect(flows.availableFlows, isEmpty);
expect(flows.authorizationCode, isNull);
expect(flows.implicit, isNull);
expect(flows.password, isNull);
expect(flows.clientCredentials, isNull);
});
});
group('ApiSecurityScheme', () {
test('creates API Key security scheme', () {
final json = {
'type': 'apiKey',
'description': 'API Key authentication',
'name': 'X-API-Key',
'in': 'header',
};
final scheme = ApiSecurityScheme.fromJson(json);
expect(scheme.type, SecuritySchemeType.apiKey);
expect(scheme.description, 'API Key authentication');
expect(scheme.name, 'X-API-Key');
expect(scheme.location, ApiKeyLocation.header);
expect(scheme.isApiKey, true);
expect(scheme.isHttp, false);
expect(scheme.isOAuth2, false);
expect(scheme.isOpenIdConnect, false);
expect(scheme.apiKeyInfo, 'header:X-API-Key');
});
test('creates HTTP Bearer security scheme', () {
final json = {
'type': 'http',
'description': 'Bearer token authentication',
'scheme': 'bearer',
'bearerFormat': 'JWT',
};
final scheme = ApiSecurityScheme.fromJson(json);
expect(scheme.type, SecuritySchemeType.http);
expect(scheme.description, 'Bearer token authentication');
expect(scheme.scheme, 'bearer');
expect(scheme.bearerFormat, 'JWT');
expect(scheme.isHttp, true);
expect(scheme.isBearer, true);
expect(scheme.isBasic, false);
expect(scheme.httpAuthInfo, 'bearer (JWT)');
});
test('creates HTTP Basic security scheme', () {
final json = {
'type': 'http',
'description': 'Basic authentication',
'scheme': 'basic',
};
final scheme = ApiSecurityScheme.fromJson(json);
expect(scheme.type, SecuritySchemeType.http);
expect(scheme.scheme, 'basic');
expect(scheme.isHttp, true);
expect(scheme.isBasic, true);
expect(scheme.isBearer, false);
expect(scheme.httpAuthInfo, 'basic');
});
test('creates OAuth2 security scheme', () {
final json = {
'type': 'oauth2',
'description': 'OAuth2 authentication',
'flows': {
'authorizationCode': {
'authorizationUrl': 'https://example.com/oauth/authorize',
'tokenUrl': 'https://example.com/oauth/token',
'scopes': {
'read': 'Read access',
'write': 'Write access',
},
},
},
};
final scheme = ApiSecurityScheme.fromJson(json);
expect(scheme.type, SecuritySchemeType.oauth2);
expect(scheme.description, 'OAuth2 authentication');
expect(scheme.isOAuth2, true);
expect(scheme.hasOAuth2Flows, true);
expect(scheme.flows?.hasAnyFlow, true);
expect(scheme.flows?.authorizationCode, isNotNull);
});
test('creates OpenID Connect security scheme', () {
final json = {
'type': 'openIdConnect',
'description': 'OpenID Connect authentication',
'openIdConnectUrl':
'https://example.com/.well-known/openid_configuration',
};
final scheme = ApiSecurityScheme.fromJson(json);
expect(scheme.type, SecuritySchemeType.openIdConnect);
expect(scheme.description, 'OpenID Connect authentication');
expect(scheme.openIdConnectUrl,
'https://example.com/.well-known/openid_configuration');
expect(scheme.isOpenIdConnect, true);
});
});
group('ApiSecurityRequirement', () {
test('creates security requirement with scopes', () {
final json = {
'oauth2': ['read', 'write'],
'apiKey': [],
};
final requirement = ApiSecurityRequirement.fromJson(json);
expect(requirement.isNotEmpty, true);
expect(requirement.schemeNames.length, 2);
expect(requirement.hasScheme('oauth2'), true);
expect(requirement.hasScheme('apiKey'), true);
expect(requirement.hasScheme('unknown'), false);
expect(requirement.getScopesForScheme('oauth2'), ['read', 'write']);
expect(requirement.getScopesForScheme('apiKey'), isEmpty);
expect(requirement.getScopesForScheme('unknown'), isEmpty);
});
test('creates empty security requirement', () {
final json = <String, dynamic>{};
final requirement = ApiSecurityRequirement.fromJson(json);
expect(requirement.isEmpty, true);
expect(requirement.isNotEmpty, false);
expect(requirement.schemeNames, isEmpty);
});
});
group('Security Scheme Types', () {
test('converts security scheme type to string', () {
expect(SecuritySchemeType.apiKey.value, 'apiKey');
expect(SecuritySchemeType.http.value, 'http');
expect(SecuritySchemeType.oauth2.value, 'oauth2');
expect(SecuritySchemeType.openIdConnect.value, 'openIdConnect');
});
test('converts string to security scheme type', () {
expect(SecuritySchemeTypeExtension.fromString('apiKey'),
SecuritySchemeType.apiKey);
expect(SecuritySchemeTypeExtension.fromString('http'),
SecuritySchemeType.http);
expect(SecuritySchemeTypeExtension.fromString('oauth2'),
SecuritySchemeType.oauth2);
expect(SecuritySchemeTypeExtension.fromString('openIdConnect'),
SecuritySchemeType.openIdConnect);
expect(SecuritySchemeTypeExtension.fromString('unknown'),
SecuritySchemeType.apiKey);
});
test('converts API key location to string', () {
expect(ApiKeyLocation.query.value, 'query');
expect(ApiKeyLocation.header.value, 'header');
expect(ApiKeyLocation.cookie.value, 'cookie');
});
test('converts string to API key location', () {
expect(ApiKeyLocationExtension.fromString('query'), ApiKeyLocation.query);
expect(
ApiKeyLocationExtension.fromString('header'), ApiKeyLocation.header);
expect(
ApiKeyLocationExtension.fromString('cookie'), ApiKeyLocation.cookie);
expect(
ApiKeyLocationExtension.fromString('unknown'), ApiKeyLocation.header);
});
test('converts OAuth2 flow type to string', () {
expect(OAuth2FlowType.authorizationCode.value, 'authorizationCode');
expect(OAuth2FlowType.implicit.value, 'implicit');
expect(OAuth2FlowType.password.value, 'password');
expect(OAuth2FlowType.clientCredentials.value, 'clientCredentials');
});
test('converts string to OAuth2 flow type', () {
expect(OAuth2FlowTypeExtension.fromString('authorizationCode'),
OAuth2FlowType.authorizationCode);
expect(OAuth2FlowTypeExtension.fromString('implicit'),
OAuth2FlowType.implicit);
expect(OAuth2FlowTypeExtension.fromString('password'),
OAuth2FlowType.password);
expect(OAuth2FlowTypeExtension.fromString('clientCredentials'),
OAuth2FlowType.clientCredentials);
expect(OAuth2FlowTypeExtension.fromString('unknown'),
OAuth2FlowType.authorizationCode);
});
});
group('API Key Authentication Integration', () {
test('creates complete API Key security scheme for header', () {
final json = {
'type': 'apiKey',
'description': 'API Key in header',
'name': 'X-API-Key',
'in': 'header',
};
final scheme = ApiSecurityScheme.fromJson(json);
expect(scheme.isApiKey, true);
expect(scheme.name, 'X-API-Key');
expect(scheme.location, ApiKeyLocation.header);
expect(scheme.apiKeyInfo, 'header:X-API-Key');
});
test('creates complete API Key security scheme for query', () {
final json = {
'type': 'apiKey',
'description': 'API Key in query parameter',
'name': 'api_key',
'in': 'query',
};
final scheme = ApiSecurityScheme.fromJson(json);
expect(scheme.isApiKey, true);
expect(scheme.name, 'api_key');
expect(scheme.location, ApiKeyLocation.query);
expect(scheme.apiKeyInfo, 'query:api_key');
});
test('creates complete API Key security scheme for cookie', () {
final json = {
'type': 'apiKey',
'description': 'API Key in cookie',
'name': 'session_token',
'in': 'cookie',
};
final scheme = ApiSecurityScheme.fromJson(json);
expect(scheme.isApiKey, true);
expect(scheme.name, 'session_token');
expect(scheme.location, ApiKeyLocation.cookie);
expect(scheme.apiKeyInfo, 'cookie:session_token');
});
test('handles API Key security requirement', () {
final requirementJson = {
'api_key': [],
};
final requirement = ApiSecurityRequirement.fromJson(requirementJson);
expect(requirement.hasScheme('api_key'), true);
expect(requirement.getScopesForScheme('api_key'), isEmpty);
});
});
group('Bearer Token Authentication Integration', () {
test('creates complete Bearer token security scheme', () {
final json = {
'type': 'http',
'description': 'Bearer token authentication',
'scheme': 'bearer',
'bearerFormat': 'JWT',
};
final scheme = ApiSecurityScheme.fromJson(json);
expect(scheme.isHttp, true);
expect(scheme.isBearer, true);
expect(scheme.scheme, 'bearer');
expect(scheme.bearerFormat, 'JWT');
expect(scheme.httpAuthInfo, 'bearer (JWT)');
});
test('creates Bearer token without format', () {
final json = {
'type': 'http',
'description': 'Bearer token authentication',
'scheme': 'bearer',
};
final scheme = ApiSecurityScheme.fromJson(json);
expect(scheme.isHttp, true);
expect(scheme.isBearer, true);
expect(scheme.scheme, 'bearer');
expect(scheme.bearerFormat, isNull);
expect(scheme.httpAuthInfo, 'bearer');
});
test('handles Bearer token security requirement', () {
final requirementJson = {
'bearerAuth': [],
};
final requirement = ApiSecurityRequirement.fromJson(requirementJson);
expect(requirement.hasScheme('bearerAuth'), true);
expect(requirement.getScopesForScheme('bearerAuth'), isEmpty);
});
});
group('HTTP Authentication Schemes', () {
test('creates Basic authentication security scheme', () {
final json = {
'type': 'http',
'description': 'Basic authentication',
'scheme': 'basic',
};
final scheme = ApiSecurityScheme.fromJson(json);
expect(scheme.type, SecuritySchemeType.http);
expect(scheme.isHttp, true);
expect(scheme.isBasic, true);
expect(scheme.isBearer, false);
expect(scheme.scheme, 'basic');
expect(scheme.httpAuthInfo, 'basic');
});
test('creates Digest authentication security scheme', () {
final json = {
'type': 'http',
'description': 'Digest authentication',
'scheme': 'digest',
};
final scheme = ApiSecurityScheme.fromJson(json);
expect(scheme.type, SecuritySchemeType.http);
expect(scheme.isHttp, true);
expect(scheme.isBasic, false);
expect(scheme.isBearer, false);
expect(scheme.scheme, 'digest');
expect(scheme.httpAuthInfo, 'digest');
});
test('creates custom HTTP authentication security scheme', () {
final json = {
'type': 'http',
'description': 'Custom HTTP authentication',
'scheme': 'custom',
};
final scheme = ApiSecurityScheme.fromJson(json);
expect(scheme.type, SecuritySchemeType.http);
expect(scheme.isHttp, true);
expect(scheme.isBasic, false);
expect(scheme.isBearer, false);
expect(scheme.scheme, 'custom');
expect(scheme.httpAuthInfo, 'custom');
});
test('handles HTTP authentication security requirement', () {
final requirementJson = {
'basicAuth': [],
'digestAuth': [],
};
final requirement = ApiSecurityRequirement.fromJson(requirementJson);
expect(requirement.hasScheme('basicAuth'), true);
expect(requirement.hasScheme('digestAuth'), true);
expect(requirement.getScopesForScheme('basicAuth'), isEmpty);
expect(requirement.getScopesForScheme('digestAuth'), isEmpty);
});
});
group('Mixed Security Requirements', () {
test('handles multiple security schemes in one requirement', () {
final requirementJson = {
'oauth2': ['read', 'write'],
'apiKey': [],
'basicAuth': [],
};
final requirement = ApiSecurityRequirement.fromJson(requirementJson);
expect(requirement.schemeNames.length, 3);
expect(requirement.hasScheme('oauth2'), true);
expect(requirement.hasScheme('apiKey'), true);
expect(requirement.hasScheme('basicAuth'), true);
expect(requirement.getScopesForScheme('oauth2'), ['read', 'write']);
expect(requirement.getScopesForScheme('apiKey'), isEmpty);
expect(requirement.getScopesForScheme('basicAuth'), isEmpty);
});
test('handles empty security requirement (no authentication)', () {
final requirementJson = <String, dynamic>{};
final requirement = ApiSecurityRequirement.fromJson(requirementJson);
expect(requirement.isEmpty, true);
expect(requirement.schemeNames, isEmpty);
});
});
}

View File

@ -0,0 +1,442 @@
import 'package:swagger_generator_flutter/core/models.dart';
import 'package:swagger_generator_flutter/generators/optimized_retrofit_generator.dart';
import 'package:swagger_generator_flutter/generators/retrofit_api_generator.dart';
import 'package:test/test.dart';
void main() {
group('Simple Generator Tests', () {
late SwaggerDocument simpleDocument;
setUp(() {
simpleDocument = SwaggerDocument(
title: 'Simple Test API',
version: '1.0.0',
description: 'A simple test API',
servers: [
ApiServer(
url: 'https://api.example.com',
description: 'Test server',
),
],
components: ApiComponents(
schemas: {},
securitySchemes: {},
),
paths: {
'/users': ApiPath(
path: '/users',
method: HttpMethod.get,
summary: 'Get users',
description: 'Get all users',
operationId: 'getUsers',
tags: ['users'],
parameters: [],
responses: {
'200': ApiResponse(
code: '200',
description: 'Success',
content: {
'application/json': ApiMediaType(
schema: {'type': 'array'},
),
},
),
},
),
'/users/{id}': ApiPath(
path: '/users/{id}',
method: HttpMethod.get,
summary: 'Get user by ID',
description: 'Get a specific user',
operationId: 'getUserById',
tags: ['users'],
parameters: [
ApiParameter(
name: 'id',
location: ParameterLocation.path,
required: true,
type: PropertyType.integer,
description: 'User ID',
),
],
responses: {
'200': ApiResponse(
code: '200',
description: 'User found',
content: {
'application/json': ApiMediaType(
schema: {'type': 'object'},
),
},
),
},
),
},
models: {
'User': ApiModel(
name: 'User',
description: 'User model',
properties: {
'id': ApiProperty(
name: 'id',
type: PropertyType.integer,
description: 'User ID',
required: true,
),
'name': ApiProperty(
name: 'name',
type: PropertyType.string,
description: 'User name',
required: true,
),
},
required: ['id', 'name'],
),
},
controllers: {},
security: [],
);
});
group('RetrofitApiGenerator Basic Tests', () {
test('generates basic API structure', () {
final generator = RetrofitApiGenerator(
className: 'TestApi',
splitByTags: false,
);
final result = generator.generateFromDocument(simpleDocument);
expect(result, isNotEmpty);
expect(result, contains('Simple Test API'));
expect(result, contains('TestApi'));
});
test('generates imports correctly', () {
final generator = RetrofitApiGenerator();
final result = generator.generateFromDocument(simpleDocument);
expect(result, contains('import'));
expect(result, contains('dio'));
expect(result, contains('retrofit'));
});
test('generates path annotations', () {
final generator = RetrofitApiGenerator();
final result = generator.generateFromDocument(simpleDocument);
expect(result, contains('@GET'));
expect(result, contains('/users'));
expect(result, contains('/users/{id}'));
});
test('generates parameter annotations', () {
final generator = RetrofitApiGenerator();
final result = generator.generateFromDocument(simpleDocument);
expect(result, contains('@Path'));
expect(result, contains('id'));
});
test('handles split by tags', () {
final generator = RetrofitApiGenerator(
splitByTags: true,
);
final result = generator.generateFromDocument(simpleDocument);
expect(result, isNotEmpty);
// Should generate modular structure when split by tags
});
});
group('OptimizedRetrofitGenerator Tests', () {
test('generates optimized code structure', () {
final generator = OptimizedRetrofitGenerator(
className: 'OptimizedApi',
generateModularApis: false,
generateBaseResult: true,
);
final result = generator.generateFromDocument(simpleDocument);
expect(result, isNotEmpty);
expect(result, contains('Simple Test API'));
expect(result, contains('BaseResult'));
});
test('generates base result types', () {
final generator = OptimizedRetrofitGenerator(
generateBaseResult: true,
);
final result = generator.generateFromDocument(simpleDocument);
expect(result, contains('class BaseResult'));
expect(result, contains('final int code'));
expect(result, contains('final String message'));
expect(result, contains('bool get isSuccess'));
});
test('generates pagination types', () {
final generator = OptimizedRetrofitGenerator(
generatePagination: true,
);
final result = generator.generateFromDocument(simpleDocument);
expect(result, contains('BasePageParameter'));
expect(result, contains('BasePageResult'));
expect(result, contains('final int page'));
expect(result, contains('final int size'));
});
test('generates file upload types', () {
final generator = OptimizedRetrofitGenerator(
generateFileUpload: true,
);
final result = generator.generateFromDocument(simpleDocument);
expect(result, contains('FileUploadRequest'));
expect(result, contains('FileUploadResult'));
expect(result, contains('MultipartFile'));
});
test('generates utility classes', () {
final generator = OptimizedRetrofitGenerator(
generateFileUpload: true,
generatePagination: true,
);
final result = generator.generateFromDocument(simpleDocument);
expect(result, contains('class ApiUtils'));
expect(result, contains('createFileUpload'));
expect(result, contains('createPageParam'));
});
test('generates modular APIs when enabled', () {
final generator = OptimizedRetrofitGenerator(
generateModularApis: true,
);
final result = generator.generateFromDocument(simpleDocument);
expect(result, contains('UsersApi'));
expect(result, contains('class ApiService'));
expect(result, contains('late final UsersApi'));
});
test('generates single API when modular disabled', () {
final generator = OptimizedRetrofitGenerator(
generateModularApis: false,
className: 'SingleApi',
);
final result = generator.generateFromDocument(simpleDocument);
expect(result, contains('abstract class SingleApi'));
expect(result, isNot(contains('UsersApi')));
});
});
group('Code Quality Tests', () {
test('generated code has proper structure', () {
final generator = RetrofitApiGenerator();
final result = generator.generateFromDocument(simpleDocument);
// Check for basic Dart syntax
expect(result, isNot(contains(';;'))); // No double semicolons
expect(result, isNot(contains(',,'))); // No double commas
// Check for proper class declarations
final classCount = 'class '.allMatches(result).length;
final abstractClassCount = 'abstract class '.allMatches(result).length;
expect(classCount + abstractClassCount, greaterThan(0));
});
test('handles empty paths gracefully', () {
final 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 result = generator.generateFromDocument(emptyDocument);
expect(result, isNotEmpty);
expect(result, contains('Empty API'));
});
test('handles special characters in API names', () {
final 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',
method: HttpMethod.get,
summary: 'Special endpoint',
description: 'Test',
operationId: 'getSpecial',
tags: ['special'],
parameters: [],
responses: {
'200': ApiResponse(
code: '200',
description: 'Success',
),
},
),
},
models: {},
controllers: {},
security: [],
);
final generator = RetrofitApiGenerator();
expect(() => generator.generateFromDocument(specialDocument),
returnsNormally);
});
test('generates valid JSON annotations', () {
final generator = OptimizedRetrofitGenerator(
generateBaseResult: true,
);
final result = generator.generateFromDocument(simpleDocument);
expect(result, contains('@JsonSerializable'));
expect(result, contains('fromJson'));
expect(result, contains('toJson'));
});
test('handles nullable parameters correctly', () {
final documentWithOptionalParams = SwaggerDocument(
title: 'Test API',
version: '1.0.0',
description: 'Test',
servers: [],
components: ApiComponents(schemas: {}, securitySchemes: {}),
paths: {
'/search': ApiPath(
path: '/search',
method: HttpMethod.get,
summary: 'Search',
description: 'Search endpoint',
operationId: 'search',
tags: ['search'],
parameters: [
ApiParameter(
name: 'query',
location: ParameterLocation.query,
required: true,
type: PropertyType.string,
description: 'Search query',
),
ApiParameter(
name: 'page',
location: ParameterLocation.query,
required: false,
type: PropertyType.integer,
description: 'Page number',
),
],
responses: {
'200': ApiResponse(
code: '200',
description: 'Success',
),
},
),
},
models: {},
controllers: {},
security: [],
);
final generator = RetrofitApiGenerator();
final result =
generator.generateFromDocument(documentWithOptionalParams);
// Required parameters should not be nullable
expect(result, contains('String query'));
// Optional parameters should be nullable
expect(result, contains('int? page'));
});
});
group('Performance Tests', () {
test('handles medium-sized documents efficiently', () {
// Create a document with multiple paths
final paths = <String, ApiPath>{};
for (int i = 0; i < 50; i++) {
paths['/resource$i'] = ApiPath(
path: '/resource$i',
method: HttpMethod.get,
summary: 'Get resource $i',
description: 'Get resource $i',
operationId: 'getResource$i',
tags: ['resources'],
parameters: [],
responses: {
'200': ApiResponse(
code: '200',
description: 'Success',
),
},
);
}
final largeDocument = SwaggerDocument(
title: 'Large API',
version: '1.0.0',
description: 'Large API',
servers: [],
components: ApiComponents(schemas: {}, securitySchemes: {}),
paths: paths,
models: {},
controllers: {},
security: [],
);
final generator = RetrofitApiGenerator();
final stopwatch = Stopwatch()..start();
final result = generator.generateFromDocument(largeDocument);
stopwatch.stop();
expect(result, isNotEmpty);
expect(stopwatch.elapsedMilliseconds,
lessThan(5000)); // Should complete within 5 seconds
expect(result.length,
greaterThan(1000)); // Should generate substantial code
});
test('memory usage is reasonable', () {
final generator = RetrofitApiGenerator();
final result = generator.generateFromDocument(simpleDocument);
// Basic memory usage check - result should not be excessively large
expect(result.length,
lessThan(100000)); // Less than 100KB for simple document
});
});
});
}

View File

@ -1,6 +1,5 @@
import 'package:test/test.dart';
import 'package:swagger_generator_flutter/utils/string_utils.dart';
import 'package:test/test.dart';
void main() {
group('StringUtils', () {

280
validate.sh Executable file
View File

@ -0,0 +1,280 @@
#!/bin/bash
# Augment 代码生成规范验证脚本
# 用于验证生成的代码是否符合规范要求
set -e
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 打印带颜色的消息
print_info() {
echo -e "${BLUE} $1${NC}"
}
print_success() {
echo -e "${GREEN}$1${NC}"
}
print_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
print_error() {
echo -e "${RED}$1${NC}"
}
print_header() {
echo -e "${BLUE}🔍 Augment 代码生成规范验证${NC}"
echo "=================================================="
}
# 检查必要的工具
check_prerequisites() {
print_info "检查必要工具..."
if ! command -v dart &> /dev/null; then
print_error "Dart SDK 未安装或不在 PATH 中"
exit 1
fi
print_success "Dart SDK 已安装"
}
# 验证 swagger.json
validate_swagger() {
print_info "验证 swagger.json..."
if [ ! -f "swagger.json" ]; then
print_error "swagger.json 文件不存在"
return 1
fi
# 检查 JSON 格式
if ! jq empty swagger.json 2>/dev/null; then
if ! python3 -m json.tool swagger.json > /dev/null 2>&1; then
print_error "swagger.json 格式错误"
return 1
fi
fi
print_success "swagger.json 格式正确"
}
# 验证生成的文件结构
validate_file_structure() {
print_info "验证文件结构..."
if [ ! -d "generator" ]; then
print_error "generator 目录不存在"
return 1
fi
if [ ! -d "generator/api" ]; then
print_error "generator/api 目录不存在"
return 1
fi
if [ ! -d "generator/api_models" ]; then
print_error "generator/api_models 目录不存在"
return 1
fi
print_success "文件结构正确"
}
# 验证代码质量
validate_code_quality() {
print_info "验证代码质量..."
# 检查 generator 目录是否存在
if [ ! -d "generator" ]; then
print_warning "generator 目录不存在,跳过代码质量检查"
return 0
fi
# 运行 dart analyze
if dart analyze generator/ 2>/dev/null; then
print_success "静态分析通过"
else
print_warning "静态分析发现问题,请检查生成的代码"
fi
}
# 验证命名规范
validate_naming_conventions() {
print_info "验证命名规范..."
local errors=0
# 检查 API 文件命名
if [ -d "generator/api" ]; then
for file in generator/api/*.dart; do
if [ -f "$file" ]; then
filename=$(basename "$file")
if [[ ! "$filename" =~ ^[a-z][a-z0-9_]*_api\.dart$ ]] && [[ "$filename" != "api_client.dart" ]]; then
print_error "API 文件命名不规范: $filename"
errors=$((errors + 1))
fi
fi
done
fi
# 检查模型文件命名
if [ -d "generator/api_models" ]; then
for file in generator/api_models/*.dart; do
if [ -f "$file" ]; then
filename=$(basename "$file")
if [[ ! "$filename" =~ ^[a-z][a-z0-9_]*\.dart$ ]] && [[ "$filename" != "index.dart" ]]; then
print_error "模型文件命名不规范: $filename"
errors=$((errors + 1))
fi
fi
done
fi
if [ $errors -eq 0 ]; then
print_success "命名规范检查通过"
else
print_error "发现 $errors 个命名规范问题"
return 1
fi
}
# 验证类型一致性
validate_type_consistency() {
print_info "验证类型一致性..."
local generator_file="lib/generators/retrofit_api_generator.dart"
if [ ! -f "$generator_file" ]; then
print_warning "生成器文件不存在,跳过类型一致性检查"
return 0
fi
# 检查是否有硬编码的类型推断
local hardcoded_patterns=(
"TaskInfoResult"
"if.*contains.*login.*return"
"if.*contains.*task.*return"
"if.*tag.*contains.*return"
)
local errors=0
for pattern in "${hardcoded_patterns[@]}"; do
if grep -qiE "$pattern" "$generator_file"; then
print_error "发现硬编码类型推断: $pattern"
errors=$((errors + 1))
fi
done
if [ $errors -eq 0 ]; then
print_success "类型一致性检查通过"
else
print_error "发现 $errors 个类型一致性问题"
return 1
fi
}
# 运行完整验证
run_full_validation() {
print_header
local total_errors=0
check_prerequisites || total_errors=$((total_errors + 1))
validate_swagger || total_errors=$((total_errors + 1))
validate_file_structure || total_errors=$((total_errors + 1))
validate_code_quality || total_errors=$((total_errors + 1))
validate_naming_conventions || total_errors=$((total_errors + 1))
validate_type_consistency || total_errors=$((total_errors + 1))
echo ""
echo "=================================================="
if [ $total_errors -eq 0 ]; then
print_success "所有检查通过!代码符合 Augment 规范。"
exit 0
else
print_error "验证失败,发现 $total_errors 个问题。"
echo ""
print_info "请参考以下文档进行修复:"
echo " - 代码生成规范: AUGMENT_CODE_GENERATION_STANDARDS.md"
echo " - 快速参考指南: QUICK_REFERENCE.md"
echo " - 代码审查清单: CODE_REVIEW_CHECKLIST.md"
exit 1
fi
}
# 显示帮助信息
show_help() {
echo "Augment 代码生成规范验证脚本"
echo ""
echo "用法:"
echo " $0 [选项]"
echo ""
echo "选项:"
echo " all, -a, --all 运行所有验证检查(默认)"
echo " swagger, -s, --swagger 只验证 swagger.json"
echo " files, -f, --files 只验证文件结构"
echo " quality, -q, --quality 只验证代码质量"
echo " naming, -n, --naming 只验证命名规范"
echo " types, -t, --types 只验证类型一致性"
echo " help, -h, --help 显示此帮助信息"
echo ""
echo "示例:"
echo " $0 # 运行所有检查"
echo " $0 swagger # 只验证 swagger.json"
echo " $0 --quality # 只验证代码质量"
}
# 主函数
main() {
case "${1:-all}" in
all|-a|--all)
run_full_validation
;;
swagger|-s|--swagger)
print_header
check_prerequisites
validate_swagger
print_success "Swagger 验证完成"
;;
files|-f|--files)
print_header
validate_file_structure
print_success "文件结构验证完成"
;;
quality|-q|--quality)
print_header
validate_code_quality
print_success "代码质量验证完成"
;;
naming|-n|--naming)
print_header
validate_naming_conventions
print_success "命名规范验证完成"
;;
types|-t|--types)
print_header
validate_type_consistency
print_success "类型一致性验证完成"
;;
help|-h|--help)
show_help
;;
*)
print_error "未知选项: $1"
show_help
exit 1
;;
esac
}
# 执行主函数
main "$@"

239
validate_standards.dart Normal file
View File

@ -0,0 +1,239 @@
#!/usr/bin/env dart
/// Augment
///
import 'dart:convert';
import 'dart:io';
void main(List<String> args) async {
print('🔍 Augment 代码生成规范验证');
print('=' * 50);
final validator = StandardsValidator();
try {
await validator.validateAll();
print('\n✅ 所有检查通过!代码符合 Augment 规范。');
} catch (e) {
print('\n❌ 验证失败:$e');
exit(1);
}
}
class StandardsValidator {
final List<String> errors = [];
final List<String> warnings = [];
///
Future<void> validateAll() async {
print('📋 开始验证...\n');
await _validateSwaggerJson();
await _validateGeneratedFiles();
await _validateCodeQuality();
await _validateNamingConventions();
await _validateTypeConsistency();
_printResults();
if (errors.isNotEmpty) {
throw Exception('发现 ${errors.length} 个错误');
}
}
/// swagger.json
Future<void> _validateSwaggerJson() async {
print('🔍 验证 swagger.json...');
final swaggerFile = File('swagger.json');
if (!swaggerFile.existsSync()) {
errors.add('swagger.json 文件不存在');
return;
}
try {
final content = await swaggerFile.readAsString();
final swagger = jsonDecode(content) as Map<String, dynamic>;
// OpenAPI
final openapi = swagger['openapi'] as String?;
if (openapi == null || !openapi.startsWith('3.0')) {
errors.add('OpenAPI 版本必须是 3.0.x当前$openapi');
}
//
if (!swagger.containsKey('paths')) {
errors.add('swagger.json 缺少 paths 定义');
}
if (!swagger.containsKey('components')) {
warnings.add('swagger.json 缺少 components 定义');
}
// schemas
final components = swagger['components'] as Map<String, dynamic>?;
if (components != null) {
final schemas = components['schemas'] as Map<String, dynamic>?;
if (schemas == null || schemas.isEmpty) {
warnings.add('components/schemas 为空,可能影响类型生成');
}
}
print(' ✅ swagger.json 格式正确');
} catch (e) {
errors.add('swagger.json 格式错误:$e');
}
}
///
Future<void> _validateGeneratedFiles() async {
print('🔍 验证生成的文件...');
final generatorDir = Directory('generator');
if (!generatorDir.existsSync()) {
errors.add('generator 目录不存在');
return;
}
//
final apiDir = Directory('generator/api');
final modelsDir = Directory('generator/api_models');
if (!apiDir.existsSync()) {
errors.add('generator/api 目录不存在');
}
if (!modelsDir.existsSync()) {
errors.add('generator/api_models 目录不存在');
}
// index.dart
final indexFile = File('generator/api_models/index.dart');
if (!indexFile.existsSync()) {
warnings.add('generator/api_models/index.dart 文件不存在');
}
print(' ✅ 文件结构正确');
}
///
Future<void> _validateCodeQuality() async {
print('🔍 验证代码质量...');
// dart analyze
final analyzeResult = await Process.run('dart', ['analyze', 'generator/']);
if (analyzeResult.exitCode != 0) {
final output = analyzeResult.stdout.toString();
final errorOutput = analyzeResult.stderr.toString();
if (output.contains('error') || errorOutput.contains('error')) {
errors.add('dart analyze 发现错误:\n$output\n$errorOutput');
} else if (output.contains('warning') ||
errorOutput.contains('warning')) {
warnings.add('dart analyze 发现警告:\n$output\n$errorOutput');
}
} else {
print(' ✅ 静态分析通过');
}
}
///
Future<void> _validateNamingConventions() async {
print('🔍 验证命名规范...');
final apiDir = Directory('generator/api');
if (apiDir.existsSync()) {
await for (final file in apiDir.list()) {
if (file is File && file.path.endsWith('.dart')) {
final fileName = file.path.split('/').last;
// API _api.dart
if (!fileName.endsWith('_api.dart')) {
errors.add('API 文件命名不规范:$fileName(应该以 _api.dart 结尾)');
}
// snake_case
final baseName = fileName.replaceAll('_api.dart', '');
if (!_isSnakeCase(baseName)) {
errors.add('API 文件名不是 snake_case$fileName');
}
}
}
}
final modelsDir = Directory('generator/api_models');
if (modelsDir.existsSync()) {
await for (final file in modelsDir.list()) {
if (file is File &&
file.path.endsWith('.dart') &&
!file.path.endsWith('index.dart')) {
final fileName = file.path.split('/').last;
final baseName = fileName.replaceAll('.dart', '');
// snake_case
if (!_isSnakeCase(baseName)) {
errors.add('模型文件名不是 snake_case$fileName');
}
}
}
}
print(' ✅ 命名规范检查完成');
}
///
Future<void> _validateTypeConsistency() async {
print('🔍 验证类型一致性...');
//
final generatorFile = File('lib/generators/retrofit_api_generator.dart');
if (generatorFile.existsSync()) {
final content = await generatorFile.readAsString();
//
final hardcodedPatterns = [
'TaskInfoResult',
'if.*contains.*login.*return',
'if.*contains.*task.*return',
'if.*tag.*contains.*return',
];
for (final pattern in hardcodedPatterns) {
final regex = RegExp(pattern, caseSensitive: false);
if (regex.hasMatch(content)) {
errors.add('发现硬编码类型推断:$pattern');
}
}
}
print(' ✅ 类型一致性检查完成');
}
/// snake_case
bool _isSnakeCase(String name) {
return RegExp(r'^[a-z][a-z0-9_]*$').hasMatch(name);
}
///
void _printResults() {
print('\n📊 验证结果:');
print(' 错误:${errors.length}');
print(' 警告:${warnings.length}');
if (errors.isNotEmpty) {
print('\n❌ 错误列表:');
for (int i = 0; i < errors.length; i++) {
print(' ${i + 1}. ${errors[i]}');
}
}
if (warnings.isNotEmpty) {
print('\n⚠️ 警告列表:');
for (int i = 0; i < warnings.length; i++) {
print(' ${i + 1}. ${warnings[i]}');
}
}
}
}