feat: 恢复代码
This commit is contained in:
parent
547a6c7f16
commit
a12bf7e618
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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) 下授权。
|
||||
|
||||
## 🙏 致谢
|
||||
|
||||
感谢所有贡献者的努力!您的贡献让这个项目变得更好。
|
||||
|
||||
### 贡献者列表
|
||||
|
||||
<!-- 这里会自动生成贡献者列表 -->
|
||||
|
||||
---
|
||||
|
||||
再次感谢您的贡献!🎉
|
||||
|
|
@ -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
247
README.md
|
|
@ -2,32 +2,207 @@
|
|||
|
||||
基于 Swagger/OpenAPI 的 Dart/Flutter API/模型代码生成工具。
|
||||
|
||||
## 功能简介
|
||||
- 根据 swagger.json 自动生成 Dart API 接口、模型、枚举等
|
||||
- 支持 Retrofit、json_serializable 等主流生态
|
||||
- 支持自定义生成规则和命名风格
|
||||
[](https://dart.dev/)
|
||||
[](https://flutter.dev/)
|
||||
[](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)
|
||||
|
||||
### 脚本命令说明
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
```
|
||||
|
|
@ -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}');
|
||||
}
|
||||
|
|
@ -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();
|
||||
/// ```
|
||||
|
|
@ -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 详情');
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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" } } } }
|
||||
|
||||
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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, // 默认启用拆分模式
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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"',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
1943
lib/core/models.dart
1943
lib/core/models.dart
File diff suppressed because it is too large
Load Diff
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
// 这里应该从文件或数据库读取缓存数据
|
||||
// 实际实现会更复杂
|
||||
}
|
||||
}
|
||||
|
|
@ -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 '/'; // 默认相对路径
|
||||
}
|
||||
|
||||
/// 获取参数位置名称
|
||||
|
|
|
|||
|
|
@ -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\';');
|
||||
|
|
|
|||
|
|
@ -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('}');
|
||||
|
|
|
|||
|
|
@ -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'^_'), '');
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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扩展名
|
||||
|
|
|
|||
|
|
@ -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('');
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
pubspec.lock
28
pubspec.lock
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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é', '美食']);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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}');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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%的任务成功
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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, '解析失败的模型');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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', () {
|
||||
|
|
|
|||
|
|
@ -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 "$@"
|
||||
|
|
@ -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]}');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue