Merge branch 'release/3.1.1'

This commit is contained in:
Max 2025-11-24 13:43:40 +08:00
commit 63a3b16ed1
194 changed files with 33166 additions and 43089 deletions

29
.editorconfig Normal file
View File

@ -0,0 +1,29 @@
# 这是一个顶级配置文件,停止向父目录查找
root = true
# === 全局通用设置 (所有文件) ===
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
# === Dart 文件专用设置 ===
[*.dart]
# Very Good Analysis 默认开启了 lines_longer_than_80_chars
# 这里设置为 80确保 IDE 的标尺线 (Ruler) 与 Linter 报错一致
max_line_length = 80
# 虽然 [*] 设置了 indent_size但对 Dart 再次显式声明是好习惯
indent_size = 2
# === YAML/JSON 文件 (配置文件) ===
[*.{yaml,yml,json}]
indent_size = 2
# === Markdown 文件 (文档) ===
[*.md]
# Markdown 中行尾的双空格代表换行,所以不能自动去除尾部空格
trim_trailing_whitespace = false
max_line_length = 0 # 文档通常自动换行,不强制限制行宽

View File

@ -1,458 +0,0 @@
# 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

View File

@ -2,6 +2,72 @@
All notable changes to this project will be documented in this file.
## [3.1.0] - 2025-11-24
### 🎉 新特性
#### 枚举键名映射支持
- ✅ **支持 OpenAPI 扩展字段**:通过 `x-enum-varnames``x-enum-descriptions` 生成有意义的枚举键名和注释
- ✅ **支持配置文件映射**:在 `generator_config.yaml` 中配置枚举键名映射,无需后端支持
- ✅ **三级优先级系统**:配置文件映射 > Swagger 扩展字段 > 智能生成
- ✅ **完整类型支持**:支持整数枚举和字符串枚举
- ✅ **部分映射支持**:可以只配置部分枚举值,未配置的使用智能生成
#### 配置文件增强
- ✅ 新增 `generation.models.enum_key_mappings` 配置项
- ✅ 更新 `generator_config.template.yaml`,添加详细的枚举映射示例和说明
- ✅ 新增 `EnumKeyMapping` 数据类,支持枚举键名和描述配置
### 📝 文档更新
- 新增 [**枚举快速参考**](./ENUM_QUICK_REFERENCE.md) - 三种生成方式对比和快速上手
- 新增 [**枚举使用指南**](./ENUM_KEY_NAMES_USAGE.md) - 详细的使用说明、后端实现示例和常见问题
- 新增 [**枚举实现总结**](./ENUM_CONFIG_MAPPING_SUMMARY.md) - 功能实现细节和测试验证
- 新增 [**枚举配置示例**](./example/enum_config_mapping_example.yaml) - 完整的配置文件示例
- 更新 README.md添加枚举键名映射功能说明
### 🔧 技术细节
- 修改 `lib/core/config_repository.dart`:添加 `enumKeyMappings` 解析逻辑
- 修改 `lib/core/config.dart`:暴露枚举映射配置
- 修改 `lib/pipeline/generate/impl/model/model_content_builders.dart`:实现三级优先级枚举生成
- 新增 `EnumKeyMapping` 类:封装枚举键名和描述配置
### 💡 使用场景
1. 后端支持 OpenAPI 扩展字段 → 使用 `x-enum-varnames`
2. 后端不支持扩展字段 → 使用配置文件映射
3. 需要覆盖 Swagger 定义 → 使用配置文件映射
4. 快速原型开发 → 使用智能生成(默认)
### 📚 相关资源
- [OpenAPI 扩展字段规范](https://swagger.io/docs/specification/openapi-extensions/)
- [示例 Swagger 文档](./example/swagger_enum_example.json)
- [配置文件示例](./example/enum_config_mapping_example.yaml)
---
## [3.0.0] - 2025-11-21
### Breaking changes
- Removed OptimizedRetrofitGenerator and its public export. RetrofitApiGenerator is now the sole supported generator.
### Migration
- Replace imports/usages of OptimizedRetrofitGenerator with RetrofitApiGenerator. Example:
- Before:
import 'package:swagger_generator_flutter/generators/optimized_retrofit_generator.dart';
final generator = OptimizedRetrofitGenerator(className: 'ApiService');
- After:
import 'package:swagger_generator_flutter/generators/retrofit_api_generator.dart';
final generator = RetrofitApiGenerator(
className: 'ApiService',
useRetrofit: true,
useDio: true,
splitByTags: true,
versionedApi: true,
);
### Docs
- Updated README and docs to reference RetrofitApiGenerator only.
## [2.1.1] - 2025-11-05
### 🎉 新特性

View File

@ -1,242 +0,0 @@
# 代码审核报告 - 版本变动审核
**审核日期**: 2025-11-05
**审核范围**: 本次版本的所有变更
**审核重点**: 文件头配置功能、文件跳过功能
---
## 📋 本次版本主要变更
### 1. 文件头配置功能 ✅
- **新增**: `ConfigLoader.getFileHeaderTemplate()` - 读取文件头模板
- **新增**: `ConfigLoader.getGeneratorName()` - 读取生成器名称
- **新增**: `ConfigLoader.getAuthor()` - 读取作者信息
- **新增**: `ConfigLoader.getCopyright()` - 读取版权信息
- **更新**: `StringUtils.generateFileHeader()` - 支持配置模板和变量替换
- **更新**: `BaseGenerator.generateFileHeader()` - 传递文件名参数
### 2. 文件跳过功能 ✅
- **新增**: `ConfigLoader.getIgnoredDirectories()` - 读取跳过的目录列表
- **新增**: `ConfigLoader.getIgnoredFiles()` - 读取跳过的文件名列表
- **新增**: `ConfigLoader.shouldSkipFile()` - 检查文件是否应该跳过
- **更新**: `GenerateCommand.execute()` - 在所有文件生成点添加跳过检查
### 3. 配置文件更新 ✅
- **更新**: `generator_config.yaml` - 添加模板配置示例
- **更新**: `generator_config.template.yaml` - 添加模板配置部分
- **更新**: `example/as_dev_dependency/generator_config.yaml` - 添加完整模板配置
---
## ✅ 代码质量检查
### 1. 代码逻辑检查
#### ✅ 文件头配置逻辑
- **状态**: ✅ 正确
- **说明**:
- 模板变量替换逻辑正确
- 支持默认值回退机制
- 当配置不存在时使用默认模板
#### ✅ 文件跳过逻辑
- **状态**: ✅ 正确
- **说明**:
- 目录级别跳过:路径标准化处理正确
- 文件名级别跳过:支持精确匹配和通配符匹配
- 边界情况处理:空目录、空文件名都有处理
#### ✅ Swagger URL 处理
- **状态**: ✅ 正确
- **说明**:
- 支持多个 Swagger URL
- 文件头使用第一个 URL合理
- URL 合并逻辑正确
### 2. 代码完整性检查
#### ✅ 所有文件生成点都已添加跳过检查
- 模型文件生成 ✅
- API 文件生成 ✅
- 版本目录生成 ✅
- 主 API 文件生成 ✅
- 参数实体类生成 ✅
- 文档文件生成 ✅
#### ✅ 配置文件完整性
- 模板配置项完整 ✅
- 注释说明完整 ✅
- 示例配置正确 ✅
---
## ⚠️ 发现的问题
### 1. 未使用的方法
**位置**: `lib/generators/retrofit_api_generator.dart:1037`
**问题**: `_getRequiredModelImports()` 方法未被引用
**影响**: 低(不影响功能)
**建议**:
```dart
// 可以考虑删除或标记为 @deprecated
// 或者保留以备将来使用
```
**处理**: 暂不处理,保留以备将来使用
---
## 🔍 潜在问题分析
### 1. 文件头模板变量替换
**潜在问题**: 当模板包含多个相同的变量时,`replaceAll` 会替换所有出现
**影响**: 低(通常模板中每个变量只出现一次)
**验证**: ✅ 代码逻辑正确,`replaceAll` 是正确的选择
### 2. 文件跳过路径匹配
**潜在问题**: 路径匹配可能在某些边界情况下不够精确
**影响**: 低(已处理主要边界情况)
**验证**: ✅ 代码包含边界检查:
- 空目录名检查
- 路径标准化(统一使用 `/`
- 目录边界检查
### 3. 多版本文件头 URL
**潜在问题**: 当有多个 Swagger URL 时,文件头只显示第一个 URL
**影响**: 低(这是合理的设计选择)
**说明**: 这是有意为之的设计,因为:
- 文件头通常只需要显示一个来源
- 多个 URL 可能导致文件头过长
- 如果需要,可以通过配置模板自定义
---
## 📊 代码质量指标
### 代码覆盖率
- ✅ 所有新增功能都有对应的配置选项
- ✅ 所有配置都有默认值处理
- ✅ 所有边界情况都有处理
### 错误处理
- ✅ 配置文件不存在时使用默认值
- ✅ 配置解析失败时显示警告
- ✅ 文件跳过检查失败时继续执行(不影响主流程)
### 代码一致性
- ✅ 命名规范一致
- ✅ 代码风格一致
- ✅ 注释风格一致
---
## 🎯 功能验证
### 1. 文件头配置功能 ✅
**测试场景**:
- ✅ 配置存在时使用配置的模板
- ✅ 配置不存在时使用默认模板
- ✅ 模板变量正确替换
- ✅ 生成器信息从配置读取
**验证结果**: ✅ 所有场景通过
### 2. 文件跳过功能 ✅
**测试场景**:
- ✅ 目录级别跳过
- ✅ 文件名级别跳过(精确匹配)
- ✅ 文件名级别跳过(通配符匹配)
- ✅ 组合跳过(目录 + 文件名)
**验证结果**: ✅ 所有场景通过
---
## 📝 建议改进
### 1. 文档完善 ✅
- ✅ 已添加 `FILE_HEADER_CONFIGURATION.md`
- ✅ 配置文件包含详细注释
- ✅ 示例配置完整
### 2. 代码优化建议
#### 建议 1: 考虑添加单元测试
- **优先级**: 中
- **说明**: 为新功能添加单元测试可以提高代码质量
#### 建议 2: 优化文件跳过性能
- **优先级**: 低
- **说明**: 当前实现已经足够高效,但如果文件数量很大,可以考虑缓存配置
### 3. 功能增强建议
#### 建议 1: 支持更多通配符模式
- **优先级**: 低
- **说明**: 当前支持 `*prefix`, `suffix*`, `*pattern*`,已满足大部分需求
#### 建议 2: 支持正则表达式匹配
- **优先级**: 低
- **说明**: 如果需要更复杂的匹配模式,可以考虑支持正则表达式
---
## ✅ 总结
### 代码质量
- ✅ **优秀**: 代码结构清晰,逻辑正确
- ✅ **完整**: 所有功能都有完整的实现
- ✅ **健壮**: 错误处理和边界情况处理完善
### 功能完整性
- ✅ **文件头配置**: 完全实现,功能完整
- ✅ **文件跳过**: 完全实现,功能完整
- ✅ **配置支持**: 配置项完整,注释详细
### 潜在风险
- ⚠️ **低风险**: 只有一个未使用的方法,不影响功能
- ✅ **无高风险问题**: 未发现高风险问题
### 建议
1. ✅ **可以发布**: 代码质量良好,可以发布
2. ✅ **文档完善**: 文档已完善,用户可以理解如何使用
3. ✅ **向后兼容**: 新功能不影响现有功能,向后兼容
---
## 📋 审核结论
**审核状态**: ✅ **通过**
**总体评价**:
- 代码质量优秀
- 功能实现完整
- 错误处理完善
- 文档齐全
**建议**: 可以发布此版本
**备注**:
- 发现一个未使用的方法(`_getRequiredModelImports`),但不影响功能,可以保留以备将来使用
- 所有核心功能都已正确实现并通过验证
---
**审核人**: AI Assistant
**审核日期**: 2025-11-05

View File

@ -1,369 +0,0 @@
# 贡献指南
感谢您对 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) 下授权。
## 🙏 致谢
感谢所有贡献者的努力!您的贡献让这个项目变得更好。
### 贡献者列表
<!-- 这里会自动生成贡献者列表 -->
---
再次感谢您的贡献!🎉

159
DEPENDENCY_UPDATE.md Normal file
View File

@ -0,0 +1,159 @@
# 依赖包版本更新说明
## 更新日期
2025-11-21
## 更新内容
### 主项目 (pubspec.yaml)
#### 更新前
```yaml
dependencies:
json_annotation: ^4.8.1
freezed_annotation: ^2.4.1
dev_dependencies:
build_runner: ^2.4.7
json_serializable: ^6.7.1
retrofit_generator: ^8.0.0
freezed: ^2.4.7
```
#### 更新后
```yaml
dependencies:
json_annotation: ^4.9.0
freezed_annotation: ^3.1.0
dev_dependencies:
build_runner: ^2.10.4
json_serializable: ^6.11.2
retrofit_generator: ^10.2.0
freezed: ^3.2.3
```
### Example 项目 (example/pubspec.yaml)
#### 更新前
```yaml
dependencies:
json_annotation: ^4.9.0
freezed_annotation: ^2.4.1
dev_dependencies:
build_runner: ^2.4.7
retrofit_generator: ^9.0.0
json_serializable: ^6.7.1
freezed: ^2.4.7
```
#### 更新后
```yaml
dependencies:
json_annotation: ^4.9.0
freezed_annotation: ^3.1.0
dev_dependencies:
build_runner: ^2.10.4
retrofit_generator: ^10.2.0
json_serializable: ^6.11.2
freezed: ^3.2.3
```
## 版本说明
### build_runner
- **旧版本**: 2.4.7
- **新版本**: 2.10.4
- **发布时间**: 2天前 (2025-11-19)
- **说明**: 最新稳定版本,修复了多个已知问题
### json_serializable
- **旧版本**: 6.7.1
- **新版本**: 6.11.2
- **说明**: 使用 6.11.2 而非 6.11.3,因为 6.11.3 与 freezed 3.2.3 存在 analyzer 版本冲突
### retrofit_generator
- **旧版本**: 8.0.0 / 9.0.0
- **新版本**: 10.2.0
- **发布时间**: 33小时前 (2025-11-20)
- **说明**: 最新版本,支持更多特性
### freezed
- **旧版本**: 2.4.7
- **新版本**: 3.2.3
- **发布时间**: 2个月前 (2025-09-10)
- **重要变更**:
- 需要 freezed_annotation 3.1.0+
- 支持 Dart 3 的原生模式匹配
- 建议使用 `switch` 表达式替代 `when`/`map` 方法
### freezed_annotation
- **旧版本**: 2.4.1 / 2.4.4
- **新版本**: 3.1.0
- **说明**: 必须升级到 3.x 以配合 freezed 3.x
### json_annotation
- **旧版本**: 4.8.1
- **新版本**: 4.9.0
- **说明**: 保持 4.x 版本以确保兼容性
## 版本兼容性矩阵
| 包名 | 版本 | analyzer 依赖 | 兼容性 |
|------|------|--------------|--------|
| freezed 3.2.3 | ✅ | 7.5.9 - 9.0.0 | ✅ |
| json_serializable 6.11.2 | ✅ | 8.x | ✅ |
| json_serializable 6.11.3 | ❌ | 9.0.0+ | ❌ 与 freezed 冲突 |
| build_runner 2.10.4 | ✅ | - | ✅ |
| retrofit_generator 10.2.0 | ✅ | - | ✅ |
## 解决的问题
1. **build_runner 缓存损坏**: 通过升级到最新版本解决
2. **版本冲突**:
- freezed 3.x 需要 freezed_annotation 3.x
- freezed 3.2.3 与 json_serializable 6.11.3 存在 analyzer 版本冲突
- 使用 json_serializable 6.11.2 解决冲突
## 清理步骤
如果遇到问题,执行以下清理步骤:
```bash
# 1. 清理缓存
rm -rf .dart_tool
rm -rf pubspec.lock
# 2. 重新获取依赖
flutter pub get
# 3. 清理构建文件
flutter clean
# 4. 重新运行 build_runner
flutter pub run build_runner build --delete-conflicting-outputs
```
## 注意事项
1. **Freezed 3.x 迁移**:
- 建议使用 Dart 3 的原生 `switch` 表达式
- 旧的 `when`/`map` 方法仍然可用,但已标记为遗留功能
2. **analyzer 版本**:
- 当前配置使用 analyzer 8.x
- 避免使用需要 analyzer 9.x 的包版本
3. **pub.dev 镜像**:
- 如果使用国内镜像 (pub.flutter-io.cn),可能需要等待同步
- 建议使用官方源或清华镜像
## 参考链接
- [build_runner](https://pub.dev/packages/build_runner)
- [json_serializable](https://pub.dev/packages/json_serializable)
- [retrofit_generator](https://pub.dev/packages/retrofit_generator)
- [freezed](https://pub.dev/packages/freezed)

View File

@ -1,293 +0,0 @@
# 📦 Dev Dependency 功能完成总结
本文档总结了将 `swagger_generator_flutter` 配置为 dev_dependencies 的所有改动。
## ✅ 完成的工作
### 1. 核心配置文件修改
#### `pubspec.yaml`
- ✅ 添加 `executables` 配置
- ✅ 映射命令:`swagger_generator: main`
- ✅ 更新描述信息
- ✅ 版本升级到 2.1.1
```yaml
executables:
swagger_generator: main
```
这使得其他项目可以通过以下命令使用:
```bash
dart run swagger_generator_flutter generate --all
```
### 2. 新增文档文件
#### `USAGE_AS_DEV_DEPENDENCY.md`
完整的使用指南,包含:
- 📦 安装步骤
- 📝 配置文件说明
- 🚀 使用方法3种方式
- 📋 命令选项说明
- 📂 生成的文件结构
- 🔧 必需的项目依赖
- 🔄 完整工作流程
- 🎯 CI/CD 集成示例
- 🐛 常见问题排除
- 💡 最佳实践
#### `generator_config.template.yaml`
配置文件模板,用户可以:
- 复制到自己的项目
- 根据项目需求修改
- 包含详细的配置说明和注释
### 3. 示例项目(`example/as_dev_dependency/`
创建了完整的示例应用,包含以下文件:
#### 项目配置文件
- ✅ `pubspec.yaml` - 依赖配置(使用本地路径引用)
- ✅ `generator_config.yaml` - 生成器配置(使用 Petstore API
- ✅ `analysis_options.yaml` - 代码分析配置
- ✅ `.gitignore` - Git 忽略配置
#### 基础代码文件
- ✅ `lib/common/api_response.dart` - 通用 API 响应包装类
- ✅ `lib/common/paged_response.dart` - 分页响应包装类
- ✅ `lib/main.dart` - Flutter 应用入口(带使用说明)
#### 自动化脚本
- ✅ `generate_api.sh` - macOS/Linux 生成脚本
- ✅ `generate_api.bat` - Windows 生成脚本
- ✅ `Makefile` - Make 命令配置12+ 命令)
#### 文档
- ✅ `README.md` - 完整的项目说明
- ✅ `QUICK_START.md` - 5分钟快速开始指南
### 4. 主项目文档更新
#### `README.md`
- ✅ 新增 "📦 作为 dev_dependencies 使用" 章节
- ✅ 提供快速开始步骤
- ✅ 添加文档链接
#### `CHANGELOG.md`
- ✅ 新增 [2.1.1] 版本说明
- ✅ 详细记录所有新特性
- ✅ 说明文档更新
## 📂 文件结构总览
```
swagger_generator_flutter/
├── pubspec.yaml ← 更新:添加 executables
├── CHANGELOG.md ← 更新:新增 2.1.1 版本
├── README.md ← 更新:新增使用章节
├── USAGE_AS_DEV_DEPENDENCY.md ← 新增:完整使用指南
├── generator_config.template.yaml ← 新增:配置模板
├── DEV_DEPENDENCY_SETUP_SUMMARY.md ← 新增:本文件
└── example/
└── as_dev_dependency/ ← 新增:完整示例项目
├── pubspec.yaml
├── generator_config.yaml
├── analysis_options.yaml
├── .gitignore
├── Makefile
├── generate_api.sh ← 可执行
├── generate_api.bat
├── README.md
├── QUICK_START.md
└── lib/
├── common/
│ ├── api_response.dart
│ └── paged_response.dart
└── main.dart
```
## 🎯 使用方式总结
### 在其他项目中使用
#### 1. 添加依赖
```yaml
dev_dependencies:
swagger_generator_flutter:
git:
url: https://github.com/your-org/swagger_generator_flutter.git
ref: develop
```
#### 2. 创建配置
```bash
cp node_modules/swagger_generator_flutter/generator_config.template.yaml \
generator_config.yaml
```
#### 3. 生成代码
```bash
dart run swagger_generator_flutter generate --all
dart run build_runner build --delete-conflicting-outputs
```
### 测试示例项目
```bash
cd example/as_dev_dependency
flutter pub get
./generate_api.sh # 或 make build
flutter run
```
## 📋 功能清单
### ✅ 已实现功能
- [x] 作为 dev_dependencies 使用
- [x] 通过 `dart run` 命令执行
- [x] 从项目根目录读取配置文件
- [x] 完整的使用文档
- [x] 配置文件模板
- [x] 完整的示例项目
- [x] 自动化生成脚本Shell/Batch
- [x] Makefile 命令支持
- [x] CI/CD 集成示例
- [x] 故障排除指南
- [x] 最佳实践建议
### 🎓 文档完整性
- [x] 快速开始指南QUICK_START.md
- [x] 完整使用指南USAGE_AS_DEV_DEPENDENCY.md
- [x] 示例项目文档example/as_dev_dependency/README.md
- [x] 配置文件模板generator_config.template.yaml
- [x] 主 README 更新
- [x] CHANGELOG 更新
### 🛠️ 辅助工具
- [x] Shell 脚本macOS/Linux
- [x] Batch 脚本Windows
- [x] Makefile12+ 命令)
- [x] .gitignore 配置
- [x] analysis_options.yaml
## 🚀 下一步建议
### 发布前检查
1. **测试示例项目**
```bash
cd example/as_dev_dependency
make build
flutter run
```
2. **验证配置文件**
- 确保 `generator_config.template.yaml` 包含所有必要配置
- 验证路径和导入是否正确
3. **文档审查**
- 检查所有链接是否有效
- 确保示例代码可以运行
- 验证命令是否正确
4. **版本发布**
- 更新版本号到 2.1.1
- 创建 Git tag
- 发布到仓库
### 可选增强功能
- [ ] 添加更多示例(不同的 Swagger API
- [ ] 创建视频教程
- [ ] 添加单元测试
- [ ] 创建 GitHub Actions 工作流模板
- [ ] 添加性能基准测试
- [ ] 创建 VSCode 插件/扩展
## 💡 使用技巧
### 1. 本地开发时使用相对路径
```yaml
dev_dependencies:
swagger_generator_flutter:
path: ../swagger_generator_flutter
```
### 2. 生产环境使用 Git 引用
```yaml
dev_dependencies:
swagger_generator_flutter:
git:
url: https://github.com/your-org/swagger_generator_flutter.git
ref: v2.1.1 # 使用具体版本标签
```
### 3. 使用 Makefile 简化命令
```bash
make build # 代替长命令
make watch # 监听模式
make clean # 清理
```
### 4. CI/CD 集成
```yaml
# .github/workflows/build.yml
- name: Generate API
run: dart run swagger_generator_flutter generate --all
```
## 📊 对比:改动前后
### 改动前
- ❌ 只能作为独立项目使用
- ❌ 需要复制整个项目到工作区
- ❌ 配置不灵活
- ❌ 缺少使用文档
### 改动后
- ✅ 可以作为 dev_dependencies 集成
- ✅ 通过包管理器安装
- ✅ 灵活的配置系统
- ✅ 完整的文档和示例
- ✅ 自动化工具支持
- ✅ CI/CD 友好
## 🎉 总结
现在 `swagger_generator_flutter` 已经完全支持作为 dev_dependencies 使用!
**主要优势:**
1. 🚀 **易于集成** - 一行依赖配置即可使用
2. 📝 **配置灵活** - 通过 YAML 文件自定义所有选项
3. 🔄 **工作流友好** - 提供脚本和 Makefile 支持
4. 📚 **文档完善** - 从快速开始到高级用法都有详细说明
5. 🎯 **实战示例** - 完整的示例项目可以直接运行
**用户体验:**
- 从添加依赖到生成代码只需 3 步
- 5 分钟即可开始使用
- 完善的错误处理和故障排除指南
- 支持多种使用方式命令行、脚本、Makefile
## 📞 支持
如有问题,请参考:
1. [快速开始指南](example/as_dev_dependency/QUICK_START.md)
2. [完整使用指南](USAGE_AS_DEV_DEPENDENCY.md)
3. [示例项目](example/as_dev_dependency/)
4. [主文档](README.md)
---
**版本**: 2.1.1
**更新日期**: 2025-11-05
**状态**: ✅ 完成

View File

@ -0,0 +1,408 @@
# 枚举配置文件映射功能实现总结
**实现日期**: 2025-11-24
**功能状态**: ✅ 已完成并测试
## 概述
成功实现了**阶段 2配置文件映射**功能,允许用户通过 `generator_config.yaml` 配置文件为枚举值定义有意义的键名和描述,即使后端不支持 OpenAPI 扩展字段。
## 核心特性
### 1. 三级优先级系统
```
配置文件映射 > x-enum-varnames > 智能生成
```
- **配置文件映射**(最高优先级):用户在 `generator_config.yaml` 中显式配置
- **x-enum-varnames**(中优先级):后端在 Swagger 文档中提供的扩展字段
- **智能生成**(最低优先级):自动生成 `valueN` 格式
### 2. 灵活的配置方式
支持在 `generator_config.yaml` 中配置:
```yaml
generation:
models:
enum_key_mappings:
SysTaskTypeEnums:
- value: 1
name: SPOT_CHECK
description: 抽查
- value: 2
name: CULTURAL
description: 文创建设
```
### 3. 完整的类型支持
- ✅ 整数枚举 (`integer`, `number`)
- ✅ 字符串枚举 (`string`)
- ✅ 枚举键名和描述
- ✅ 部分映射(可以只配置部分枚举值)
- ✅ 自动添加 `UNKNOWN` 枚举值(整数类型用 `-9999`,字符串类型用 `'UNKNOWN'`
- ✅ 容错处理(未知值返回 `UNKNOWN` 而不是抛异常)
## 实现细节
### 修改的文件
#### 1. `lib/core/config_repository.dart`
新增 `EnumKeyMapping` 数据类和解析逻辑:
```dart
/// 枚举键名映射
class EnumKeyMapping {
const EnumKeyMapping({
required this.name,
this.description,
});
final String name;
final String? description;
}
class ConfigRepository {
/// 获取枚举键名映射配置
Map<String, Map<dynamic, EnumKeyMapping>>? get enumKeyMappings {
// 从配置文件解析枚举映射
// 返回格式: { "EnumName": { value: EnumKeyMapping } }
}
}
```
#### 2. `lib/core/config.dart`
暴露枚举映射配置:
```dart
class SwaggerConfig {
/// 获取枚举键名映射配置(从配置文件读取)
static Map<String, Map<dynamic, EnumKeyMapping>>? get enumKeyMappings =>
ConfigRepository.loadSync().enumKeyMappings;
}
```
#### 3. `lib/pipeline/generate/impl/model/model_content_builders.dart`
修改枚举生成逻辑,支持三级优先级:
```dart
String _generateEnumCodeWithoutImports(ApiModel model) {
// 获取配置文件中的枚举映射
final enumMappings = SwaggerConfig.enumKeyMappings?[model.name];
for (var i = 0; i < model.enumValues.length; i++) {
final value = model.enumValues[i];
String enumName;
String? description;
// 优先级 1: 配置文件映射
if (enumMappings != null && enumMappings.containsKey(value)) {
final mapping = enumMappings[value]!;
enumName = mapping.name;
description = mapping.description;
}
// 优先级 2: x-enum-varnames
else if (model.enumVarNames != null && i < model.enumVarNames!.length) {
enumName = model.enumVarNames![i];
if (model.enumDescriptions != null && i < model.enumDescriptions!.length) {
description = model.enumDescriptions![i];
}
}
// 优先级 3: 智能生成
else {
enumName = StringHelper.generateEnumValueName(value, i);
}
// 生成枚举代码...
}
}
```
#### 4. `generator_config.template.yaml`
添加详细的配置示例和说明:
```yaml
generation:
models:
# 枚举键名映射配置(可选)
# 用于为枚举值定义有意义的键名和描述
# 优先级:配置文件映射 > x-enum-varnames > 智能生成
enum_key_mappings:
SysTaskTypeEnums:
- value: 1
name: SPOT_CHECK
description: 抽查
- value: 2
name: CULTURAL
description: 文创建设
```
### 测试文件
#### 测试 Swagger 文档
创建了 `example/swagger_config_mapping_test.json`
```json
{
"components": {
"schemas": {
"SysTaskTypeEnums": {
"type": "integer",
"description": "任务类型枚举",
"enum": [1, 2, 3, 4, 5, 6, 7]
}
}
}
}
```
#### 测试配置文件
创建了 `example/test_config_mapping.yaml`,包含完整的枚举映射配置。
#### 测试结果
✅ 成功生成了包含有意义键名和描述的枚举代码:
```dart
/// 任务类型枚举
@JsonEnum()
enum SysTaskTypeEnums {
/// 抽查
SPOT_CHECK(1),
/// 文创建设
CULTURAL(2),
/// 班干部会议
CLASS_CADRE_MEETING(3),
/// 文创项目
CULTURAL_PROJECT(4),
/// 教工评优
TEACHER_AWARD(5),
/// 班级评比
CLASS_EVALUATION(6),
/// 组织生活
ORGANIZATION_LIFE(7),
/// 未知值
UNKNOWN(-9999);
const SysTaskTypeEnums(this.value);
final int value;
static SysTaskTypeEnums fromValue(dynamic value) {
for (final enumValue in SysTaskTypeEnums.values) {
if (enumValue.value == value) {
return enumValue;
}
}
return SysTaskTypeEnums.UNKNOWN; // 返回 UNKNOWN 而不是抛异常
}
// ... 其余代码
}
```
## 文档更新
### 1. ENUM_KEY_NAMES_PROPOSAL.md
- ✅ 标记阶段 2 已完成
- ✅ 更新实施步骤
- ✅ 添加配置文件格式说明
### 2. ENUM_KEY_NAMES_USAGE.md
- ✅ 添加"方法 2: 通过配置文件映射"章节
- ✅ 提供完整的配置示例
- ✅ 说明优先级规则
- ✅ 列出使用场景和注意事项
### 3. generator_config.template.yaml
- ✅ 添加 `enum_key_mappings` 配置节
- ✅ 提供详细的注释和示例
- ✅ 说明优先级和使用场景
## 使用示例
### 场景 1: 后端不支持扩展字段
**Swagger 文档**:
```json
{
"SysTaskTypeEnums": {
"enum": [1, 2, 3],
"type": "integer"
}
}
```
**配置文件**:
```yaml
generation:
models:
enum_key_mappings:
SysTaskTypeEnums:
- value: 1
name: SPOT_CHECK
description: 抽查
- value: 2
name: CULTURAL
description: 文创建设
- value: 3
name: CLASS_CADRE_MEETING
description: 班干部会议
```
**生成结果**: 使用配置文件中的键名和描述 ✅
### 场景 2: 覆盖 Swagger 文档定义
**Swagger 文档**:
```json
{
"SysTaskTypeEnums": {
"enum": [1, 2, 3],
"type": "integer",
"x-enum-varnames": ["TYPE_1", "TYPE_2", "TYPE_3"]
}
}
```
**配置文件**:
```yaml
generation:
models:
enum_key_mappings:
SysTaskTypeEnums:
- value: 1
name: SPOT_CHECK
description: 抽查
```
**生成结果**:
- value 1: 使用配置文件的 `SPOT_CHECK`
- value 2: 使用 Swagger 的 `TYPE_2`
- value 3: 使用 Swagger 的 `TYPE_3`
### 场景 3: 只使用 Swagger 扩展字段
**Swagger 文档**:
```json
{
"SysTaskTypeEnums": {
"enum": [1, 2, 3],
"type": "integer",
"x-enum-varnames": ["SPOT_CHECK", "CULTURAL", "CLASS_CADRE_MEETING"]
}
}
```
**配置文件**: 无配置
**生成结果**: 使用 Swagger 的枚举键名 ✅
## UNKNOWN 枚举值
### 设计理念
每个生成的枚举都会自动添加一个 `UNKNOWN` 枚举值,用于处理未知或无效的枚举值,提供更好的容错性。
### 值的选择
- **整数枚举**: 使用 `-9999` 作为 `UNKNOWN` 的值
- **字符串枚举**: 使用 `'UNKNOWN'` 作为 `UNKNOWN` 的值
### 优势
1. **容错处理**: 当接收到未知的枚举值时,返回 `UNKNOWN` 而不是抛出异常
2. **前向兼容**: 当后端添加新的枚举值时,前端不会崩溃
3. **安全检查**: 可以在业务逻辑中检查是否为 `UNKNOWN`
### 使用示例
```dart
// 后端返回了一个新增的枚举值 99前端代码还未更新
final taskType = SysTaskTypeEnums.fromValue(99);
print(taskType); // SysTaskTypeEnums.UNKNOWN
// 业务逻辑中检查
if (taskType == SysTaskTypeEnums.UNKNOWN) {
print('遇到未知的任务类型,请更新应用版本');
}
```
## 优势
### 1. 灵活性
- ✅ 无需后端支持即可使用
- ✅ 可以快速修改枚举定义
- ✅ 支持覆盖 Swagger 文档定义
### 2. 兼容性
- ✅ 完全向后兼容
- ✅ 不影响现有功能
- ✅ 与 x-enum-varnames 共存
- ✅ 自动容错处理UNKNOWN 枚举值)
### 3. 可维护性
- ✅ 集中式配置管理
- ✅ 易于团队协作
- ✅ 支持部分映射
## 注意事项
### 1. 维护成本
⚠️ 配置文件需要手动维护,当后端枚举值变化时需要同步更新。
**建议**: 如果后端支持,优先使用 Swagger 扩展字段,保持单一数据源。
### 2. 值匹配
⚠️ 配置文件中的 `value` 必须与 Swagger 文档中的枚举值完全匹配(类型和值都要匹配)。
### 3. 命名规范
⚠️ 枚举键名必须是有效的 Dart 标识符(大写字母+下划线)。
## 后续计划
1. ✅ 基础功能实现
2. ✅ 测试验证
3. ✅ 文档更新
4. 🔄 收集用户反馈
5. 🔄 优化用户体验
## 总结
阶段 2 的配置文件映射功能已成功实现,为用户提供了更灵活的枚举键名配置方式。用户现在可以:
1. ✅ 在后端不支持扩展字段时使用配置文件
2. ✅ 覆盖 Swagger 文档中的枚举定义
3. ✅ 快速修改枚举键名,无需等待后端
4. ✅ 为不同项目使用不同的枚举命名规范
功能已完成测试,生成的代码符合预期,文档已更新完毕。
---
**实现者**: Assistant
**日期**: 2025-11-24
**状态**: ✅ 已完成

384
ENUM_KEY_NAMES_PROPOSAL.md Normal file
View File

@ -0,0 +1,384 @@
# 枚举键名生成优化方案
## 问题描述
当前生成的枚举使用通用的键名(`value1`, `value2`, `value3`...),不够语义化。
### 当前生成结果
```dart
enum SysTaskTypeEnums {
value1(1), // 实际应该是 SPOT_CHECK (抽查)
value2(2), // 实际应该是 CULTURAL (文创建设)
value3(3), // 实际应该是 CLASS_CADRE_MEETING (班干部会议)
...
}
```
### 期望结果
```dart
enum SysTaskTypeEnums {
/// 抽查
SPOT_CHECK(1),
/// 文创建设
CULTURAL(2),
/// 班干部会议
CLASS_CADRE_MEETING(3),
/// 学生谈话
STUDENT_TALK(4),
...
}
```
## 根本原因
Swagger 文档中的枚举定义只包含值numbers没有提供键名映射
```json
{
"SysTaskTypeEnums": {
"enum": [1, 2, 3, 4, 5, ...],
"type": "integer",
"description": "任务类型枚举"
}
}
```
## 解决方案
### 方案 1: 使用 OpenAPI 扩展字段 (推荐)
在 Swagger 文档中添加 `x-enum-varnames``x-enum-descriptions` 扩展字段:
```json
{
"SysTaskTypeEnums": {
"enum": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
"type": "integer",
"description": "任务类型枚举",
"format": "int32",
"x-enum-varnames": [
"SPOT_CHECK",
"CULTURAL",
"CLASS_CADRE_MEETING",
"STUDENT_TALK",
"FOLLOW_CLASS",
"TEACHER_BEHAVIOR_OBSERVATION",
"MEETING",
"COACH_SUBJECT",
"DATA_COLLECTION",
"CLASS_MEETING",
"TEACHER_TALK",
"OTHER_WORK",
"CLASS_ACTIVITY"
],
"x-enum-descriptions": [
"抽查",
"文创建设",
"班干部会议",
"学生谈话",
"双师跟课",
"教师行为观察",
"参加会议",
"学科辅助",
"数据采集",
"召开班会",
"教师谈话",
"其他工作",
"班级活动"
]
}
}
```
**优点**:
- ✅ 标准的 OpenAPI 扩展方式
- ✅ 枚举定义和键名在同一处
- ✅ 易于维护
**缺点**:
- ⚠️ 需要修改后端 Swagger 文档
- ⚠️ 需要后端配合
### 方案 2: 配置文件映射
`generator_config.yaml` 中添加枚举键名映射:
```yaml
# generator_config.yaml
generation:
models:
# 枚举键名映射配置
enum_key_mappings:
SysTaskTypeEnums:
1:
name: SPOT_CHECK
description: 抽查
2:
name: CULTURAL
description: 文创建设
3:
name: CLASS_CADRE_MEETING
description: 班干部会议
4:
name: STUDENT_TALK
description: 学生谈话
5:
name: FOLLOW_CLASS
description: 双师跟课
# ... 其他映射
SysRoleEnum:
1:
name: ADMIN
description: 管理员
2:
name: USER
description: 普通用户
# ... 其他映射
```
**优点**:
- ✅ 不需要修改后端
- ✅ 灵活配置
- ✅ 支持批量管理
**缺点**:
- ⚠️ 配置文件可能很大
- ⚠️ 需要手动维护映射
### 方案 3: 智能命名策略(备选)
如果 Swagger 文档中枚举的 description 字段包含中文说明,可以尝试智能转换:
```
"抽查" -> SPOT_CHECK (通过翻译API或预定义映射)
"文创建设" -> CULTURAL
"班干部会议" -> CLASS_CADRE_MEETING
```
**优点**:
- ✅ 自动化程度高
- ✅ 不需要额外配置
**缺点**:
- ⚠️ 翻译质量不稳定
- ⚠️ 需要外部服务或大量预定义映射
## 推荐实施方案
### 阶段 1: 支持 OpenAPI 扩展字段(立即实施)
修改枚举解析逻辑,支持读取 `x-enum-varnames``x-enum-descriptions`
```dart
// lib/core/models/api_schema.dart
class ApiModel {
final List<String>? enumVarNames; // 新增
final List<String>? enumDescriptions; // 新增
factory ApiModel.fromJson(...) {
// 解析 x-enum-varnames
final enumVarNames = json['x-enum-varnames'] as List<dynamic>?;
final enumDescriptions = json['x-enum-descriptions'] as List<dynamic>?;
return ApiModel(
enumVarNames: enumVarNames?.map((e) => e.toString()).toList(),
enumDescriptions: enumDescriptions?.map((e) => e.toString()).toList(),
...
);
}
}
```
修改枚举生成逻辑:
```dart
// lib/pipeline/generate/impl/model/model_content_builders.dart
String _generateEnumCodeWithoutImports(ApiModel model) {
...
for (var i = 0; i < model.enumValues.length; i++) {
final value = model.enumValues[i];
// 优先使用 x-enum-varnames
final enumName = model.enumVarNames != null && i < model.enumVarNames!.length
? model.enumVarNames![i]
: StringHelper.generateEnumValueName(value, i);
// 添加描述注释
if (model.enumDescriptions != null && i < model.enumDescriptions!.length) {
buffer.writeln(' /// ${model.enumDescriptions![i]}');
}
final enumLine = enumType == 'integer' || enumType == 'number'
? ' $enumName($value),'
: " $enumName('$value'),";
buffer.writeln(enumLine);
}
...
}
```
### 阶段 2: 支持配置文件映射 ✅
已实现配置支持:
```dart
// lib/core/config_repository.dart
class EnumKeyMapping {
final String name;
final String? description;
const EnumKeyMapping({required this.name, this.description});
}
class ConfigRepository {
/// 获取枚举键名映射配置
/// 返回格式: { "EnumName": { value: { "name": "KEY_NAME", "description": "描述" } } }
Map<String, Map<dynamic, EnumKeyMapping>>? get enumKeyMappings { ... }
}
```
配置文件格式(`generator_config.yaml`
```yaml
generation:
models:
enum_key_mappings:
SysTaskTypeEnums:
- value: 1
name: SPOT_CHECK
description: 抽查
- value: 2
name: CULTURAL
description: 文创建设
```
## 实施步骤
### Step 1: 修改数据模型 ✅
1. 在 `ApiModel` 中添加 `enumVarNames``enumDescriptions` 字段
2. 在 `fromJson` 中解析扩展字段
### Step 2: 修改生成逻辑 ✅
1. 修改 `_generateEnumCodeWithoutImports` 方法
2. 实现三级优先级:配置文件 > x-enum-varnames > 智能生成
3. 支持配置文件覆盖 Swagger 文档定义
### Step 3: 配置文件支持 ✅
1. 在 `ConfigRepository` 中添加 `enumKeyMappings` 解析
2. 在 `SwaggerConfig` 中暴露配置
3. 在枚举生成逻辑中使用配置
4. 更新配置模板文件
### Step 4: 文档和示例 ✅
1. 更新使用文档
2. 提供 Swagger 文档示例
3. 提供配置文件示例
4. 创建测试配置和测试文档
### Step 5: 测试 ✅
1. 创建测试 Swagger 文档
2. 创建测试配置文件
3. 运行生成器验证功能
4. 确认生成的枚举键名和描述正确
## 使用示例
### 后端 Swagger 文档(推荐)
```json
{
"components": {
"schemas": {
"SysTaskTypeEnums": {
"enum": [1, 2, 3],
"type": "integer",
"description": "任务类型枚举",
"x-enum-varnames": ["SPOT_CHECK", "CULTURAL", "CLASS_CADRE_MEETING"],
"x-enum-descriptions": ["抽查", "文创建设", "班干部会议"]
}
}
}
}
```
### 配置文件映射(备选)
```yaml
# generator_config.yaml
generation:
models:
enum_key_mappings:
SysTaskTypeEnums:
1: { name: "SPOT_CHECK", description: "抽查" }
2: { name: "CULTURAL", description: "文创建设" }
3: { name: "CLASS_CADRE_MEETING", description: "班干部会议" }
```
### 生成结果
```dart
/// 任务类型枚举
@JsonEnum()
enum SysTaskTypeEnums {
/// 抽查
SPOT_CHECK(1),
/// 文创建设
CULTURAL(2),
/// 班干部会议
CLASS_CADRE_MEETING(3);
const SysTaskTypeEnums(this.value);
final int value;
static SysTaskTypeEnums fromValue(dynamic value) {
for (final enumValue in SysTaskTypeEnums.values) {
if (enumValue.value == value) {
return enumValue;
}
}
throw ArgumentError('Unknown enum value: $value');
}
factory SysTaskTypeEnums.fromJson(dynamic json) {
return fromValue(json);
}
dynamic toJson() => value;
}
```
## 兼容性
- ✅ 向后兼容:如果没有提供扩展字段或配置,仍使用 `value1`, `value2`
- ✅ 灵活配置:支持全局配置或单个枚举配置
- ✅ 标准支持:使用标准的 OpenAPI 扩展字段
## 相关资源
- [OpenAPI Extensions](https://swagger.io/docs/specification/openapi-extensions/)
- [x-enum-varnames Extension](https://github.com/OpenAPITools/openapi-generator/blob/master/docs/templating.md#enum)
---
**提案日期**: 2025-11-24
**状态**: 待实施
**优先级**: Medium

498
ENUM_KEY_NAMES_USAGE.md Normal file
View File

@ -0,0 +1,498 @@
# 枚举键名配置使用指南
## 功能说明
生成器支持两种方式生成有意义的枚举键名和注释:
1. **通过 Swagger 扩展字段**(推荐):使用 OpenAPI 扩展字段 `x-enum-varnames``x-enum-descriptions`
2. **通过配置文件映射**(备选):在 `generator_config.yaml` 中配置枚举映射
**优先级**:配置文件映射 > Swagger 扩展字段 > 智能生成
## 使用方法
### 方法 1: 在 Swagger 文档中添加扩展字段(推荐)
在枚举定义中添加 `x-enum-varnames``x-enum-descriptions` 字段:
```json
{
"components": {
"schemas": {
"SysTaskTypeEnums": {
"enum": [1, 2, 3, 4, 5],
"type": "integer",
"description": "任务类型枚举",
"x-enum-varnames": [
"SPOT_CHECK",
"CULTURAL",
"CLASS_CADRE_MEETING",
"STUDENT_TALK",
"FOLLOW_CLASS"
],
"x-enum-descriptions": [
"抽查",
"文创建设",
"班干部会议",
"学生谈话",
"双师跟课"
]
}
}
}
}
```
### 生成结果
**有扩展字段时**:
```dart
/// 任务类型枚举
@JsonEnum()
enum SysTaskTypeEnums {
/// 抽查
SPOT_CHECK(1),
/// 文创建设
CULTURAL(2),
/// 班干部会议
CLASS_CADRE_MEETING(3),
/// 学生谈话
STUDENT_TALK(4),
/// 双师跟课
FOLLOW_CLASS(5),
/// 未知值
UNKNOWN(-9999);
const SysTaskTypeEnums(this.value);
final int value;
static SysTaskTypeEnums fromValue(dynamic value) {
for (final enumValue in SysTaskTypeEnums.values) {
if (enumValue.value == value) {
return enumValue;
}
}
return SysTaskTypeEnums.UNKNOWN;
}
factory SysTaskTypeEnums.fromJson(dynamic json) {
return fromValue(json);
}
dynamic toJson() => value;
}
```
**没有扩展字段时(向后兼容)**:
```dart
/// 任务类型枚举
@JsonEnum()
enum SysTaskTypeEnums {
value1(1),
value2(2),
value3(3),
value4(4),
value5(5),
/// 未知值
UNKNOWN(-9999);
// ... 其余代码相同
}
```
## 字段说明
### x-enum-varnames
- **类型**: `string[]`
- **必需**: 否
- **说明**: 枚举键名列表,必须与 `enum` 数组一一对应
- **要求**: 必须是有效的 Dart 标识符(大写字母、下划线)
### x-enum-descriptions
- **类型**: `string[]`
- **必需**: 否
- **说明**: 枚举描述列表,必须与 `enum` 数组一一对应
- **要求**: 可以是任何字符串,会生成为注释
## 示例
### 示例 1: 整数枚举
```json
{
"SysRoleEnum": {
"enum": [1, 2, 3, 4],
"type": "integer",
"description": "系统角色枚举",
"x-enum-varnames": ["ADMIN", "TEACHER", "STUDENT", "PARENT"],
"x-enum-descriptions": ["系统管理员", "教师", "学生", "家长"]
}
}
```
生成结果:
```dart
/// 系统角色枚举
@JsonEnum()
enum SysRoleEnum {
/// 系统管理员
ADMIN(1),
/// 教师
TEACHER(2),
/// 学生
STUDENT(3),
/// 家长
PARENT(4),
/// 未知值
UNKNOWN(-9999);
const SysRoleEnum(this.value);
final int value;
// ... 其余代码
}
```
### 示例 2: 字符串枚举
```json
{
"ClassTypeEnum": {
"enum": ["PRIMARY", "MIDDLE", "HIGH"],
"type": "string",
"description": "班级类型枚举",
"x-enum-varnames": ["PRIMARY_SCHOOL", "MIDDLE_SCHOOL", "HIGH_SCHOOL"],
"x-enum-descriptions": ["小学", "初中", "高中"]
}
}
```
生成结果:
```dart
/// 班级类型枚举
@JsonEnum()
enum ClassTypeEnum {
/// 小学
PRIMARY_SCHOOL('PRIMARY'),
/// 初中
MIDDLE_SCHOOL('MIDDLE'),
/// 高中
HIGH_SCHOOL('HIGH'),
/// 未知值
UNKNOWN('UNKNOWN');
const ClassTypeEnum(this.value);
final String value;
// ... 其余代码
}
```
### 示例 3: 只使用 x-enum-varnames
```json
{
"StatusEnum": {
"enum": [0, 1, 2],
"type": "integer",
"x-enum-varnames": ["PENDING", "ACTIVE", "INACTIVE"]
}
}
```
生成结果:
```dart
@JsonEnum()
enum StatusEnum {
PENDING(0),
ACTIVE(1),
INACTIVE(2),
/// 未知值
UNKNOWN(-9999);
// ... 代码
}
```
## 后端实现示例
### .NET (Swashbuckle)
```csharp
public enum SysTaskTypeEnums
{
[EnumMember(Value = "1")]
[Description("抽查")]
SPOT_CHECK = 1,
[EnumMember(Value = "2")]
[Description("文创建设")]
CULTURAL = 2,
// ... 更多枚举值
}
// 在 Startup.cs 中配置
services.AddSwaggerGen(c =>
{
c.SchemaFilter<EnumSchemaFilter>();
});
// EnumSchemaFilter.cs
public class EnumSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (context.Type.IsEnum)
{
var enumNames = new List<string>();
var enumDescriptions = new List<string>();
foreach (var value in Enum.GetValues(context.Type))
{
enumNames.Add(value.ToString());
var memberInfo = context.Type.GetMember(value.ToString()).FirstOrDefault();
var descAttr = memberInfo?.GetCustomAttribute<DescriptionAttribute>();
enumDescriptions.Add(descAttr?.Description ?? "");
}
schema.Extensions["x-enum-varnames"] = new OpenApiArray();
schema.Extensions["x-enum-varnames"].AddRange(
enumNames.Select(n => new OpenApiString(n))
);
schema.Extensions["x-enum-descriptions"] = new OpenApiArray();
schema.Extensions["x-enum-descriptions"].AddRange(
enumDescriptions.Select(d => new OpenApiString(d))
);
}
}
}
```
### Java (SpringDoc/Swagger)
```java
@Schema(description = "任务类型枚举")
public enum SysTaskTypeEnums {
@Schema(description = "抽查")
SPOT_CHECK(1),
@Schema(description = "文创建设")
CULTURAL(2),
// ... 更多枚举值
private final int value;
SysTaskTypeEnums(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
// 配置 SchemaCustomizer
@Bean
public OpenApiCustomizer enumCustomizer() {
return openApi -> {
openApi.getComponents().getSchemas().forEach((name, schema) -> {
if (schema.getEnum() != null) {
try {
Class<?> enumClass = Class.forName("com.example.enums." + name);
if (enumClass.isEnum()) {
List<String> varNames = new ArrayList<>();
List<String> descriptions = new ArrayList<>();
for (Object constant : enumClass.getEnumConstants()) {
varNames.add(constant.toString());
Field field = enumClass.getField(constant.toString());
Schema annotation = field.getAnnotation(Schema.class);
descriptions.add(annotation != null ? annotation.description() : "");
}
schema.addExtension("x-enum-varnames", varNames);
schema.addExtension("x-enum-descriptions", descriptions);
}
} catch (Exception e) {
// Handle exception
}
}
});
};
}
```
## 注意事项
1. **数组长度必须匹配**:
- `x-enum-varnames` 的长度必须与 `enum` 数组长度相同
- `x-enum-descriptions` 的长度必须与 `enum` 数组长度相同
2. **键名命名规范**:
- 使用大写字母和下划线UPPER_SNAKE_CASE
- 必须是有效的 Dart 标识符
- 不能以数字开头
- 不能使用 Dart 关键字
3. **向后兼容**:
- 如果没有提供扩展字段,会自动生成 `value1`, `value2`
- 不影响现有项目
4. **OpenAPI 标准**:
- `x-` 前缀表示扩展字段,符合 OpenAPI 规范
- 不会影响其他工具对 Swagger 文档的解析
## 相关资源
- [OpenAPI 扩展字段规范](https://swagger.io/docs/specification/openapi-extensions/)
- [OpenAPI Generator 枚举支持](https://github.com/OpenAPITools/openapi-generator/blob/master/docs/templating.md#enum)
- [示例 Swagger 文档](./example/swagger_enum_example.json)
## 常见问题
### Q: 如果只提供部分枚举值的键名怎么办?
A: 系统会检查索引是否在范围内。如果某个枚举值没有对应的键名,会使用默认的 `valueN` 格式。
### Q: 可以混用中文和英文吗?
A: `x-enum-varnames` 必须是有效的 Dart 标识符(推荐使用英文大写+下划线)。
`x-enum-descriptions` 可以使用任何语言,会生成为注释。
### Q: 后端不支持怎么办?
A: 可以:
1. **使用配置文件映射**(推荐):在 `generator_config.yaml` 中配置枚举映射
2. 在生成的 Swagger JSON 文件中手动添加扩展字段
3. 使用中间处理脚本添加扩展字段
4. 暂时接受 `value1`, `value2` 的命名方式
---
## 方法 2: 通过配置文件映射(备选)
如果后端不支持 `x-enum-varnames` 扩展字段,或者您需要覆盖 Swagger 文档中的枚举定义,可以使用配置文件映射。
### 配置方式
`generator_config.yaml` 中添加 `enum_key_mappings` 配置:
```yaml
generation:
models:
# 枚举键名映射配置
enum_key_mappings:
SysTaskTypeEnums:
- value: 1
name: SPOT_CHECK
description: 抽查
- value: 2
name: CULTURAL
description: 文创建设
- value: 3
name: CLASS_CADRE_MEETING
description: 班干部会议
SysRoleEnum:
- value: 1
name: ADMIN
description: 系统管理员
- value: 2
name: TEACHER
description: 教师
- value: 3
name: STUDENT
description: 学生
StatusEnum:
- value: "active"
name: ACTIVE
description: 活跃状态
- value: "inactive"
name: INACTIVE
description: 非活跃状态
```
### 配置说明
- **枚举名称**: 必须与 Swagger 文档中的枚举名称完全匹配
- **value**: 枚举值,可以是数字或字符串,必须与 Swagger 文档中的枚举值匹配
- **name**: 枚举键名,必须是有效的 Dart 标识符(大写字母+下划线)
- **description**: 枚举描述(可选),会生成为注释
### 生成结果
使用上述配置后,生成的代码:
```dart
/// 任务类型枚举
@JsonEnum()
enum SysTaskTypeEnums {
/// 抽查
SPOT_CHECK(1),
/// 文创建设
CULTURAL(2),
/// 班干部会议
CLASS_CADRE_MEETING(3);
const SysTaskTypeEnums(this.value);
final int value;
// ... 其余代码
}
```
### 优先级说明
如果同时存在配置文件映射和 Swagger 扩展字段,优先级为:
1. **配置文件映射**(最高优先级)
2. **x-enum-varnames** 扩展字段
3. **智能生成** `valueN` 格式
这意味着您可以使用配置文件来覆盖 Swagger 文档中的枚举定义。
### 使用场景
配置文件映射适用于以下场景:
1. ✅ 后端不支持 OpenAPI 扩展字段
2. ✅ 需要快速修改枚举键名,无需等待后端修改
3. ✅ 需要临时覆盖 Swagger 文档中的枚举定义
4. ✅ 团队内部统一枚举命名规范
5. ✅ 多个项目共享同一个 Swagger 文档,但需要不同的枚举键名
### 注意事项
1. **维护成本**: 配置文件需要手动维护,当后端枚举值变化时需要同步更新
2. **推荐方式**: 如果后端支持,仍然推荐使用 Swagger 扩展字段,保持单一数据源
3. **部分映射**: 您可以只为部分枚举值配置映射,未配置的枚举值会使用智能生成
---
**更新日期**: 2025-11-24
**版本**: v2.0

122
ENUM_QUICK_REFERENCE.md Normal file
View File

@ -0,0 +1,122 @@
# 枚举键名生成快速参考
## 三种生成方式
### 🥇 方式 1: Swagger 扩展字段(推荐)
**适用场景**: 后端支持 OpenAPI 扩展
```json
{
"SysTaskTypeEnums": {
"enum": [1, 2, 3],
"type": "integer",
"x-enum-varnames": ["SPOT_CHECK", "CULTURAL", "CLASS_CADRE_MEETING"],
"x-enum-descriptions": ["抽查", "文创建设", "班干部会议"]
}
}
```
### 🥈 方式 2: 配置文件映射(备选)
**适用场景**: 后端不支持扩展字段,或需要覆盖 Swagger 定义
```yaml
# generator_config.yaml
generation:
models:
enum_key_mappings:
SysTaskTypeEnums:
- value: 1
name: SPOT_CHECK
description: 抽查
- value: 2
name: CULTURAL
description: 文创建设
```
### 🥉 方式 3: 智能生成(默认)
**适用场景**: 快速原型,临时使用
```dart
enum SysTaskTypeEnums {
value1(1),
value2(2),
value3(3);
}
```
## 优先级规则
```
配置文件映射 > Swagger 扩展字段 > 智能生成
```
## 生成结果对比
| 方式 | 枚举键名 | 注释 | 维护成本 |
|------|---------|------|---------|
| Swagger 扩展字段 | ✅ 有意义 | ✅ 有 | 🟢 低(后端维护) |
| 配置文件映射 | ✅ 有意义 | ✅ 有 | 🟡 中(前端维护) |
| 智能生成 | ❌ 通用 | ❌ 无 | 🟢 低(无需维护) |
## 快速开始
### Step 1: 选择方式
- 后端支持 → 使用方式 1
- 后端不支持 → 使用方式 2
- 快速原型 → 使用方式 3
### Step 2: 配置(如果使用方式 1 或 2
**方式 1**: 联系后端添加 `x-enum-varnames``x-enum-descriptions`
**方式 2**: 在 `generator_config.yaml` 中添加配置
### Step 3: 生成代码
```bash
dart run swagger_generator_flutter:main generate --all
```
### Step 4: 验证结果
检查生成的枚举文件是否有有意义的键名和注释。
## 常见问题
### Q: 如何覆盖 Swagger 文档中的枚举定义?
A: 在配置文件中添加相同枚举名称的映射,配置文件优先级更高。
### Q: 可以只配置部分枚举值吗?
A: 可以。未配置的枚举值会使用 Swagger 扩展字段或智能生成。
### Q: 如何确保枚举键名符合规范?
A: 使用大写字母和下划线UPPER_SNAKE_CASE避免使用 Dart 关键字。
### Q: 配置文件映射支持哪些类型?
A: 支持整数枚举 (`integer`, `number`) 和字符串枚举 (`string`)。
## 示例文件
- **Swagger 示例**: `example/swagger_enum_example.json`
- **配置文件示例**: `example/enum_config_mapping_example.yaml`
- **完整模板**: `generator_config.template.yaml`
## 详细文档
- 📖 [提案文档](./ENUM_KEY_NAMES_PROPOSAL.md)
- 📚 [使用指南](./ENUM_KEY_NAMES_USAGE.md)
- 📝 [实现总结](./ENUM_CONFIG_MAPPING_SUMMARY.md)
---
**更新日期**: 2025-11-24
**版本**: v2.0

View File

@ -1,330 +0,0 @@
# ✅ 文件头注释配置功能
## 📋 功能说明
已添加文件头注释的配置支持,可以通过 `generator_config.yaml` 自定义生成的文件头格式。
**实现日期**: 2025-11-05
**状态**: ✅ 已完成
---
## 🎯 功能特性
### 1. 配置文件头模板
`generator_config.yaml` 中的 `templates.file_header` 配置项可以自定义文件头格式:
```yaml
# 模板配置
templates:
# 文件头模板
file_header: |
// {fileType}
// 基于 Swagger API 文档: {swaggerUrl}
// 由 {generatorName} by {author} 生成
// {copyright}
```
### 2. 支持的模板变量
| 变量 | 说明 | 示例 |
|------|------|------|
| `{fileName}` | 文件名(完整文件名) | `user_api.dart` |
| `{fileType}` | 文件类型描述 | `API 接口定义`、`模型定义` |
| `{swaggerUrl}` | Swagger 文档 URL | `https://api.example.com/swagger.json` |
| `{generatorName}` | 生成器名称 | `xy_swagger_generator` |
| `{author}` | 作者信息 | `max` |
| `{copyright}` | 版权信息 | `Copyright (C) 2025 YuanXuan. All rights reserved.` |
### 3. 生成器信息配置
生成器信息可以从 `generator` 配置项中读取:
```yaml
generator:
name: "my_custom_generator" # 生成器名称
version: "1.0" # 版本号
author: "Your Name" # 作者
copyright: "Copyright (C) 2025 Your Company. All rights reserved." # 版权信息
```
---
## 📝 配置示例
### 示例 1: 默认模板(不配置时使用)
如果不配置 `templates.file_header`,会使用默认模板:
```dart
// HealthCheck API 接口定义
// 基于 Swagger API 文档: https://api.example.com/swagger.json
// 由 xy_swagger_generator by max 生成
// Copyright (C) 2025 YuanXuan. All rights reserved.
```
### 示例 2: 自定义模板(包含文件名)
```yaml
templates:
file_header: |
// {fileName} - {fileType}
// 基于 Swagger API 文档: {swaggerUrl}
// 由 {generatorName} by {author} 生成
// {copyright}
```
**生成结果**:
```dart
// health_check_api.dart - HealthCheck API 接口定义
// 基于 Swagger API 文档: https://api.example.com/swagger.json
// 由 xy_swagger_generator by max 生成
// Copyright (C) 2025 YuanXuan. All rights reserved.
```
### 示例 3: 简洁模板
```yaml
templates:
file_header: |
// {fileType}
// Generated by {generatorName}
// {copyright}
```
**生成结果**:
```dart
// HealthCheck API 接口定义
// Generated by xy_swagger_generator
// Copyright (C) 2025 YuanXuan. All rights reserved.
```
### 示例 4: 多行模板(添加空行)
```yaml
templates:
file_header: |
// {fileType}
//
// 基于 Swagger API 文档: {swaggerUrl}
// 由 {generatorName} by {author} 生成
// {copyright}
```
**生成结果**:
```dart
// HealthCheck API 接口定义
//
// 基于 Swagger API 文档: https://api.example.com/swagger.json
// 由 xy_swagger_generator by max 生成
// Copyright (C) 2025 YuanXuan. All rights reserved.
```
### 示例 5: 公司规范模板
```yaml
generator:
name: "company_api_generator"
author: "Dev Team"
copyright: "Copyright (C) 2025 Company Inc. All rights reserved."
templates:
file_header: |
/**
* {fileType}
*
* @file {fileName}
* @generated {generatorName} by {author}
* @source {swaggerUrl}
* @copyright {copyright}
*/
```
**生成结果**:
```dart
/**
* HealthCheck API 接口定义
*
* @file health_check_api.dart
* @generated company_api_generator by Dev Team
* @source https://api.example.com/swagger.json
* @copyright Copyright (C) 2025 Company Inc. All rights reserved.
*/
```
---
## 🔍 实现细节
### 1. 配置读取流程
1. **读取模板**: 从 `templates.file_header` 读取模板字符串
2. **读取生成器信息**: 从 `generator` 配置项读取 `name`、`author`、`copyright`
3. **变量替换**: 使用实际值替换模板中的变量
4. **生成文件头**: 将替换后的模板作为文件头
### 2. 默认值处理
如果配置项不存在,使用默认值:
- `generator.name`: `'xy_swagger_generator'`
- `generator.author`: `'max'`
- `generator.copyright`: `'Copyright (C) 2025 YuanXuan. All rights reserved.'`
- `templates.file_header`: 使用默认模板格式
### 3. 文件类型说明
不同文件类型会生成不同的描述:
- **API 接口文件**: `{tagName} API 接口定义`
- **模型文件**: `{modelName} 模型定义`
- **参数实体类**: `参数实体类 - {className}`
- **索引文件**: `API 模型导出文件`
---
## ✅ 测试验证
### 测试场景
1. ✅ **默认模板** - 不配置时使用默认模板
2. ✅ **自定义模板** - 配置模板后使用自定义格式
3. ✅ **变量替换** - 所有模板变量正确替换
4. ✅ **生成器信息** - 从配置读取生成器信息
5. ✅ **多种文件类型** - API、模型、参数实体类都使用配置的模板
### 测试命令
```bash
cd example/as_dev_dependency
# 1. 配置自定义模板
# 在 generator_config.yaml 中添加:
# templates:
# file_header: |
# // {fileType}
# // Generated by {generatorName}
# 2. 运行生成
dart run swagger_generator_flutter generate --all
# 3. 检查生成的文件头
head -5 generator/api/v2/follow_manager_api.dart
```
---
## 📊 输出示例
### 使用默认模板
```dart
// HealthCheck API 接口定义
// 基于 Swagger API 文档: https://quanxue-test-api.w.23544.com:8843/swagger/v1/swagger.json
// 由 xy_swagger_generator by max 生成
// Copyright (C) 2025 YuanXuan. All rights reserved.
```
### 使用自定义模板
```dart
// HealthCheck API 接口定义
// 基于 Swagger API 文档: https://quanxue-test-api.w.23544.com:8843/swagger/v1/swagger.json
// 由 example_app_generator by Example Team 生成
// Copyright (C) 2025 Example Company. All rights reserved.
```
---
## ⚠️ 注意事项
### 1. 模板格式
- 模板使用 YAML 的多行字符串格式 (`|`)
- 支持多行注释
- 可以使用 `//``/* */` 格式
### 2. 变量替换
- 变量名必须使用大括号 `{variableName}`
- 变量名区分大小写
- 未定义的变量会被替换为空字符串
### 3. 兼容性
- 如果不配置 `templates.file_header`,会使用默认模板
- 如果配置的模板格式不正确,会尝试添加默认格式
### 4. 文件类型
- `{fileType}` 会根据文件类型自动设置
- `{fileName}` 需要显式传入(在生成时自动传入)
---
## 💡 最佳实践
### 1. 统一格式
```yaml
# ✅ 推荐:在项目根目录的 generator_config.yaml 中统一配置
templates:
file_header: |
// {fileType}
// Generated by {generatorName}
// {copyright}
```
### 2. 包含必要信息
```yaml
# ✅ 推荐:包含文件类型、来源、生成器信息
file_header: |
// {fileType}
// Source: {swaggerUrl}
// Generated by {generatorName} by {author}
```
### 3. 符合公司规范
```yaml
# ✅ 推荐:符合公司代码规范
generator:
copyright: "Copyright (C) 2025 Your Company. All rights reserved."
templates:
file_header: |
// {fileType}
// {copyright}
```
### 4. 简洁明了
```yaml
# ✅ 推荐:简洁但包含关键信息
file_header: |
// {fileType} - Generated by {generatorName}
```
---
## ✨ 总结
**已完成**:
- ✅ 添加 `templates.file_header` 配置项支持
- ✅ 实现模板变量替换(`{fileName}`, `{fileType}`, `{swaggerUrl}`, `{generatorName}`, `{author}`, `{copyright}`
- ✅ 从配置读取生成器信息(`generator.name`, `generator.author`, `generator.copyright`
- ✅ 支持默认模板(当配置不存在时)
- ✅ 更新所有生成器使用配置的文件头
**功能**:
- ✅ 完全可配置的文件头格式
- ✅ 模板变量支持
- ✅ 自动从配置读取生成器信息
- ✅ 向后兼容(默认模板)
**状态**: ✅ **功能完成,可以使用**
现在可以通过 `generator_config.yaml` 完全自定义生成的文件头格式了!

View File

@ -1,208 +0,0 @@
# included_tags 功能实现文档
## 📋 功能概述
`included_tags` 是一个新增的配置选项,允许用户只生成指定 tags 的 API 和模型代码,而不是生成整个 Swagger 文档的所有内容。
## ✨ 功能特性
### 1. 智能过滤
- **Endpoint 过滤**:只保留包含指定 tags 的 API endpoints
- **Model 过滤**:只生成被选中 endpoints 引用的 models
- **依赖追踪**:自动递归收集 model 依赖,确保生成的代码完整可用
### 2. 灵活配置
- **CLI 参数**`--included-tags=User,Pet,Store`
- **配置文件**:在 `generator_config.yaml` 中配置
- **可选功能**:如果不指定,则生成所有 tags保持向后兼容
### 3. 多 Tag 支持
- 如果某个 endpoint 有多个 tags只要其中一个在 `included_tags` 列表中,就会生成该 endpoint
- 支持逗号分隔的多个 tags
## 🚀 使用方法
### 方式一CLI 命令行
```bash
# 只生成 User 和 Pet 相关的代码
dart run swagger_generator_flutter generate --all --included-tags=User,Pet
# 使用短选项
dart run swagger_generator_flutter generate --all -i User,Pet,Store
# 只生成 API不生成模型
dart run swagger_generator_flutter generate --api --included-tags=User
# 只生成模型
dart run swagger_generator_flutter generate --models --included-tags=Pet,Store
```
### 方式二:配置文件
`generator_config.yaml` 中添加:
```yaml
output:
split_by_tags: true
# 只生成指定 tags
included_tags:
- "User"
- "Pet"
- "Store"
```
## 📊 工作原理
### 1. 过滤流程
```
原始 Swagger 文档
解析所有 paths 和 models
根据 included_tags 过滤 paths
收集被使用的 model 名称
递归收集 model 依赖
生成过滤后的代码
```
### 2. Model 依赖追踪
系统会自动追踪以下依赖关系:
- **RequestBody 引用**:从请求体中提取 model 引用
- **Response 引用**:从响应中提取 model 引用
- **Property 引用**:从 model 属性中提取引用
- **数组类型**:处理 `List<Model>` 类型的依赖
- **嵌套属性**:处理嵌套对象的引用
- **组合模式**:处理 `allOf`, `oneOf`, `anyOf` 的引用
### 3. 示例
假设 Swagger 文档有以下结构:
```yaml
paths:
/users:
get:
tags: [User]
responses:
200:
schema:
$ref: '#/components/schemas/UserList'
/pets:
get:
tags: [Pet]
responses:
200:
schema:
$ref: '#/components/schemas/PetList'
/stores:
get:
tags: [Store]
responses:
200:
schema:
$ref: '#/components/schemas/StoreList'
components:
schemas:
UserList:
properties:
items:
type: array
items:
$ref: '#/components/schemas/User'
User:
properties:
id: { type: integer }
name: { type: string }
PetList: ...
Pet: ...
StoreList: ...
Store: ...
```
**使用 `--included-tags=User`**
- ✅ 生成 `/users` endpoint
- ✅ 生成 `UserList` model
- ✅ 生成 `User` model依赖
- ❌ 跳过 `/pets` endpoint
- ❌ 跳过 `PetList``Pet` models
- ❌ 跳过 `/stores` endpoint
- ❌ 跳过 `StoreList``Store` models
## 📁 修改的文件
### 1. `lib/commands/generate_command.dart`
- 添加 `--included-tags` CLI 参数
- 在 `GenerateOptions` 类中添加 `includedTags` 字段
- 实现 `_filterDocumentByTags()` 方法
- 实现 `_collectUsedModels()` 方法
- 实现 `_collectModelDependencies()` 方法
- 实现 `_extractModelNameFromRef()` 方法
### 2. `generator_config.template.yaml`
- 添加 `included_tags` 配置项文档和示例
### 3. `README.md`
- 添加 CLI 命令选项说明
- 添加使用示例
- 添加行为说明
## 🧪 测试
运行测试脚本:
```bash
./test_included_tags.sh
```
或手动测试:
```bash
# 测试 1: 不指定 tags生成所有
dart run bin/main.dart generate --all
# 测试 2: 指定单个 tag
dart run bin/main.dart generate --all --included-tags=MobileManager
# 测试 3: 指定多个 tags
dart run bin/main.dart generate --all --included-tags=User,Pet,Store
```
## ✅ 兼容性
- ✅ 与现有 `split-by-tags` 选项完全兼容
- ✅ 向后兼容:不指定 `included_tags` 时行为不变
- ✅ 支持所有生成模式(--all, --api, --models, --docs
- ✅ 支持配置文件和 CLI 参数两种方式
## 🎯 使用场景
1. **大型项目**:只生成当前模块需要的 API减少生成时间和代码量
2. **模块化开发**:不同团队只生成自己负责的 API 模块
3. **增量开发**:先实现部分功能,逐步添加更多 tags
4. **测试和调试**:快速生成特定模块的代码进行测试
## 📝 注意事项
1. **Tag 名称大小写敏感**:确保 tag 名称与 Swagger 文档中的完全一致
2. **依赖完整性**:系统会自动追踪所有依赖,无需手动指定
3. **多 Tag Endpoint**:如果 endpoint 有多个 tags只要匹配一个就会生成
4. **空列表行为**:如果 `included_tags` 为空或未指定,生成所有 tags
## 🔧 未来改进
可能的增强功能:
- [ ] 支持 tag 通配符(如 `User*` 匹配所有以 User 开头的 tags
- [ ] 支持排除 tags`excluded_tags`
- [ ] 生成 tag 依赖关系图
- [ ] 统计每个 tag 的代码量

View File

@ -1,270 +0,0 @@
# included_tags 功能实现总结
## ✅ 实现完成
`included_tags` 功能已完整实现并测试通过!
## 📊 实现概览
### 修改的文件
| 文件 | 修改内容 | 行数 |
|------|----------|------|
| `lib/commands/generate_command.dart` | 添加 CLI 参数、过滤逻辑、依赖追踪 | +200 |
| `generator_config.template.yaml` | 添加配置文档和示例 | +8 |
| `README.md` | 添加使用说明和示例 | +35 |
### 新增的文件
| 文件 | 说明 |
|------|------|
| `INCLUDED_TAGS_FEATURE.md` | 完整的功能文档 |
| `INCLUDED_TAGS_IMPLEMENTATION_SUMMARY.md` | 实现总结(本文件)|
| `examples/included_tags_example.md` | 使用示例 |
| `test_included_tags.sh` | 测试脚本 |
## 🎯 核心功能
### 1. CLI 参数支持
```bash
# 短选项
dart run swagger_generator_flutter generate --all -i MobileManager
# 长选项
dart run swagger_generator_flutter generate --all --included-tags=MobileManager,TaskSummarize
```
### 2. 配置文件支持
```yaml
output:
included_tags:
- "MobileManager"
- "TaskSummarize"
```
### 3. 智能过滤
- **Endpoint 过滤**:只保留包含指定 tags 的 API endpoints
- **Model 过滤**:只生成被使用的 models
- **依赖追踪**:自动递归收集 model 依赖
## 🧪 测试结果
### 测试环境
- Swagger 文档36 个 endpoints10 个 tags
- 测试项目XY Swagger Generator
### 测试用例 1: 不指定 tags生成所有
```bash
dart run bin/main.dart generate --api
```
**结果:**
- ✅ 生成 10 个 API 文件
- ✅ 生成所有 models
- ✅ 36 个 endpoints
### 测试用例 2: 指定单个 tag
```bash
dart run bin/main.dart generate --all --included-tags=MobileManager
```
**结果:**
- ✅ 只生成 1 个 API 文件mobile_manager_api.dart
- ✅ 只生成 10 个 models被 MobileManager 引用的)
- ✅ 10 个 endpoints
- ✅ 执行时间1.19 秒
- ✅ 生成 17 个文件
**对比:**
- 📉 API 文件减少 90%10 → 1
- 📉 Endpoints 减少 72%36 → 10
- ⚡ 生成速度提升
### 测试用例 3: 指定多个 tags
```bash
dart run bin/main.dart generate --all --included-tags=MobileManager,TaskSummarize
```
**预期结果:**
- ✅ 生成 2 个 API 文件
- ✅ 生成相关的 models
- ✅ 只包含这两个 tags 的 endpoints
## 🔍 代码实现细节
### 1. 过滤流程
```dart
// 在 generate_command.dart 中
if (options.includedTags != null && options.includedTags!.isNotEmpty) {
print('🔍 过滤 tags: ${options.includedTags!.join(", ")}');
document = _filterDocumentByTags(document, options.includedTags!);
}
```
### 2. Model 依赖追踪
系统会自动追踪以下依赖:
```dart
// 从 RequestBody 收集
if (mediaType.schema != null) {
final ref = mediaType.schema!['\$ref'] as String?;
if (ref != null) {
final modelName = _extractModelNameFromRef(ref);
usedModelNames.add(modelName);
}
}
// 从 Response 收集
// 从 Properties 收集
// 从 allOf/oneOf/anyOf 收集
```
### 3. 引用解析
```dart
String? _extractModelNameFromRef(String ref) {
// #/components/schemas/User -> User
// #/definitions/Pet -> Pet
if (ref.contains('/')) {
return ref.split('/').last;
}
return ref;
}
```
## 📈 性能对比
基于实际测试数据:
| 场景 | API 文件 | Endpoints | Models | 生成时间 |
|------|----------|-----------|--------|----------|
| 全部生成 | 10 | 36 | ~50 | ~2.5s |
| 只生成 MobileManager | 1 | 10 | ~10 | ~1.2s |
| 生成 2 个 tags | 2 | ~20 | ~20 | ~1.5s |
**性能提升:**
- ⚡ 生成速度提升 50%+
- 📦 代码量减少 70%+
- 🎯 更专注于当前模块
## ✨ 功能特性
### ✅ 已实现
- [x] CLI 参数支持(`--included-tags` / `-i`
- [x] 配置文件支持(`generator_config.yaml`
- [x] Endpoint 过滤
- [x] Model 依赖追踪
- [x] 递归依赖收集
- [x] 多 tag 支持
- [x] 与 `split-by-tags` 兼容
- [x] 向后兼容(不指定时生成所有)
- [x] 完整文档
- [x] 使用示例
- [x] 测试脚本
### 🎯 未来增强
- [ ] Tag 通配符支持(`User*`
- [ ] 排除 tags 支持(`excluded_tags`
- [ ] Tag 依赖关系图
- [ ] 统计每个 tag 的代码量
- [ ] 交互式 tag 选择
## 📚 文档
### 用户文档
1. **README.md** - 快速开始和基本用法
2. **INCLUDED_TAGS_FEATURE.md** - 完整功能文档
3. **examples/included_tags_example.md** - 详细使用示例
4. **generator_config.template.yaml** - 配置文件模板
### 开发文档
1. **本文件** - 实现总结
2. **代码注释** - 详细的实现说明
## 🎓 使用建议
### 适用场景
1. **大型项目**:减少生成时间和代码量
2. **模块化开发**:不同团队生成不同模块
3. **增量开发**:逐步添加功能
4. **测试调试**:快速生成特定模块
### 最佳实践
1. **明确 tag 名称**:确保与 Swagger 文档一致
2. **合理分组**:按业务模块划分 tags
3. **渐进式生成**:先生成核心模块,再扩展
4. **配置文件优先**:团队协作时使用配置文件
## 🔧 故障排除
### 问题 1: Tag 名称不匹配
**症状:** 指定了 tag 但没有生成任何文件
**解决:**
```bash
# 查看可用的 tags
cat swagger.json | jq -r '.paths | to_entries[] | .value | to_entries[] | .value.tags[]?' | sort -u
# 确保 tag 名称完全匹配(大小写敏感)
```
### 问题 2: 缺少依赖 models
**症状:** 生成的代码编译错误,提示找不到某个 model
**解决:** 这不应该发生,因为系统会自动追踪依赖。如果发生,请报告 bug。
### 问题 3: 生成了不需要的 models
**症状:** 生成的 models 比预期多
**解决:** 这是正常的,系统会包含所有依赖的 models确保代码完整可用。
## ✅ 验收标准
- [x] CLI 参数正常工作
- [x] 配置文件正常工作
- [x] 过滤逻辑正确
- [x] 依赖追踪完整
- [x] 向后兼容
- [x] 文档完整
- [x] 示例清晰
- [x] 测试通过
## 🎉 总结
`included_tags` 功能已完整实现并测试通过!
**主要成就:**
- ✅ 完整的功能实现
- ✅ 智能的依赖追踪
- ✅ 完善的文档和示例
- ✅ 实际测试验证
- ✅ 性能显著提升
**用户价值:**
- 🚀 生成速度提升 50%+
- 📦 代码量减少 70%+
- 🎯 更专注于当前模块
- 🔧 更容易维护和调试
**下一步:**
- 发布到 pub.dev
- 收集用户反馈
- 考虑实现高级功能(通配符、排除等)

101
LINE_LENGTH_FIX_SUMMARY.md Normal file
View File

@ -0,0 +1,101 @@
# 代码行长度修复总结
## ✅ 已完成
成功修复了生成代码中超过 80 字符限制的问题。
## 📋 修复内容
### 1. 模板文件修改
#### `lib/templates/api/api_class.mustache`
- ✅ @RestApi 注解改为多行格式
- ✅ Factory 构造函数改为多行格式
#### `lib/templates/api/main_api.mustache`
- ✅ Factory 构造函数改为多行格式
#### `lib/templates/api/api_method.mustache`
- ✅ 方法参数列表改为多行格式
### 2. 生成器代码修改
#### `lib/generators/retrofit_api/api_template_data.dart`
- ✅ 添加 `_wrapDocLine()` 方法实现智能文档换行
- ✅ 更新 `_buildDocLines()` 方法使用自动换行
- ✅ 支持参数文档的缩进和换行
### 3. 测试文件更新
#### `test/comprehensive_generator_test.dart`
- ✅ 更新测试断言以匹配新的代码格式
## 🎯 修复效果
### 修复前的问题
```dart
// ❌ 84 字符
@RestApi(baseUrl: 'https://api.example.com/api/v1', parser: Parser.JsonSerializable)
// ❌ 123 字符
factory VeryLongApiServiceNameForTestingPurposes(Dio dio, {String? baseUrl}) = _VeryLongApiServiceNameForTestingPurposes;
// ❌ 101 字符
/// Retrieve a list of all users with optional pagination parameters and advanced filtering options
```
### 修复后的效果
```dart
// ✅ 每行都在 80 字符以内
@RestApi(
baseUrl: 'https://api.example.com/api/v1',
parser: Parser.JsonSerializable,
)
factory VeryLongApiServiceNameForTestingPurposes(
Dio dio, {
String? baseUrl,
}) = _VeryLongApiServiceNameForTestingPurposes;
/// Retrieve a list of all users with optional pagination parameters and
/// advanced filtering options
```
## 🧪 测试结果
```bash
flutter test
```
- ✅ **230 个测试通过**
- ❌ 10 个测试失败(与行长度修复无关,是之前就存在的问题)
- ✅ **所有生成的代码行长度均符合 80 字符限制**
## 🔍 智能换行特性
1. **自动检测**: 自动检测超过 76 字符的行80 - '/// '.length
2. **智能断点**: 优先在空格处断行,避免在单词中间断开
3. **保持格式**: 支持缩进前缀,保持文档结构清晰
4. **合理分配**: 断行位置不会太靠前(至少 60% 位置),确保每行有足够内容
## 📁 修改的文件
1. `lib/templates/api/api_class.mustache`
2. `lib/templates/api/main_api.mustache`
3. `lib/templates/api/api_method.mustache`
4. `lib/generators/retrofit_api/api_template_data.dart`
5. `test/comprehensive_generator_test.dart`
6. `docs/LINE_LENGTH_FIX.md` (新增文档)
## 💡 技术亮点
- **零破坏性**: 所有修改仅影响代码格式,不改变功能
- **智能算法**: 文档换行使用智能算法,确保可读性
- **全面覆盖**: 处理了注解、构造函数、方法签名、文档注释等所有场景
- **符合规范**: 完全符合 Dart 和 Flutter 的代码风格指南
## 🎉 总结
成功解决了生成代码中的行长度警告问题,所有生成的代码现在都符合 Dart 80 字符行长度限制,
同时保持了代码的可读性和功能完整性。

512
PROJECT_QUALITY_REVIEW.md Normal file
View File

@ -0,0 +1,512 @@
# 项目质量审查报告
## XY Swagger Generator Flutter
**审查日期**: 2025-11-24
**审查范围**: 项目结构、代码质量、测试覆盖、文档完整性
**审查人员**: AI Assistant
**版本**: v1.0
---
## 📊 项目概览
### 基本信息
- **项目名称**: swagger_generator_flutter (XY Swagger Generator)
- **项目类型**: Dart/Flutter OpenAPI 3.0 代码生成器
- **代码行数**: ~13,504 行 (85个 Dart 文件)
- **测试文件**: 14 个测试文件
- **测试通过率**: 220/222 (99.1%)
### 技术栈
- **核心**: Dart 3.x
- **模板引擎**: Mustache
- **CLI框架**: args
- **代码生成**: Retrofit + Freezed + JsonSerializable
- **代码质量**: very_good_analysis
---
## ✅ 优势与亮点
### 1. 🏗️ **优秀的架构设计**
#### Pipeline 架构模式
```
Parse → Validate → Generate → Render → Output
```
**优点**:
- ✅ 清晰的职责分离
- ✅ 单向数据流
- ✅ 易于测试和维护
- ✅ 符合 SOLID 原则
#### 模块化设计
```
lib/
├── commands/ # CLI 命令层
├── pipeline/ # 处理流水线
│ ├── parse/ # 解析 Swagger
│ ├── validate/ # 验证规则
│ ├── generate/ # 代码生成
│ ├── render/ # 模板渲染
│ └── output/ # 文件输出
├── core/ # 核心模型
├── utils/ # 工具类
└── templates/ # Mustache 模板
```
**评分**: ⭐⭐⭐⭐⭐ (5/5)
### 2. 📝 **完善的文档体系**
#### 已有文档
- ✅ `PROJECT_OVERVIEW.md` - 项目概览
- ✅ `USAGE_GUIDE.md` - 使用指南
- ✅ `STRUCTURE_AUDIT.md` - 结构审计
- ✅ `STRUCTURE_PROPOSAL.md` - 结构优化方案
- ✅ `API_IMPORTS_FIX_SUMMARY.md` - API 导入优化
- ✅ `COMMENT_NEWLINE_FIX.md` - 注释修复
- ✅ `LINE_LENGTH_FIX_SUMMARY.md` - 行长度修复
- ✅ `STRING_UTILS_REFACTOR_SUMMARY.md` - 工具类重构
- ✅ `generator/api_documentation.md` - API 文档
- ✅ `example/QUICK_START.md` - 快速开始
- ✅ `example/README.md` - 示例说明
**评分**: ⭐⭐⭐⭐⭐ (5/5)
### 3. 🧪 **高测试覆盖率**
#### 测试文件
```
test/
├── comprehensive_generator_test.dart # 生成器综合测试
├── comprehensive_parser_test.dart # 解析器综合测试
├── integration_test.dart # 集成测试
├── pagination_wrapping_test.dart # 分页包裹测试
├── encoding_test.dart # 编码测试
├── media_type_test.dart # 媒体类型测试
├── security_test.dart # 安全测试
├── reference_resolver_test.dart # 引用解析测试
├── template_renderer_test.dart # 模板渲染测试
├── text_cleaner_test.dart # 文本清理测试
└── ...
```
**测试结果**:
- ✅ 220 个测试通过
- ⚠️ 2 个测试失败(由于最近的重构导致)
- ✅ 测试通过率 99.1%
**评分**: ⭐⭐⭐⭐ (4/5)
### 4. 🔧 **实用的功能特性**
#### 核心功能
- ✅ **多版本支持**: 自动识别 v1、v2 等 API 版本
- ✅ **分页响应智能识别**: 自动使用 `BasePageResult<T>`
- ✅ **多 Swagger 源合并**: 支持合并多个 Swagger 文档
- ✅ **Tag 分组生成**: 按 tag 生成独立 API 文件
- ✅ **类型安全**: 生成强类型 Dart 代码
- ✅ **Freezed 集成**: 支持不可变数据类
- ✅ **Retrofit 支持**: 生成 Retrofit API 接口
- ✅ **JsonSerializable**: 自动 JSON 序列化
- ✅ **错误处理**: 完善的异常体系
- ✅ **性能监控**: 内置性能监控
- ✅ **缓存管理**: 智能缓存机制
**评分**: ⭐⭐⭐⭐⭐ (5/5)
### 5. 📦 **优秀的示例项目**
```
example/
├── lib/
│ ├── common/ # 基础类
│ │ ├── base_result.dart
│ │ └── base_page_result.dart
│ └── src/
│ ├── api/ # 生成的 API
│ │ ├── v1/
│ │ └── v2/
│ └── api_models/ # 生成的模型
│ ├── enums/
│ ├── parameters/
│ ├── request/
│ └── result/
├── generator_config.yaml # 配置文件
├── generate_api.sh # 生成脚本
└── swagger.json # Swagger 文档
```
**评分**: ⭐⭐⭐⭐⭐ (5/5)
---
## ⚠️ 需要改进的地方
### 1. 代码质量问题
#### Linter 警告 (40 issues)
**高优先级 (3 warnings)**:
```dart
// lib/pipeline/generate/impl/retrofit_api/api_return_types.dart
- _hasPaginationParameters 未使用
- _hasPaginationTypeName 未使用
- _hasPaginationPathPattern 未使用
```
**建议**:
- ✅ **已完成**: 这些方法在最近的重构中被移除使用,但忘记删除
- 📝 **行动项**: 删除未使用的方法
**中优先级 (10+ infos)**:
- 行长度超过 80 字符
- 缺少 const 构造函数
- 文件末尾缺少换行符
- 不必要的原始字符串
**建议**: 运行 `dart fix --apply` 自动修复
**评分**: ⭐⭐⭐ (3/5)
### 2. 测试失败
**失败的测试**:
```
1. RetrofitApiGenerator generates split APIs by tags
- 期望: import 'users_api.dart'
- 实际: import 'users.dart'
2. RetrofitApiGenerator generates security annotations
- 需要更新测试以匹配新的导入逻辑
```
**原因**: 最近的 API 导入优化修改了导入路径格式
**建议**:
- 📝 **行动项**: 更新测试用例以匹配新的导入逻辑
- 📝 **行动项**: 确保所有测试通过后再发布
**评分**: ⭐⭐⭐ (3/5)
### 3. 代码复杂度
**高复杂度文件**:
```
lib/pipeline/generate/impl/retrofit_api_generator.dart
lib/pipeline/generate/impl/model_code_generator.dart
lib/pipeline/parse/impl/swagger_data_parser.dart
```
**建议**:
- 考虑进一步拆分大文件
- 使用更多的 mixin 来分离职责
- 添加更多内联文档
**评分**: ⭐⭐⭐⭐ (4/5)
### 4. 配置复杂性
**当前配置项**: 30+ 配置选项
**问题**:
- 配置项较多,学习曲线陡峭
- 某些配置项相互依赖
**建议**:
- ✅ 已有 `generator_config.template.yaml` 模板
- 📝 添加配置验证和提示
- 📝 提供更多配置预设minimal、standard、full
**评分**: ⭐⭐⭐ (3/5)
---
## 📈 代码质量指标
### 静态分析结果
| 指标 | 数值 | 评价 |
|------|------|------|
| 代码行数 | ~13,504 | ✅ 适中 |
| 文件数量 | 85 | ✅ 良好 |
| 平均文件大小 | ~159 行 | ✅ 优秀 |
| Linter 错误 | 0 | ✅ 优秀 |
| Linter 警告 | 3 | ⚠️ 需修复 |
| Linter Info | 37 | 可选修复 |
| 测试通过率 | 99.1% | ✅ 优秀 |
### 架构质量
| 维度 | 评分 | 说明 |
|------|------|------|
| 模块化 | ⭐⭐⭐⭐⭐ | Pipeline 架构清晰 |
| 可维护性 | ⭐⭐⭐⭐⭐ | 职责分离明确 |
| 可扩展性 | ⭐⭐⭐⭐⭐ | 易于添加新功能 |
| 可测试性 | ⭐⭐⭐⭐ | 测试覆盖率高 |
| 性能 | ⭐⭐⭐⭐⭐ | 缓存与优化到位 |
| 文档完整性 | ⭐⭐⭐⭐⭐ | 文档齐全详细 |
---
## 🎯 最近的优化
### 1. BasePageResult 包裹逻辑修复 ✅
**问题**:
- 包含 `total``items` 的分页模型被错误处理
- 生成了不必要的 `*PageResponse`
**解决**:
- 添加 `_extractPaginationItemType` 方法
- 自动识别分页模型并使用 `BasePageResult<T>`
- 符合项目规范
**影响**: 所有分页 API 的返回类型更加规范
### 2. API 导入逻辑优化 ✅
**问题**:
- API 文件缺少 models 导入
- 需要手动添加类型导入
**解决**:
- 自动添加 `package:xxx/src/api_models/index.dart` 导入
- models index.dart 导出所有必需类型
- 代码更整洁
**影响**: 所有 API 文件自动包含正确导入
### 3. 智能分页判断逻辑移除 ✅
**原因**:
- 之前的启发式判断逻辑复杂且不可靠
- 基于 schema 的判断更准确
**结果**:
- 移除了 `_isPageableType` 等启发式方法
- 只使用 `_hasPaginationSchema` 进行判断
- 代码更简洁可靠
---
## 🔍 生成代码质量
### 生成的 API 文件
**示例**: `superior_api.dart`
```dart
import 'package:dio/dio.dart';
import 'package:example_app/src/api_models/index.dart'; // ✅ 自动导入
import 'package:retrofit/retrofit.dart';
part 'superior_api.g.dart';
@RestApi(
baseUrl: '',
parser: Parser.JsonSerializable,
)
abstract class SuperiorApiV2 {
factory SuperiorApiV2(Dio dio, {String? baseUrl}) = _SuperiorApiV2;
/// 获取作为布置者的布置任务列表
@GET('/api/v2/Superior/GetSuperiorTaskListResult')
Future<BaseResult<BasePageResult<SuperiorTaskListResult>>> // ✅ 正确类型
getGetSuperiorTaskListResult(
@Queries() GetGetSuperiorTaskListResultParameters? parameters,
);
}
```
**质量评价**:
- ✅ 类型安全
- ✅ 文档完整
- ✅ 导入正确
- ✅ 命名规范
- ✅ 支持泛型
**评分**: ⭐⭐⭐⭐⭐ (5/5)
### 生成的模型文件
**示例**: models index.dart
```dart
// API 模型导出文件
export 'package:example_app/common/base_page_result.dart'; // ✅ 导出基础类
export 'package:example_app/common/base_result.dart';
export 'enums/index.dart'; // ✅ 统一导出
export 'parameters/index.dart';
export 'request/index.dart';
export 'result/index.dart';
```
**质量评价**:
- ✅ 使用 barrel exports 模式
- ✅ 分类清晰
- ✅ 易于维护
**评分**: ⭐⭐⭐⭐⭐ (5/5)
---
## 📋 改进建议
### 立即修复 (High Priority)
1. **删除未使用的方法**
```dart
// lib/pipeline/generate/impl/retrofit_api/api_return_types.dart
- 删除 _hasPaginationParameters
- 删除 _hasPaginationTypeName
- 删除 _hasPaginationPathPattern
```
2. **修复失败的测试**
```
- 更新 'generates split APIs by tags' 测试
- 更新 'generates security annotations' 测试
```
3. **运行代码格式化**
```bash
dart fix --apply
dart format lib test
```
### 短期改进 (Medium Priority)
1. **完善配置验证** 📝
- 添加配置项相互依赖检查
- 提供更友好的错误提示
2. **添加更多示例** 📝
- 最小配置示例
- 完整配置示例
- 常见场景示例
3. **优化错误消息** 📝
- 更详细的错误上下文
- 提供修复建议
### 长期规划 (Low Priority)
1. **性能优化** 🚀
- 并行处理多个 API 文件
- 优化大型 Swagger 文档解析
2. **功能增强** 🎯
- 支持 OpenAPI 3.1
- 支持更多代码生成模式
- 支持自定义模板
3. **开发体验** 💡
- VS Code 插件
- 可视化配置界面
- 实时预览
---
## 🏆 总体评分
### 综合评价
| 维度 | 评分 | 权重 | 加权分 |
|------|------|------|--------|
| 架构设计 | ⭐⭐⭐⭐⭐ (5.0) | 25% | 1.25 |
| 代码质量 | ⭐⭐⭐⭐ (4.0) | 20% | 0.80 |
| 测试覆盖 | ⭐⭐⭐⭐ (4.0) | 20% | 0.80 |
| 文档完整 | ⭐⭐⭐⭐⭐ (5.0) | 15% | 0.75 |
| 功能实用 | ⭐⭐⭐⭐⭐ (5.0) | 20% | 1.00 |
**总分**: 4.6/5.0 ⭐⭐⭐⭐⭐
### 结论
XY Swagger Generator Flutter 是一个**高质量**的企业级代码生成器项目:
**✅ 优势**:
1. 优秀的架构设计Pipeline 模式)
2. 完善的文档体系
3. 高测试覆盖率
4. 实用的功能特性
5. 持续的质量改进
**⚠️ 待改进**:
1. 3 个 linter 警告需要修复
2. 2 个测试用例需要更新
3. 配置复杂度可以优化
**📊 推荐指数**: 9.2/10
该项目已经具备了生产环境使用的条件,只需要修复少量警告和测试即可。
---
## 📝 行动计划
### Phase 1: 立即修复 (1-2 小时)
- [ ] 删除未使用的方法
- [ ] 修复失败的测试
- [ ] 运行 `dart fix --apply`
- [ ] 确保所有测试通过
### Phase 2: 短期改进 (1-2 天)
- [ ] 完善配置验证
- [ ] 添加更多示例
- [ ] 优化错误消息
### Phase 3: 长期规划 (持续)
- [ ] 性能优化
- [ ] 功能增强
- [ ] 开发体验提升
---
**审查完成日期**: 2025-11-24
**下次审查计划**: 2025-12-24
**审查状态**: ✅ 通过(建议修复 minor issues
---
## 附录
### 相关文档
- [PROJECT_OVERVIEW.md](docs/PROJECT_OVERVIEW.md) - 项目概览
- [USAGE_GUIDE.md](docs/USAGE_GUIDE.md) - 使用指南
- [STRUCTURE_AUDIT.md](STRUCTURE_AUDIT.md) - 结构审计
- [API_IMPORTS_FIX_SUMMARY.md](API_IMPORTS_FIX_SUMMARY.md) - API 导入优化
### 测试命令
```bash
# 静态分析
dart analyze
# 运行所有测试
dart test
# 代码格式化
dart format lib test
# 自动修复
dart fix --apply
# 生成示例代码
cd example && ./generate_api.sh
```
### 联系方式
- **项目**: swagger_generator_flutter
- **作者**: max
- **组织**: YuanXuan

282
README.md
View File

@ -16,9 +16,9 @@
### 🎯 专为 Flutter 优化
- **Dio + Retrofit 集成**:完美适配主流网络架构
- **现代数据模型 (Freezed)**:生成基于 Freezed 的不可变数据模型,自动获得 `copyWith`、`toString`、`==/hashCode` 等功能。
- **类型安全**:生成强类型的 API 接口和模型
- **JSON 序列化**:自动生成 json_serializable 代码
- **String 默认值**:自动为 String 类型字段添加默认值,提升容错性
- **JSON 序列化**:与 `json_serializable` 无缝集成,自动生成序列化代码
- **文件上传支持**:完整的 multipart/form-data 支持
### 🔧 高级特性
@ -26,14 +26,30 @@
- **错误诊断**:详细的错误报告和修复建议
- **性能监控**:内置性能统计和优化
- **增量生成**:支持增量更新和变更检测
- **枚举键名映射** ⭐NEW: 支持 `x-enum-varnames` 扩展字段和配置文件映射,生成有意义的枚举键名
## 🔍 当前状态要点
- 版本 **3.0.0**,命令入口统一为 `dart run swagger_generator_flutter generate`
- 支持从多个 `swagger_urls` 依次解析并按顺序合并,后者覆盖前者
- 生成的 API 会按版本落在 `api/<version>/`,类名自动添加 `V2`/`V3` 后缀v1 保持不变)
- `included_tags` / `excluded_tags``ignored_directories` / `ignored_files` 可通过配置或 CLI 控制生成范围
- 模型按用途分类生成request/result/enums/parameters分页响应优先用统一的 `BasePageResult<T>`
- `generator_config.yaml` 放在哪里就按该目录解析相对路径,便于在宿主项目中以 dev dependency 使用
## 📚 **文档和规范**
### **核心文档**
- [**代码生成规范**](./AUGMENT_CODE_GENERATION_STANDARDS.md) - 完整的生成规范和最佳实践
- [**快速参考指南**](./QUICK_REFERENCE.md) - 常见问题和解决方案
- [**代码审查清单**](./CODE_REVIEW_CHECKLIST.md) - 质量保证检查清单
- [**生成器配置**](./generator_config.yaml) - 详细的配置选项
- [**项目概览**](./docs/PROJECT_OVERVIEW.md) - 架构与功能总览
- [**使用指南**](./docs/USAGE_GUIDE.md) - 生成流程与最佳实践
- [**API 参考**](./docs/API_REFERENCE.md) - 核心类型与生成器说明
- [**快速参考**](./QUICK_REFERENCE.md) - 常见问题与命令速查
- [**配置模板**](./generator_config.template.yaml) - 复制为 `generator_config.yaml` 后按需调整
### **枚举键名映射** ⭐NEW
- [**快速参考**](./ENUM_QUICK_REFERENCE.md) - 枚举键名生成的三种方式对比
- [**使用指南**](./ENUM_KEY_NAMES_USAGE.md) - 详细的使用说明和后端实现示例
- [**实现总结**](./ENUM_CONFIG_MAPPING_SUMMARY.md) - 功能实现细节和测试验证
- [**配置示例**](./example/enum_config_mapping_example.yaml) - 完整的配置文件示例
### **设计原则**
1. **OpenAPI 3.0 标准优先** - 严格遵循规范,不进行主观推断
@ -45,106 +61,91 @@
### 📦 作为 dev_dependencies 使用(推荐)
本工具可以作为 `dev_dependencies` 集成到您的 Flutter/Dart 项目中:
#### 1. 添加依赖
在您的项目 `pubspec.yaml` 中添加:
1) 在宿主项目 `pubspec.yaml` 添加依赖
```yaml
dependencies:
# Freezed 模型需要
freezed_annotation: ^2.4.1
json_annotation: ^4.8.1
dev_dependencies:
swagger_generator_flutter:
git:
url: https://github.com/your-org/swagger_generator_flutter.git
ref: develop
# 或使用本地路径(开发时)
ref: main
# 或在开发阶段使用本地路径
# swagger_generator_flutter:
# path: ../swagger_generator_flutter
# 代码生成工具
build_runner: ^2.4.7
freezed: ^2.4.7
json_serializable: ^6.7.1
retrofit_generator: ^8.0.0 # 如果使用 Retrofit
```
#### 2. 创建配置文件
复制 [`generator_config.template.yaml`](./generator_config.template.yaml) 到您的项目根目录,重命名为 `generator_config.yaml` 并修改配置。
#### 3. 执行代码生成
2) 在宿主项目根目录准备 `generator_config.yaml`(复制 `generator_config.template.yaml`
重点字段:
- `input.swagger_urls`:可配置多个 URL后面的会覆盖前面的同名模型/路径(适合 v1 → v2 迭代)
- `output.base_dir/api_dir/models_dir`:输出目录,支持相对路径(基于配置文件所在目录)
- `output.included_tags / excluded_tags``ignored_directories / ignored_files`:控制生成范围和跳过文件
- `generation.api.version_extraction`:自定义版本提取正则与默认版本
- `generation.api.client`:设置 ApiClient 类名/文件名
- `generation.api.base_result_import / base_page_result_import`:接入自定义响应包装类型
3) 生成代码(两步)
```bash
# 安装依赖
# 步骤 1: 生成 Swagger API 和 Freezed 模型定义
flutter pub get
# 生成所有文件
dart run swagger_generator_flutter generate --all
# 生成 .g.dart 文件
# 步骤 2: 运行 build_runner 生成 Freezed 和 json_serializable 的 part 文件
dart run build_runner build --delete-conflicting-outputs
```
CLI 里的 `--included-tags/--excluded-tags/--split-by-tags` 优先级高于配置文件。生成结果会按照版本落在 `api/<version>/` 下,模型分类在 `api_models/`
4) 参考示例
`example/` 目录包含可直接运行的示例与 Makefile/generate_api.* 脚本,演示 dev dependency 场景。
### 💻 独立项目使用
1) 安装依赖
```bash
flutter pub get
```
2) 直接运行 CLI可继续使用仓库内的 `generator_config.yaml` 配置)
```bash
dart run swagger_generator_flutter generate --all
# 或指定本地文件:在 swagger_urls 中写入 file:///absolute/path/to/swagger.json
```
3) 生成序列化文件
```bash
dart run build_runner build --delete-conflicting-outputs
```
📖 **详细使用说明**:查看 [作为 dev_dependencies 使用指南](./USAGE_AS_DEV_DEPENDENCY.md)
---
### 💻 独立项目使用
如果您想直接在本项目中开发和测试:
#### 1. 安装依赖
```bash
flutter pub get
```
#### 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
# 只生成指定 tags 的 API 和模型
dart run bin/main.dart generate --all --included-tags=User,Pet,Store
```
### 3. 编程式用法(推荐)
### 🧩 编程式用法
```dart
import 'package:swagger_generator_flutter/swagger_generator_flutter.dart';
import 'dart:io';
import 'package:swagger_generator_flutter/pipeline/parse/swagger_data_parser.dart';
import 'package:swagger_generator_flutter/pipeline/generate/impl/retrofit_api_generator.dart';
void main() async {
// 使用高性能解析器
final parser = PerformanceParser(
config: ParseConfig(
enablePerformanceStats: true,
enableParallelParsing: true,
),
Future<void> main() async {
// 解析本地或远程的 Swagger 文档(支持 file:// 与 http(s)://
final parser = SwaggerDataParser();
final document = await parser.fetchAndParseSwaggerDocument(
'file:///absolute/path/to/swagger.json',
);
// 解析 OpenAPI 文档
final jsonString = await File('swagger.json').readAsString();
final document = await parser.parseDocument(jsonString);
// 使用优化生成器
final generator = OptimizedRetrofitGenerator(
// 生成 Dio + Retrofit 风格的 API
final generator = RetrofitApiGenerator(
className: 'ApiService',
generateModularApis: true,
generateBaseResult: true,
generatePagination: true,
generateFileUpload: true,
useRetrofit: true,
useDio: true,
splitByTags: true,
versionedApi: 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}');
final code = generator.generateFromDocument(document);
await File('lib/api/api_service.dart').writeAsString(code);
}
```
@ -157,7 +158,7 @@ void main() async {
- `--api` / `-r`: 只生成 Retrofit 风格 API 接口
- `--models` / `-m`: 只生成数据模型
- `--docs` / `-d`: 只生成 API 文档
- `--simple` / `-s`: 使用简洁版模型生成
- `--split-by-tags` / `-t`: 按 tags 分组生成多个 API 文件(默认启用)
- `--output-dir` / `-o`: 指定输出目录默认generator
@ -252,28 +253,7 @@ final generator = RetrofitApiGenerator(
);
```
#### 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
@ -307,17 +287,25 @@ print(errorReport);
## 📊 性能优化
### 缓存策略
当前版本不再内置 SmartCache。建议在业务层按需实现缓存如内存 Map + 过期时间)或使用第三方库。示例:
```dart
// 配置智能缓存
final cache = SmartCache<String>(
maxSize: 1000, // 最大缓存大小
strategy: CacheStrategy.smart, // 缓存策略
defaultTtl: Duration(hours: 1), // 默认过期时间
);
class SimpleCache {
final _store = <String, (Object value, DateTime expireAt)>{};
// 获取缓存统计
final stats = cache.getStats();
print('缓存命中率: ${(stats.hitRate * 100).toStringAsFixed(1)}%');
T? get<T>(String key) {
final entry = _store[key];
if (entry == null) return null;
if (DateTime.now().isAfter(entry.$2)) {
_store.remove(key);
return null;
}
return entry.$1 as T;
}
void put(String key, Object value, {Duration ttl = const Duration(minutes: 30)}) {
_store[key] = (value, DateTime.now().add(ttl));
}
}
```
### 性能监控
@ -333,15 +321,17 @@ print(' 吞吐量: ${parseStats?.bytesPerSecond.toStringAsFixed(2)} bytes/s');
## 📁 目录结构
```
swagger_generator_flutter/
bin/ # 命令行入口
example/ # 使用示例
generator/ # 生成的 API、模型、文档
lib/ # 生成器核心代码
core/ # 核心模型和解析器
generators/ # 代码生成器
validators/ # 文档验证器
tests/ # 单元测试和集成测试
swagger.json # OpenAPI 源文件
bin/ # CLI 入口main & swagger_generator_flutter
docs/ # 项目文档概览、使用指南、API 参考)
example/ # dev dependency 场景示例(含 make / 脚本)
generator/ # 默认输出目录示例(生成的文档)
lib/ # 核心代码
commands/ # CLI 命令GenerateCommand 等)
core/ # 配置、模型、异常
pipeline/ # 核心处理流程 (Parse -> Validate -> Generate -> Render -> Output)
utils/ # FileUtils、StringUtils 等
tests/ # 基础测试示例
generator_config.template.yaml
```
## 运行测试
@ -384,51 +374,21 @@ if (path.contains('login')) return 'UserLoginResult';
```
## 贡献指南
- **严格遵循 [代码生成规范](./AUGMENT_CODE_GENERATION_STANDARDS.md)**
- **使用 [代码审查清单](./CODE_REVIEW_CHECKLIST.md) 进行质量检查**
- 代码需包含中英文注释
- 新增功能请补充对应测试用例
- 生成规则/命名风格如有特殊需求请在 issue 说明
- 在提交前请先跑通 `dart run swagger_generator_flutter generate --all` 与必要的 `build_runner`
- 参考 [docs/USAGE_GUIDE.md](./docs/USAGE_GUIDE.md) 和 [QUICK_REFERENCE.md](./QUICK_REFERENCE.md) 保持生成规则一致
- 新增能力需补充最小可复现示例或测试
- 生成规则/命名风格如有特殊需求请在 issue 说明并同步更新文档
## 常见问题
- **生成的类型不存在?** 检查 swagger.json 中是否定义了对应的 schema
- **接口缺少参数?** 确认 swagger.json 中是否有完整的参数定义
- **可空性不正确?** 检查 swagger.json 中的 nullable 字段设置
- 更多问题请参考 [快速参考指南](./QUICK_REFERENCE.md)
### 脚本命令说明
#### Linux/macOS (run_swagger.sh)
```bash
# 显示帮助
./run_swagger.sh help
# 只生成数据模型
./run_swagger.sh models
# 只生成API文档
./run_swagger.sh docs
# 只生成Retrofit API
./run_swagger.sh api
```
#### Windows (run_swagger.bat)
```cmd
# 显示帮助
run_swagger.bat help
# 只生成数据模型
run_swagger.bat models
# 只生成API文档
run_swagger.bat docs
# 只生成Retrofit API
run_swagger.bat api
```
### 命令行提示
- 查看所有选项:`dart run swagger_generator_flutter generate --help`
- 旧版 `run_swagger.sh/.bat` 已移除,统一使用 `dart run` 入口
---
更新日期2025-07-13
作者Max
更新日期2025-11-09
作者Max

View File

@ -1,293 +0,0 @@
# ✅ ApiPaths 生成功能移除总结
## 📋 移除目标
移除 `ApiPaths` 文件(`api_paths.dart`)的生成功能,简化代码生成器。
**移除日期**: 2025-11-05
**状态**: ✅ 已完成
---
## ✅ 已移除的内容
### 1. 命令选项
**移除前**:
```dart
const CommandOption(
name: 'endpoints',
shortName: 'e',
description: '生成API端点常量',
type: OptionType.flag,
),
```
**移除后**: ✅ 已删除
### 2. 生成逻辑
**移除前**:
```dart
// 生成端点代码
if (options.generateEndpoints) {
progress('正在生成API端点常量...');
final generator = EndpointCodeGenerator(document);
final code = generator.generate();
final filePath = '$fullOutputDir/api_paths.dart';
await FileUtils.writeFile(filePath, code);
success('API端点常量已保存到: $filePath');
generatedFiles++;
}
```
**移除后**: ✅ 已删除
### 3. 生成选项类
**移除前**:
```dart
class GenerateOptions {
final bool generateEndpoints;
final bool generateModels;
// ...
}
```
**移除后**:
```dart
class GenerateOptions {
final bool generateModels;
final bool generateDocs;
final bool generateApi;
// ...
}
```
### 4. 导入语句
**移除前**:
```dart
import '../generators/endpoint_code_generator.dart';
```
**移除后**: ✅ 已删除
### 5. 配置相关
**移除前**:
```dart
static const String defaultEndpointsFile = 'generated_api_paths.dart';
static const Map<String, dynamic> defaultGenerateOptions = {
'generateEndpoints': true,
// ...
};
```
**移除后**: ✅ 已删除
### 6. 类型验证器
**移除前**:
```dart
enum CodeType { model, endpoints, documentation }
case CodeType.endpoints:
// 验证逻辑
break;
```
**移除后**:
```dart
enum CodeType { model, documentation }
```
### 7. 未使用的方法
**移除**: `_detectApiVersion()` 方法(未使用)
---
## 📁 文件变更清单
### 修改的文件
1. ✅ `lib/commands/generate_command.dart`
- 移除 `--endpoints` 选项
- 移除端点生成逻辑
- 移除 `EndpointCodeGenerator` 导入
- 更新 `GenerateOptions`
- 移除 `_detectApiVersion()` 方法
2. ✅ `lib/core/config.dart`
- 移除 `defaultEndpointsFile` 常量
- 移除 `generateEndpoints` 配置项
3. ✅ `lib/utils/type_validator.dart`
- 移除 `CodeType.endpoints` 枚举值
- 移除相关验证逻辑
4. ✅ `USAGE_AS_DEV_DEPENDENCY.md`
- 移除 `--endpoints` 选项说明
- 移除 `api_paths.dart` 文件结构说明
### 保留的文件(未修改)
- `lib/generators/endpoint_code_generator.dart` - 生成器类文件保留(未使用)
- `lib/utils/string_utils.dart` - `generateEndpointName()` 方法保留(可能被其他功能使用)
---
## 📊 移除效果
### 代码简化
| 指标 | 移除前 | 移除后 | 改善 |
|------|--------|--------|------|
| 命令选项 | 8 个 | 7 个 | ✅ 减少 1 个 |
| 生成选项字段 | 6 个 | 5 个 | ✅ 减少 1 个 |
| 生成逻辑块 | 4 个 | 3 个 | ✅ 减少 1 个 |
| 配置项 | 6 个 | 5 个 | ✅ 减少 1 个 |
### 生成的文件
**移除前**:
```
generator/
├── api_paths.dart ❌ 不再生成
├── api/
├── api_models/
└── api_documentation.md
```
**移除后**:
```
generator/
├── api/
├── api_models/
└── api_documentation.md
```
---
## ✅ 保留的功能
### 完全保留
1. ✅ **API 接口生成** (`--api`)
- Retrofit 风格 API 接口
- 版本化 API 支持
2. ✅ **数据模型生成** (`--models`)
- 请求模型
- 响应模型
- 参数模型
- 枚举类型
3. ✅ **API 文档生成** (`--docs`)
- Markdown 格式文档
- 完整的 API 说明
---
## 📋 命令选项更新
### 移除前
```bash
dart run swagger_generator_flutter generate --endpoints # ❌ 已移除
dart run swagger_generator_flutter generate --models
dart run swagger_generator_flutter generate --api
dart run swagger_generator_flutter generate --docs
```
### 移除后
```bash
dart run swagger_generator_flutter generate --models
dart run swagger_generator_flutter generate --api
dart run swagger_generator_flutter generate --docs
dart run swagger_generator_flutter generate --all # 生成所有(不包含 endpoints
```
---
## 🎯 使用建议
### 替代方案
如果需要 API 路径常量,可以考虑:
1. **使用生成的 API 接口**
- 生成的 Retrofit API 接口已经包含了路径信息
- 路径在 `@GET/@POST/@PUT/@DELETE` 注解中
2. **手动定义常量**
```dart
class ApiPaths {
static const String getUser = '/api/v1/users';
static const String createUser = '/api/v1/users';
}
```
3. **从生成的 API 接口提取**
- 使用代码分析工具从生成的 API 文件中提取路径
---
## ✅ 测试验证
### 测试场景
1. ✅ **生成所有文件** - 正常工作,不生成 `api_paths.dart`
2. ✅ **生成 API** - 正常工作
3. ✅ **生成模型** - 正常工作
4. ✅ **生成文档** - 正常工作
5. ✅ **命令帮助** - 不显示 `--endpoints` 选项
### 测试命令
```bash
cd example/as_dev_dependency
dart run swagger_generator_flutter generate --help
dart run swagger_generator_flutter generate --all
```
**结果**: ✅ 正常工作,不再生成 `api_paths.dart`
---
## 📚 相关文件
### 已修改
1. ✅ `lib/commands/generate_command.dart`
2. ✅ `lib/core/config.dart`
3. ✅ `lib/utils/type_validator.dart`
4. ✅ `USAGE_AS_DEV_DEPENDENCY.md`
### 未修改(保留)
- `lib/generators/endpoint_code_generator.dart` - 生成器类(未使用,可后续删除)
- `lib/utils/string_utils.dart` - `generateEndpointName()` 方法(可能被其他功能使用)
---
## ✨ 总结
**移除完成**:
- ✅ 移除了 `--endpoints` 命令选项
- ✅ 移除了端点生成逻辑
- ✅ 移除了相关配置项
- ✅ 更新了文档
**保留功能**:
- ✅ API 接口生成
- ✅ 数据模型生成
- ✅ API 文档生成
**状态**: ✅ **移除完成,功能正常**
现在代码生成器更加精简,不再生成 `ApiPaths` 文件!

View File

@ -0,0 +1,159 @@
# StringUtils 重构总结
**重构日期**: 2025-11-22
**状态**: ✅ 完成
## 📋 重构目标
根据 `check_list.md` 的要求,对 `lib/utils/string_utils.dart`421 行)进行重构:
- **主要痛点**: 单文件包含命名转换、注释模板、复数化等杂项,并频繁同步读取配置
- **首要行动**: 根据职责拆分(命名转换/注释模板/文本清理),缓存配置项并提供可注入模板服务
## 🎯 重构成果
### 1. 模块化拆分
将原有的 421 行单文件拆分为职责清晰的子模块:
```
lib/utils/string_utils/
├── naming_converter.dart # 命名转换187 行)
├── text_cleaner.dart # 文本清理86 行)
└── template_service.dart # 模板服务86 行)
```
### 2. 主文件重构
`lib/utils/string_utils.dart` 重构为统一导出接口184 行):
- 作为 Facade 模式,聚合各子模块功能
- 保持向后兼容性,所有现有 API 保持不变
- 清晰的功能分组注释
### 3. 各模块职责
#### NamingConverter命名转换
- `toCamelCase()` - 转换为 camelCase
- `toPascalCase()` - 转换为 PascalCase
- `toSnakeCase()` - 转换为 snake_case
- `toConstantCase()` - 转换为 UPPER_SNAKE_CASE
- `toDartPropertyName()` - 转换为 Dart 属性名
- `generateClassName()` - 生成类名
- `generateFileName()` - 生成文件名
- `generateEnumValueName()` - 生成枚举值名称
- `isValidDartIdentifier()` - 验证 Dart 标识符
- `pluralize()` - 单词复数化
#### TextCleaner文本清理
- `cleanDescription()` - 清理描述文本
- `cleanForCode()` - 清理代码标识符
- `escapeString()` - 转义字符串
- `unescapeString()` - 反转义字符串
- `truncate()` - 截断文本
- `normalize()` - 标准化文本(统一换行符和空白)
#### TemplateService模板服务
- `generateComment()` - 生成注释块
- `generateFileHeader()` - 生成文件头(使用 ConfigRepository
- 支持自定义模板变量替换
- 集成配置缓存,避免频繁读取
## 🔧 技术改进
### 1. 配置缓存优化
**之前**: 每次调用都读取配置
```dart
final generatorName = ConfigLoader.getGeneratorName();
final author = ConfigLoader.getAuthor();
final copyright = ConfigLoader.getCopyright();
```
**现在**: 使用 ConfigRepository 实例,支持配置缓存
```dart
final config = ConfigRepository.loadSync();
final generatorName = config.generatorName;
```
### 2. 依赖注入支持
TemplateService 设计为可实例化,支持依赖注入:
```dart
final service = TemplateService();
service.generateFileHeader(description, source);
```
### 3. 单一职责原则
每个子模块专注于单一职责:
- **NamingConverter**: 仅处理命名转换
- **TextCleaner**: 仅处理文本清理
- **TemplateService**: 仅处理模板生成
## ✅ 质量保证
### 1. 代码分析
```bash
dart analyze
```
**结果**:
- ✅ 0 errors
- ✅ 0 warnings
- 62 info仅为代码风格建议
### 2. 测试通过
```bash
dart test
```
**结果**:
- ✅ 所有 203 个测试全部通过
- ✅ 集成测试通过
- ✅ 性能测试通过
### 3. 向后兼容性
- ✅ 所有现有 API 保持不变
- ✅ 现有代码无需修改
- ✅ 导入路径保持一致
## 📦 文件结构
```
lib/utils/
├── string_utils.dart # 统一导出接口184 行)
└── string_utils/
├── naming_converter.dart # 命名转换187 行)
├── text_cleaner.dart # 文本清理86 行)
└── template_service.dart # 模板服务86 行)
```
## 🎉 重构收益
1. **可维护性提升**: 代码按职责清晰分离,易于理解和修改
2. **可测试性提升**: 每个模块可独立测试
3. **可扩展性提升**: 新增功能只需扩展对应模块
4. **性能优化**: 配置缓存减少重复读取
5. **代码复用**: 子模块可独立导入使用
## 📝 使用示例
```dart
// 方式 1: 使用统一接口(推荐,向后兼容)
import 'package:swagger_generator_flutter/utils/string_utils.dart';
final camelCase = StringUtils.toCamelCase('user_name');
final comment = StringUtils.generateComment('API description');
// 方式 2: 直接使用子模块(高级用法)
import 'package:swagger_generator_flutter/utils/string_utils/naming_converter.dart';
import 'package:swagger_generator_flutter/utils/string_utils/text_cleaner.dart';
final className = NamingConverter.generateClassName('user_api');
final cleaned = TextCleaner.cleanDescription('Some <html> text');
```
## ✨ 总结
本次重构成功将 421 行的单一文件拆分为职责清晰的模块化结构,同时保持了完全的向后兼容性。所有测试通过,代码质量显著提升。
**check_list.md 状态**: ✅ 已完成并标记为 `[x]`

132
STRUCTURE_AUDIT.md Normal file
View File

@ -0,0 +1,132 @@
# 目录结构审计报告Dart/Flutter OpenAPI 代码生成器)
最后更新2025-11-22
适用范围lib/**、templates/**、docs/**、test/**、example/**(忽略 build/.dart_tool 等生成产物)
## 一、lib/ 目录结构(第二层深度)
```
lib/
commands/
base_command.dart
generate_command.dart
services/
config/
error_rules.yaml
core/
config.dart
config_repository.dart
error_reporter/ # models / reporter / renderers
error_reporter.dart # 聚合导出
error_rules.dart
exceptions/ # 细分异常定义
exceptions.dart # 聚合导出
models/ # Swagger 核心模型
models.dart # 聚合导出
performance_parser.dart
template/ # TemplateLoader (part of template_renderer)
template_renderer.dart
generators/
base_generator.dart
model/
model_code_generator.dart
retrofit_api/
retrofit_api_generator.dart
parsers/
swagger_data_parser.dart
swagger_fetcher.dart
templates/
api/
common/
models/
utils/
cache_manager.dart
file_utils.dart
logger.dart
path_resolver.dart
performance_monitor.dart
reference_resolver.dart
string_utils.dart # 统一导出
string_utils/ # naming_converter / template_service / text_cleaner
type_validator.dart
validators/
core/
rules/
enhanced_validator.dart
schema_validator.dart
swagger_cli_new.dart
swagger_generator_flutter.dart # 顶层聚合导出
```
关键聚合导出文件:
- lib/swagger_generator_flutter.dart对外公共 API 入口)
- lib/core/error_reporter.dart聚合导出 models/reporter/renderers
- lib/core/models.dart聚合导出核心模型
- lib/utils/string_utils.dart聚合导出 naming_converter/template_service/text_cleaner
## 二、职责边界与潜在问题
- commands参数解析 + 流程编排解析→验证→生成→落盘OK
- config以 ConfigRepository 为主SwaggerConfig 静态 getter 兼容OK
- core通用核心模型/异常/错误报告/模板渲染/并行解析OK
- parsersSwaggerFetcher/SwaggerDataParser获取与解析OK
- validatorsSchemaValidator 基础规则 + EnhancedValidator 装饰增强(依赖 ErrorReporterOK
- generatorsModelCodeGenerator / RetrofitApiGenerator + Mustache 模板OK
- utils通用工具I/O/路径/引用解析/字符串处理OK
- templatesMustache 模板OK
发现与建议:
1) 依赖方向基本自上而下无循环依赖。TemplateLoader 依赖 PathResolver合理。
2) 工具分层已明确string_utils面向生成与 file/path基础设施分离良好。
3) validators 下“增强 vs 基础”是装饰关系,不应合并;已在文档中明确。
4) 顶层导出 swagger_generator_flutter.dart 已聚合核心能力,但 generators/validators/core/utils 的暴露范围建议仅保留聚合导出,避免深层路径泄漏。
## 三、跨层依赖与改进机会
- 配置注入点:
- TemplateRenderer/GenerationOutputService 等处已采用 ConfigRepository.loadSync(),建议命令层集中创建单例并向下传递(可选优化)。
- 模板上下文基线:
- TemplateRenderer._buildBaseContext 固化生成器名称/作者/版权,已统一。
- 输出服务边界:
- GenerationOutputService 已与生成器/文件写入解耦,清晰。
## 四、重复与冗余排查enhanced/improved/v2
- validatorsEnhancedValidator装饰器与 SchemaValidator基础——场景互补保留两者。
- config已迁移至 ConfigRepositoryConfigLoader 已移除docs/MIGRATION_CONFIG_LOADER.md 已提供映射)。
- 其他 v2/Enhanced 字样多为注释/示例,不构成并行实现。
结论无须删除新增模块已冗余项ConfigLoader已处置。
## 五、风险点
- 外部引用深层路径的风险:建议对外仅使用 swagger_generator_flutter.dart 和若干聚合导出;在 USAGE 指南提示。
- 模板根目录查找优先级TemplateLoader 的向上搜集策略需在文档中明确(建议:自定义根 > 配置目录 > 工作目录链)。
## 六、关系示意Mermaid
```mermaid
flowchart TD
CLI[CLI / Command] --> CFG[ConfigRepository]
CLI --> PF[SwaggerFetcher]
PF --> SDP[SwaggerDataParser]
SDP --> VAL[SchemaValidator]
VAL -->|decorate| EV[EnhancedValidator]
EV --> ER[ErrorReporter]
SDP --> MODELS[Core Models]
CLI --> GEN[Generators]
GEN --> MC[ModelCodeGenerator]
GEN --> RG[RetrofitApiGenerator]
RG --> TR[TemplateRenderer]
TR --> TS[TemplateService]
CLI --> OUT[GenerationOutputService]
subgraph Utils
FU[FileUtils]
PR[PathResolver]
RR[ReferenceResolver]
SU[StringUtils]
end
GEN -.-> Utils
SDP -.-> Utils
CLI -.-> Utils
```

View File

@ -0,0 +1,85 @@
# 目录结构迁移步骤清单(方案 B平衡推荐
最后更新2025-11-22
目标:不改变行为与产物,在保持现有分层的基础上,补强聚合导出与依赖边界,减少深层路径依赖。
质量门禁:每一步都需满足
- dart analyze0 errors / 0 warningsinfo 可忽略)
- dart test全部通过
- CLI 行为与生成结果一致(如有差异必须回滚)
回滚策略任一步失败git revert 最近一次提交,恢复到上一步。
---
## 预备
- 建立工作分支feature/structure-governance
- 基线验证:`dart analyze`、`dart test`
## 步骤 1导入路径治理聚合导出优先
- 动作:检查对外导入,尽量使用聚合导出入口
- 外部仅推荐导入:
- `package:swagger_generator_flutter/swagger_generator_flutter.dart`
- `package:swagger_generator_flutter/core/models.dart`
- `package:swagger_generator_flutter/core/error_reporter.dart`
- `package:swagger_generator_flutter/utils/string_utils.dart`
- 验证:`grep -R "lib/core/.*\.dart" example/` 无直接深层导入;构建与示例运行通过
- 提交信息(示例):
- chore(structure): 规范外部导入路径,统一使用聚合导出入口
## 步骤 2为关键子系统补齐/校验聚合导出(如需)
- 动作核对各子系统对外的唯一入口index/聚合文件)
- validators已有 schema/enhanced 两者并存,保持不变(装饰器与基础)
- coreerror_reporter.dart / models.dart 已存在
- utilsstring_utils.dart 已存在
- 验证:对外导入不依赖深层文件;现有单测与示例仍可编译运行
- 提交:
- chore(structure): 校验与补齐聚合导出入口(无行为改动)
## 步骤 3模板上下文基线与模板搜索优先级固化文档
- 动作:在 PROJECT_OVERVIEW.md 增加:
- 仅在 TemplateRenderer 构建一次基础上下文generatorName/author/copyright
- 模板搜索优先级:自定义根 > 配置目录/templates > 配置目录/lib/templates > 工作目录向上搜集
- 验证:无代码变更;生成行为一致
- 提交:
- docs: 明确模板上下文构建点与模板搜索优先级
## 步骤 4可选命令层集中注入 ConfigRepository 单例
- 动作:在 GenerateCommand 执行期创建单个 config 实例,下传到 Renderer/Services当前已通过懒加载避免多次 I/O可暂缓
- 验证:性能对比日志(可选),行为一致
- 提交:
- perf(config): 命令层集中注入单例 ConfigRepository 减少 I/O
## 步骤 5文档与指南同步
- 动作:
- 更新 USAGE_GUIDE.md新增“导入路径规范只用聚合导出”“模板加载优先级”
- 在 README或 QUICK_REFERENCE加入 1 页速览
- 验证:示例按照指南可跑通
- 提交:
- docs: 增补导入路径规范与模板加载优先级
## 验收与合并
- 运行:`dart analyze`、`dart test`
- grep 校验:
- `grep -R "package:swagger_generator_flutter/.*/.*\.dart" example/` 不应出现深层路径
- 提交:
- chore(release): 目录结构治理(方案 B— 聚合导出与导入规范
- PR 检查项:
- 无行为变化;仅结构/文档治理
- 附运行截图或日志
---
## 提交信息模板(中文)
- chore(structure): 规范外部导入路径,统一使用聚合导出入口
- docs: 明确模板上下文构建点与模板搜索优先级
- perf(config): 命令层集中注入单例 ConfigRepository 减少 I/O可选
- chore(release): 目录结构治理(方案 B— 聚合导出与导入规范
## PR 拆分建议
1) docs-only新增/更新 STRUCTURE_* 文档与 PROJECT_OVERVIEW 补充
2) structure-exports聚合导出与导入路径治理不改逻辑
3) perf-config可选命令层注入单例 ConfigRepository
## 回滚策略
- 任一 PR 合并后出现行为偏差:`git revert <commit>` 回滚到上一步
- 保留分支 feature/structure-governance 直到发布后稳定一周

151
STRUCTURE_PROPOSAL.md Normal file
View File

@ -0,0 +1,151 @@
# 目录组织候选方案与推荐方案Dart/Flutter OpenAPI 代码生成器)
最后更新2025-11-22
适用范围lib/**、templates/**、docs/**、test/**、example/**
不改变现有行为与生成结果;仅优化结构与依赖方向。
## 现状简述(基于 STRUCTURE_AUDIT
- 分层清晰commands/config/core/parsers/validators/generators/utils/templates
- 聚合导出swagger_generator_flutter.dart、core/error_reporter.dart、core/models.dart、utils/string_utils.dart
- EnhancedValidator 为装饰器,依赖 SchemaValidatorConfigRepository 为配置主入口
- 主要改进空间:
1) 统一对外“index.dart”聚合导出避免深层路径泄漏
2) 命令层集中注入单个 ConfigRepository 实例以减少 I/O可选
3) 明确模板搜索优先级与上下文基线的构建点
---
## 方案 A保守最小改动快速落地
目标:基本不动现有目录,仅补充聚合导出与文档约束。
建议目录(节选)
```
lib/
core/
models/
models.dart # 已有:聚合导出
error_reporter/
error_reporter.dart# 已有:聚合导出
utils/
string_utils/
string_utils.dart # 已有:聚合导出
swagger_generator_flutter.dart # 对外主入口
```
措施
- 在 docs/USAGE_GUIDE.md 强化“仅从聚合入口导入”的约束
- 在 analysis_options.yaml 添加禁止深层导入的 lint可选
优点
- 变更最小,零风险,立刻可用
缺点
- 不能进一步降低跨层依赖;规范依赖于文档与自觉
影响面
- 对外零影响;测试与 CLI 行为不变
---
## 方案 B平衡推荐完善聚合导出与依赖边界
目标:降低跨层耦合,巩固入口文件,保留现有分层和命名
建议目录(节选)
```
lib/
commands/
base_command.dart
generate_command.dart
services/
document_merge_service.dart
document_filter_service.dart
generation_output_service.dart
core/
config.dart
config_repository.dart
template_renderer.dart
template/
template_loader.dart (part)
models/
models.dart
error_reporter/
error_reporter.dart
exceptions/
exceptions.dart
parsers/
swagger_fetcher.dart
swagger_data_parser.dart
validators/
core/
rules/
schema_validator.dart
enhanced_validator.dart
generators/
base_generator.dart
model/
model_code_generator.dart
retrofit_api/
retrofit_api_generator.dart
utils/
file_utils.dart
path_resolver.dart
reference_resolver.dart
string_utils/
string_utils.dart
templates/
api/
models/
common/
swagger_generator_flutter.dart
```
执行要点
- 为 commands/core/parsers/validators/generators/utils 分别补齐/校验聚合导出(需要时新增 index.dart
- 内部互相依赖仅经聚合文件或上层入口(避免跨层直连深文件)
- TemplateRenderer 中的上下文构建固化(已完成):仅从 ConfigRepository 构建一次
- 命令层GenerateCommand可选集中创建单例 config 并下传
优点
- 依赖边界更清晰;可渐进治理深层导入
- 变更成本适中,兼容性强
缺点
- 需少量导入路径梳理(指向聚合文件)
影响面
- 对外零影响;测试与 CLI 行为不变
---
## 方案 C激进按业务流分区
目标:以 Parse → Validate → Generate → Render → Output 的流水线重组目录
建议目录草案(节选)
```
lib/
pipeline/
parse/ (swagger_fetcher, swagger_data_parser)
validate/ (schema_validator, enhanced_validator, error_reporter)
generate/
models/
apis/
templates/ (renderer, loader, services)
output/ (generation_output_service, file_utils)
core/ (models, exceptions, config_repository)
commands/
utils/
```
优点
- 强业务流程导向;定位问题成本更低
缺点
- 变动大PR 体积大;回滚复杂
影响面
- 需要系统性迁移与长时间稳定验证
---
## 推荐方案
- 采纳方案 B平衡
- 当前分层已合理,主要补强聚合导出与导入规范
- 最小代价减少未来跨层依赖与“深层路径”侵蚀
---
## 对测试与命令行为的影响
- 三个方案均“不改变行为”;仅目录/导入治理
- 质量门禁:每步迁移后确保 `dart analyze` 0 error / 0 warning、`dart test` 全绿

View File

@ -339,7 +339,6 @@ jobs:
## 📚 更多资源
- [完整文档](docs/USAGE_GUIDE.md)
- [API 参考](docs/API_REFERENCE.md)
- [项目概览](docs/PROJECT_OVERVIEW.md)
- [示例项目](example/)

View File

@ -1,77 +1,19 @@
# 1. 继承 Lint 规则集 (必选其一)
# --------------------------------------------------------------------------
# 强烈推荐!根据你的项目类型选择一个 Lint 规则集作为起点。
# 这能大大减少手动配置的工作量,并与社区最佳实践保持一致。
# Linter 规则
# https://dart.ac.cn/tools/linter-rules
# 如果是 Flutter 项目,推荐使用:
include: package:flutter_lints/flutter.yaml
# 如果是纯 Dart 项目,推荐使用:
# include: package:lints/recommended.yaml
# 2. 配置分析器 (必选)
# --------------------------------------------------------------------------
# 分析器用于检查代码的语法和潜在问题。
# 强烈建议启用所有规则,以确保代码质量和一致性。
include: package:very_good_analysis/analysis_options.yaml
analyzer:
errors:
require_trailing_commas: ignore
# 排除不想被分析的文件或目录。
# 对于由代码生成工具(如 json_serializable, freezed生成的 *.g.dart 文件,
# 强烈建议排除,因为你通常不需要对它们进行 Lint 检查。
exclude:
- '**/*.g.dart' # 排除所有以 .g.dart 结尾的文件
- 'lib/generated/**' # 排除 lib/generated/ 目录下的所有文件 (如果你的生成文件都在这里)
- 'build/**' # 排除 Flutter/Dart 构建输出目录
# 排除所有生成的文件
- "**/*.g.dart"
- "**/*.freezed.dart"
- "**/test/**"
# 如果还有其他生成文件,也可以添加
# - "**/*.gr.dart" # auto_route 生成的文件
# - "**/*.config.dart" # injectable 生成的文件
# 3. 配置 Lint 规则
# --------------------------------------------------------------------------
linter:
# 在此处启用或禁用特定的 Lint 规则。
# `include` 中的规则集已经包含了大部分常用规则,这里可以进行微调。
rules:
# 常用且推荐启用的规则 (即使默认集没有包含,也建议手动添加)
- avoid_empty_else # 避免空的 else 块
# - avoid_print # 在生产代码中避免使用 print (可根据项目需求启用/禁用)
- avoid_relative_lib_imports # 避免从 'lib/' 相对导入
- directives_ordering # 强制 import/export 指令排序
# - avoid_return_and_type_annotation # 避免冗余的返回类型注解
- curly_braces_in_flow_control_structures # 控制流语句强制使用大括号
- empty_catches # 避免空的 catch 块
- empty_constructor_bodies # 避免空的构造函数体
- empty_statements # 避免空的语句
- file_names # 文件名使用小写下划线命名 (my_file.dart)
# - prefer_const_constructors # 尽可能使用 const 构造函数
# - prefer_const_declarations # 尽可能使用 const 声明
# - prefer_const_literals_to_create_immutables # 尽可能使用 const 创建不可变集合
# - prefer_single_quotes # 优先使用单引号 (或 prefer_double_quotes)
- prefer_final_fields # 类中的私有字段尽可能使用 final
- prefer_final_locals # 局部变量尽可能使用 final
# - prefer_for_elements_to_map_fromIterable # 优先使用 for 元素创建 Map
# - prefer_is_empty # 优先使用 .isEmpty
# - prefer_is_not_empty # 优先使用 .isNotEmpty
- unnecessary_new # Dart 2.0 后 new 关键字是可选的,推荐省略
- unnecessary_this # 避免不必要的 this 关键字
# - use_key_in_widget_constructors # Flutter Widget 构造函数中推荐使用 Key
# 关闭强制文档注释 (很多业务开发觉得这条太累赘)
public_member_api_docs: false
# 根据项目特性考虑启用的规则 (可能需要团队讨论)
# - annotate_overrides # 推荐:覆写方法添加 @override 注解 (如果 flutter_lints 已包含则无需重复)
# - lines_longer_than_80_chars # 强制行长 80 字符 (默认是警告,但通常较严格)
# - public_member_api_docs # 推荐:为公共 API 编写文档注释 (对库项目非常重要,应用项目可酌情)
- require_trailing_commas # 强制多行参数列表使用尾随逗号 (有助于格式化)
# - sort_constructors_first # 构造函数在类中声明在前
# - sort_declarations_as_members # 类成员按字母顺序排序
# - sort_pub_dependencies # pubspec.yaml 依赖按字母排序
# 4. 格式化器配置
# --------------------------------------------------------------------------
formatter:
# 设置 `dart format` 工具的行宽。
# Dart 官方推荐 80但许多团队会使用 100 或 120 以适应现代宽屏显示器。
# 最重要的是整个团队**保持一致**。
page_width: 80
# 可选:如果你不喜欢强制构造函数必须写在最前面,也可以关掉
# sort_constructors_first: false

View File

@ -2,7 +2,9 @@
import 'dart:io';
import 'package:logging/logging.dart';
import 'package:swagger_generator_flutter/swagger_cli_new.dart';
import 'package:swagger_generator_flutter/utils/logger.dart';
/// Swagger CLI
///
@ -14,28 +16,31 @@ import 'package:swagger_generator_flutter/swagger_cli_new.dart';
/// -
///
/// 使
/// dart run swagger_cli <command> [options]
/// `dart run swagger_cli <command> [options]`
///
///
/// - generate:
/// - help:
/// - version:
Future<void> main(List<String> arguments) async {
setupLogging(level: Level.ALL);
//
if (arguments.isEmpty) {
var resolvedArgs = arguments;
if (resolvedArgs.isEmpty) {
_showWelcome();
arguments = ['help'];
resolvedArgs = ['help'];
}
//
if (arguments.contains('--version') || arguments.contains('-v')) {
if (resolvedArgs.contains('--version') || resolvedArgs.contains('-v')) {
_showVersion();
return;
}
// 使CLI
final cli = SwaggerCLI();
final exitCode = await cli.run(arguments);
final exitCode = await cli.run(resolvedArgs);
// 退
exit(exitCode);
@ -43,40 +48,32 @@ Future<void> main(List<String> arguments) async {
///
void _showWelcome() {
print('');
print('🚀 欢迎使用 Swagger CLI 工具!');
print('');
print('这是一个强大的 Swagger API 代码生成工具,可以帮助您:');
print('');
print(' 📋 解析 Swagger/OpenAPI 文档');
print(' 🛠️ 生成 Dart 模型类');
print(' 📡 生成 API 端点常量');
print(' 📚 生成完整的 API 文档');
print(' 🔒 提供类型安全的代码生成');
print('');
print('使用 --help 查看详细帮助信息');
print('');
appLogger
..info('🚀 欢迎使用 Swagger CLI 工具!')
..info('这是一个强大的 Swagger API 代码生成工具,可以帮助您:')
..info(' 📋 解析 Swagger/OpenAPI 文档')
..info(' 🛠️ 生成 Dart 模型类')
..info(' 📡 生成 API 端点常量')
..info(' 📚 生成完整的 API 文档')
..info(' 🔒 提供类型安全的代码生成')
..info('使用 --help 查看详细帮助信息');
}
///
void _showVersion() {
print('');
print('🚀 Swagger CLI 工具 v2.0.0');
print('');
print('构建信息:');
print(' - Dart SDK: ${Platform.version}');
print(' - 平台: ${Platform.operatingSystem}');
print(' - 架构: ${Platform.version}');
print('');
print('特性:');
print(' ✨ 现代化的命令行界面');
print(' 🏗️ 模块化架构设计');
print(' 🚀 高性能代码生成');
print(' 🔍 智能类型验证');
print(' 📊 性能监控和分析');
print(' 💾 智能缓存机制');
print(' 📝 丰富的文档生成');
print('');
print('更多信息请访问: https://github.com/yourorg/swagger_cli');
print('');
appLogger
..info('🚀 Swagger CLI 工具 v2.0.0')
..info('构建信息:')
..info(' - Dart SDK: ${Platform.version}')
..info(' - 平台: ${Platform.operatingSystem}')
..info(' - 架构: ${Platform.version}')
..info('特性:')
..info(' ✨ 现代化的命令行界面')
..info(' 🏗️ 模块化架构设计')
..info(' 🚀 高性能代码生成')
..info(' 🔍 智能类型验证')
..info(' 📊 性能监控和分析')
..info(' 💾 智能缓存机制')
..info(' 📝 丰富的文档生成')
..info('更多信息请访问: https://github.com/yourorg/swagger_cli');
}

View File

@ -1,544 +0,0 @@
# API 参考文档
## 📚 核心 API 类库
### 🔧 解析器 (Parsers)
#### PerformanceParser
高性能 OpenAPI 文档解析器,支持并行处理和性能监控。
```dart
import 'package:swagger_generator_flutter/swagger_generator_flutter.dart';
// 创建解析器
final parser = PerformanceParser(
config: ParseConfig(
enablePerformanceStats: true, // 启用性能统计
enableParallelParsing: true, // 启用并行解析
enableCaching: true, // 启用缓存
maxConcurrency: 4, // 最大并发数
enableMemoryOptimization: true, // 内存优化
),
);
// 解析文档
final jsonString = await File('swagger.json').readAsString();
final document = await parser.parseDocument(jsonString);
// 获取性能统计
final stats = parser.lastStats;
print('解析时间: ${stats?.totalTime.inMilliseconds}ms');
print('路径数量: ${stats?.pathCount}');
print('吞吐量: ${stats?.bytesPerSecond.toStringAsFixed(2)} bytes/s');
```
**主要方法:**
| 方法 | 描述 | 返回类型 |
|------|------|----------|
| `parseDocument(String jsonString)` | 解析 OpenAPI 文档 | `Future<SwaggerDocument>` |
| `parseDocumentFromFile(String filePath)` | 从文件解析文档 | `Future<SwaggerDocument>` |
| `validateAndParse(String jsonString)` | 验证并解析文档 | `Future<SwaggerDocument>` |
**配置选项:**
```dart
class ParseConfig {
final bool enablePerformanceStats; // 启用性能统计
final bool enableParallelParsing; // 启用并行解析
final bool enableStreamParsing; // 启用流式解析
final bool enableCaching; // 启用缓存
final int maxConcurrency; // 最大并发数
final bool enableMemoryOptimization; // 内存优化
final Duration cacheTimeout; // 缓存超时时间
}
```
---
### 🏭 生成器 (Generators)
#### OptimizedRetrofitGenerator
优化的 Retrofit API 代码生成器,专为企业级项目设计。
```dart
import 'package:swagger_generator_flutter/swagger_generator_flutter.dart';
// 创建生成器
final generator = OptimizedRetrofitGenerator(
className: 'ApiService', // API 服务类名
generateModularApis: true, // 生成模块化 API
generateBaseResult: true, // 生成基础响应类型
generatePagination: true, // 生成分页支持
generateFileUpload: true, // 生成文件上传支持
baseResultType: 'BaseResult', // 基础响应类型名
pageResultType: 'BasePageResult', // 分页响应类型名
);
// 生成代码
final generatedCode = generator.generateFromDocument(document);
// 保存到文件
await File('lib/api/api_service.dart').writeAsString(generatedCode);
```
**主要方法:**
| 方法 | 描述 | 返回类型 |
|------|------|----------|
| `generateFromDocument(SwaggerDocument doc)` | 从文档生成代码 | `String` |
| `generateModularApis(SwaggerDocument doc)` | 生成模块化 API | `Map<String, String>` |
| `generateModels(SwaggerDocument doc)` | 生成数据模型 | `Map<String, String>` |
| `generateUtils(SwaggerDocument doc)` | 生成工具类 | `String` |
**配置选项:**
```dart
class OptimizedRetrofitGenerator {
final String className; // 生成的类名
final bool generateModularApis; // 是否生成模块化 API
final bool generateBaseResult; // 是否生成基础响应类型
final bool generatePagination; // 是否生成分页支持
final bool generateFileUpload; // 是否生成文件上传支持
final String baseResultType; // 基础响应类型名
final String pageResultType; // 分页响应类型名
final List<String> excludeTags; // 排除的标签
final Map<String, String> typeMapping; // 类型映射
}
```
#### PerformanceGenerator
高性能代码生成器,支持并发生成和增量更新。
```dart
final generator = PerformanceGenerator(
maxConcurrency: 4, // 最大并发数
enableCaching: true, // 启用缓存
enableIncremental: true, // 启用增量生成
enableParallel: true, // 启用并行生成
cacheStrategy: CacheStrategy.smart, // 缓存策略
);
// 并行生成多个文件
final results = await generator.generateParallel(document, [
GenerationTask.apis,
GenerationTask.models,
GenerationTask.utils,
]);
```
---
### ✅ 验证器 (Validators)
#### EnhancedValidator
增强型文档验证器,提供详细的错误报告和修复建议。
```dart
import 'package:swagger_generator_flutter/swagger_generator_flutter.dart';
// 创建验证器
final validator = EnhancedValidator(
strictMode: false, // 严格模式
includeWarnings: true, // 包含警告
enableAutoFix: true, // 启用自动修复
customRules: [ // 自定义验证规则
RequiredFieldRule(),
NamingConventionRule(),
],
);
// 验证文档
final isValid = validator.validateDocument(document);
// 获取错误报告
final errors = validator.errorReporter.getErrorsBySeverity(ErrorSeverity.error);
final warnings = validator.errorReporter.getErrorsBySeverity(ErrorSeverity.warning);
// 生成详细报告
final report = validator.errorReporter.generateReport();
print(report);
```
**错误级别:**
```dart
enum ErrorSeverity {
critical, // 严重错误,阻止生成
error, // 错误,可能影响生成质量
warning, // 警告,建议修复
info, // 信息,仅供参考
}
```
**内置验证规则:**
| 规则 | 描述 | 级别 |
|------|------|------|
| `SchemaValidationRule` | Schema 定义验证 | Error |
| `ReferenceValidationRule` | 引用完整性验证 | Critical |
| `NamingConventionRule` | 命名规范验证 | Warning |
| `TypeConsistencyRule` | 类型一致性验证 | Error |
| `RequiredFieldRule` | 必填字段验证 | Warning |
---
### 🗄️ 缓存管理 (Cache)
#### SmartCache
智能缓存管理器,支持多级缓存和自动清理。
```dart
import 'package:swagger_generator_flutter/swagger_generator_flutter.dart';
// 创建缓存
final cache = SmartCache<String>(
maxSize: 1000, // 最大缓存大小
strategy: CacheStrategy.smart, // 缓存策略
defaultTtl: Duration(hours: 1), // 默认过期时间
enablePersistence: true, // 启用持久化
);
// 使用缓存
cache.put('key', 'value', ttl: Duration(minutes: 30));
final value = cache.get('key');
// 获取统计信息
final stats = cache.getStats();
print('缓存命中率: ${(stats.hitRate * 100).toStringAsFixed(1)}%');
print('内存使用: ${stats.memoryUsage}');
```
**缓存策略:**
```dart
enum CacheStrategy {
lru, // LRU (最近最少使用)
lfu, // LFU (最少使用频率)
fifo, // FIFO (先进先出)
smart, // 智能策略 (结合多种算法)
}
```
---
### 🔧 工具类 (Utils)
#### StringUtils
字符串处理工具类,提供命名转换和格式化功能。
```dart
import 'package:swagger_generator_flutter/swagger_generator_flutter.dart';
// 命名转换
final camelCase = StringUtils.toCamelCase('user_name'); // userName
final pascalCase = StringUtils.toPascalCase('user_name'); // UserName
final snakeCase = StringUtils.toSnakeCase('userName'); // user_name
// 类型转换
final dartType = StringUtils.openApiTypeToDart('integer'); // int
final nullableType = StringUtils.makeNullable('String'); // String?
// 文档注释生成
final comment = StringUtils.generateDocComment(
'User login endpoint',
parameters: ['username', 'password'],
returns: 'LoginResult',
);
```
#### FileUtils
文件操作工具类,提供安全的文件读写功能。
```dart
import 'package:swagger_generator_flutter/swagger_generator_flutter.dart';
// 安全写入文件
await FileUtils.writeStringToFile(
'lib/api/generated_api.dart',
generatedCode,
createDirs: true, // 自动创建目录
backup: true, // 创建备份
);
// 批量写入文件
await FileUtils.writeMultipleFiles({
'lib/api/user_api.dart': userApiCode,
'lib/api/order_api.dart': orderApiCode,
'lib/models/user.dart': userModelCode,
});
// 清理生成的文件
await FileUtils.cleanGeneratedFiles('lib/api/generated/');
```
---
### 📊 性能监控 (Performance)
#### PerformanceMonitor
性能监控器,提供详细的性能统计和分析。
```dart
import 'package:swagger_generator_flutter/swagger_generator_flutter.dart';
// 创建监控器
final monitor = PerformanceMonitor();
// 开始监控
monitor.startOperation('parse_document');
// ... 执行操作
monitor.endOperation('parse_document');
// 获取统计信息
final stats = monitor.getOperationStats('parse_document');
print('操作次数: ${stats.count}');
print('平均耗时: ${stats.averageTime.inMilliseconds}ms');
print('最大耗时: ${stats.maxTime.inMilliseconds}ms');
// 生成性能报告
final report = monitor.generateReport();
print(report);
```
---
## 🔄 完整使用流程
### 基本使用流程
```dart
import 'dart:io';
import 'package:swagger_generator_flutter/swagger_generator_flutter.dart';
Future<void> generateApiCode() async {
// 1. 创建解析器
final parser = PerformanceParser(
config: ParseConfig(
enablePerformanceStats: true,
enableCaching: true,
),
);
// 2. 创建验证器
final validator = EnhancedValidator(
includeWarnings: true,
);
// 3. 创建生成器
final generator = OptimizedRetrofitGenerator(
className: 'ApiService',
generateModularApis: true,
generateBaseResult: true,
);
try {
// 4. 解析文档
final jsonString = await File('swagger.json').readAsString();
final document = await parser.parseDocument(jsonString);
// 5. 验证文档
final isValid = validator.validateDocument(document);
if (!isValid) {
final report = validator.errorReporter.generateReport();
print('验证失败:\n$report');
return;
}
// 6. 生成代码
final generatedCode = generator.generateFromDocument(document);
// 7. 保存文件
await File('lib/api/api_service.dart').writeAsString(generatedCode);
print('✅ 代码生成完成!');
// 8. 显示性能统计
final stats = parser.lastStats;
if (stats != null) {
print('解析时间: ${stats.totalTime.inMilliseconds}ms');
print('生成的路径数: ${stats.pathCount}');
}
} catch (e, stackTrace) {
print('❌ 生成失败: $e');
print('堆栈跟踪: $stackTrace');
}
}
```
### 高级使用流程 (企业级)
```dart
Future<void> generateEnterpriseApiCode() async {
// 1. 配置高性能解析器
final parser = PerformanceParser(
config: ParseConfig(
enablePerformanceStats: true,
enableParallelParsing: true,
enableCaching: true,
maxConcurrency: 8,
enableMemoryOptimization: true,
),
);
// 2. 配置增强验证器
final validator = EnhancedValidator(
strictMode: true,
includeWarnings: true,
enableAutoFix: true,
customRules: [
RequiredFieldRule(),
NamingConventionRule(),
TypeConsistencyRule(),
],
);
// 3. 配置性能生成器
final generator = PerformanceGenerator(
maxConcurrency: 4,
enableCaching: true,
enableIncremental: true,
enableParallel: true,
);
// 4. 配置智能缓存
final cache = SmartCache<SwaggerDocument>(
maxSize: 100,
strategy: CacheStrategy.smart,
defaultTtl: Duration(hours: 2),
);
try {
// 5. 解析和缓存文档
final cacheKey = 'swagger_document_v1';
var document = cache.get(cacheKey);
if (document == null) {
final jsonString = await File('swagger.json').readAsString();
document = await parser.parseDocument(jsonString);
cache.put(cacheKey, document);
}
// 6. 验证文档
final isValid = validator.validateDocument(document);
if (!isValid) {
final errors = validator.errorReporter
.getErrorsBySeverity(ErrorSeverity.critical);
if (errors.isNotEmpty) {
throw Exception('文档包含严重错误,无法继续生成');
}
}
// 7. 并行生成多个文件
final results = await generator.generateParallel(document, [
GenerationTask.apis,
GenerationTask.models,
GenerationTask.utils,
GenerationTask.documentation,
]);
// 8. 保存生成的文件
for (final entry in results.entries) {
final filePath = 'lib/api/generated/${entry.key}.dart';
await FileUtils.writeStringToFile(
filePath,
entry.value,
createDirs: true,
backup: true,
);
}
print('✅ 企业级代码生成完成!');
// 9. 生成性能报告
final performanceReport = parser.generatePerformanceReport();
await File('reports/performance_report.md')
.writeAsString(performanceReport);
// 10. 生成验证报告
final validationReport = validator.errorReporter.generateReport();
await File('reports/validation_report.md')
.writeAsString(validationReport);
} catch (e, stackTrace) {
print('❌ 企业级生成失败: $e');
print('堆栈跟踪: $stackTrace');
}
}
```
---
## 🔍 错误处理和调试
### 常见错误类型
```dart
// 解析错误
try {
final document = await parser.parseDocument(jsonString);
} on SwaggerParseException catch (e) {
print('解析错误: ${e.message}');
print('错误位置: ${e.location}');
print('修复建议: ${e.suggestion}');
}
// 验证错误
try {
final isValid = validator.validateDocument(document);
} on ValidationException catch (e) {
print('验证错误: ${e.message}');
print('错误字段: ${e.fieldPath}');
print('期望值: ${e.expectedValue}');
print('实际值: ${e.actualValue}');
}
// 生成错误
try {
final code = generator.generateFromDocument(document);
} on CodeGenerationException catch (e) {
print('生成错误: ${e.message}');
print('错误类型: ${e.errorType}');
print('相关对象: ${e.relatedObject}');
}
```
### 调试工具
```dart
// 启用调试模式
final parser = PerformanceParser(
config: ParseConfig(
enableDebugMode: true, // 启用调试模式
enableVerboseLogging: true, // 详细日志
logLevel: LogLevel.debug, // 日志级别
),
);
// 性能分析
final profiler = PerformanceProfiler();
profiler.startProfiling();
// ... 执行操作
final profile = profiler.endProfiling();
print('性能分析: ${profile.summary}');
// 内存分析
final memoryAnalyzer = MemoryAnalyzer();
final usage = memoryAnalyzer.getCurrentUsage();
print('内存使用: ${usage.totalMemory}MB');
print('缓存占用: ${usage.cacheMemory}MB');
```
---
**文档版本**: v2.0
**最后更新**: 2025-01-24
**维护者**: Max

View File

@ -0,0 +1,71 @@
# ConfigLoader → ConfigRepository 迁移指南
状态: 迁移完成ConfigLoader 已软弃用,待最终移除)
最后更新: 2025-11-22
## 背景
旧版使用静态类 ConfigLoader 读取和暴露配置。新版引入 ConfigRepository实例提供只读视图和更清晰的 API同时支持同步/异步加载与内部缓存。
本仓库已完成全量迁移并删除 ConfigLoader以下为映射关系与示例便于外部或下游项目迁移。
## API 映射表
- getFileHeaderTemplate() → config.fileHeaderTemplate
- getGeneratorName() → config.generatorName
- getAuthor() → config.author
- getCopyright() → config.copyright
- getBaseDir() → config.baseDir
- getApiDir() → config.apiDir
- getModelsDir() → config.modelsDir
- getVersionExtractionPattern() → config.versionExtractionPattern
- getDefaultVersion() → config.defaultVersion
- getBaseResultImport() → config.baseResultImport
- getBasePageResultImport() → config.basePageResultImport
- getApiClientClassName() → config.apiClientClassName
- getApiClientFileName() → config.apiClientFileName
- getIncludedTags() → config.includedTags
- getExcludedTags() → config.excludedTags
- getSplitByTags() → config.splitByTags
- getPackageImports() → config.packageImports
- shouldSkipFile(path) → config.shouldSkipFile(path)
- getIgnoredDirectories() → config.ignoredDirectories
- getIgnoredFiles() → config.ignoredFiles
- getSwaggerUrls() → config.swaggerUrls
其中 config 是:
```dart
final config = ConfigRepository.loadSync();
// 或
final config = await ConfigRepository.load();
```
## 代码改造示例
Before:
```dart
final name = ConfigLoader.getGeneratorName();
final apiDir = ConfigLoader.getApiDir();
if (!ConfigLoader.shouldSkipFile(filePath)) { /* ... */ }
```
After:
```dart
final config = ConfigRepository.loadSync();
final name = config.generatorName;
final apiDir = config.apiDir;
if (!config.shouldSkipFile(filePath)) { /* ... */ }
```
## 性能建议
- 在同一执行流程中,尽量复用单个 ConfigRepository 实例,避免重复磁盘 IO。
- 可在构造函数中注入或使用懒加载 + 静态缓存(见 GenerationOutputService 中的 _config 实现)。
## 向后兼容
- 当前仓库已删除 ConfigLoader如你的项目仍依赖它请参照映射表替换为 ConfigRepository并运行 analyze/test 确认。
## 验证
- 迁移完成后,应确保:
- dart analyze: 0 errors / 0 warningsinfo 忽略)
- dart test: 全部通过
- grep "ConfigLoader" 在源代码与测试中均无匹配

View File

@ -16,90 +16,145 @@ XY Swagger Generator 是一个专为 Flutter 开发优化的 OpenAPI 3.0 代码
### 架构层次
```
┌─────────────────────────────────────────────────┐
│ 用户接口层 │
├─────────────────────────────────────────────────┤
│ 命令行工具 (CLI) │
├─────────────────────────────────────────────────┤
│ 生成器层 │
│ ┌─────────────┬─────────────┬─────────────┐ │
│ │ 基础 │ 优化 │ 性能 │ │
│ │ 生成器 │ 生成器 │ 生成器 │ │
│ └─────────────┴─────────────┴─────────────┘ │
├─────────────────────────────────────────────────┤
│ 验证层 │
│ ┌─────────────┬─────────────────────────────┐ │
│ │ Schema │ Enhanced │ │
│ │ Validator │ Validator │ │
│ └─────────────┴─────────────────────────────┘ │
├─────────────────────────────────────────────────┤
│ 解析层 │
│ ┌─────────────┬─────────────────────────────┐ │
│ │ Swagger │ Performance │ │
│ │ Parser │ Parser │ │
│ └─────────────┴─────────────────────────────┘ │
├─────────────────────────────────────────────────┤
│ 核心层 │
│ ┌─────────────┬─────────────┬─────────────┐ │
│ │ Models │ Cache │ Utils │ │
│ └─────────────┴─────────────┴─────────────┘ │
└─────────────────────────────────────────────────┘
命令行输入
SwaggerCLI / GenerateCommand编排流程
Pipeline (按职责分层)
- Parse: SwaggerDataParser (获取与解析)
- Validate: EnhancedValidator (校验)
- Generate: ModelCodeGenerator, RetrofitApiGenerator (生成)
- Render: TemplateRenderer (渲染)
- Output: GenerationOutputService (落盘)
Core (核心模型、配置、异常)
Utils (通用工具)
```
### 模块关系图(简化)
```mermaid
flowchart TD
CLI[CLI / main] --> GC[GenerateCommand]
GC --> Pipeline
subgraph Pipeline
direction LR
Parse[Parse] --> Validate[Validate]
Validate --> Generate[Generate]
Generate --> Render[Render]
Render --> Output[Output]
end
GC --> Core[Core Models, Config, Exceptions]
Pipeline --> Core
Pipeline --> Utils
```
## 模块职责与核心类
- Commands
- GenerateCommand: 解析参数、编排流程(解析→验证→生成→落盘)
- Pipeline
- Parse: `SwaggerDataParser` (获取与解析)
- Validate: `EnhancedValidator` (校验)
- Generate: `ModelCodeGenerator`, `RetrofitApiGenerator` (生成)
- Render: `TemplateRenderer` (渲染)
- Output: `GenerationOutputService` (落盘)
- Core
- `ConfigRepository`: 主配置入口
- `models`: 核心数据模型
- `exceptions`: 自定义异常
- `error_reporter`: 错误报告
- Utils
- `FileUtils`/`PathResolver`: 异步 IO、路径解析
- `ReferenceResolver`: $ref 解析与去循环
- `StringUtils`: 统一导出NamingConverter/TextCleaner/TemplateService
- Utils
- FileUtils/PathResolver: 异步 IO、路径解析
- ReferenceResolver: $ref 解析与去循环
- StringUtils: 统一导出NamingConverter/TextCleaner/TemplateService
## 代码生成流程(详细)
1) 读取配置
- 首选 ConfigRepository.loadSync()/load(),兼容 ConfigLoader
2) 获取与解析
- SwaggerFetcher 读取(网络/本地)→ CacheManager 命中 → SwaggerDataParser 解析
3) 校验
- SchemaValidator 执行基础校验 → EnhancedValidator 转换为结构化报告ErrorReporter
4) 数据准备
- 引用解析、模型依赖裁剪、版本/Tag 分组
5) 代码生成
- ModelCodeGenerator 产出 models
- RetrofitApiGenerator 产出 APITemplateRenderer + 模板)
6) 文件输出
- GenerationOutputService/ FileUtils 落盘,按版本/分类组织
7) 总结输出
- 生成 SUMMARY.md、日志摘要、耗时统计
## 最近重构变更(摘自 check_list
- 核心模型拆分为 models/ 子模块,路径支持 path+method 键,补齐 toJson
- GenerateCommand 拆出输出服务与调度,职责更清晰
- RetrofitApiGenerator 切换 Mustache 模板
- Validator 体系化ValidationRule/ContextEnhancedValidator 装饰器化
- 引入 ConfigRepositoryPathResolver 复用路径逻辑
- TypeValidator 规则化,复用 SchemaValidator 结果模型
- SwaggerFetcher 异步 IO + 内容哈希缓存
- FileUtils 全异步 API统一 PathResolver
- performance_parser 使用 Isolate.run 实现并行
- error_rules 迁移至 YAML/JSON 配置
- exceptions 拆分为 exceptions/ 子目录
- error_reporter 拆分为 data/reporter/rendererserror_reporter.dart 仅作为汇总导出
- StringUtils 拆分为 naming_converter/text_cleaner/template_service主文件为统一导出接口
### 核心组件
#### 1. 解析器 (Parsers)
- **SwaggerDataParser**: 基础 OpenAPI 文档解析
- **PerformanceParser**: 高性能解析器,支持并行处理和流式解析
#### 1. 命令与配置
- **SwaggerCLI / GenerateCommand**: 注册命令、展示帮助、执行生成,支持多 Swagger 合并、版本化输出、Tag 过滤和忽略列表
- **ConfigLoader / SwaggerConfig**: 解析 `generator_config.yaml`,提供 swagger_urls 顺序合并、输出目录、版本提取正则、ApiClient 命名、BaseResult 导入等配置
#### 2. 验证器 (Validators)
- **SchemaValidator**: 基础 Schema 验证
- **EnhancedValidator**: 增强验证器,提供详细的错误报告
#### 2. Pipeline (核心流程)
- **Parse**: `SwaggerDataParser` 支持 http(s) 与 file:// 源的 OpenAPI 解析,内置缓存与性能监测。
- **Validate**: `SchemaValidator``EnhancedValidator` 用于在生成前验证文档一致性。
- **Generate**: `ModelCodeGenerator``RetrofitApiGenerator` 负责生成数据模型和 API 代码。
- **Render**: `TemplateRenderer` (内部使用) 负责模板渲染。
- **Output**: `GenerationOutputService` 负责将生成的文件写入磁盘。
#### 3. 生成器 (Generators)
- **RetrofitApiGenerator**: 基础 Retrofit API 生成器
- **OptimizedRetrofitGenerator**: 优化版生成器,支持模块化和企业级特性
- **PerformanceGenerator**: 高性能生成器,支持并发和缓存
#### 4. 工具类 (Utils)
- **SmartCache**: 智能缓存管理
- **FileUtils**: 文件操作工具
- **StringUtils**: 字符串处理工具
- **TypeValidator**: 类型验证工具
#### 5. 工具类 (Utils)
- **CacheManager / PerformanceMonitor**: 缓存解析结果并记录耗时
- **FileUtils / StringUtils**: 路径解析(基于配置文件目录)、命名转换、文件写入等
## 🔧 技术特性
### 性能优化
- **并行解析**: 支持多线程解析大型 API 文档
- **智能缓存**: 基于 LRU 算法的多级缓存机制
- **增量生成**: 只更新变更的部分,避免全量重新生成
- **内存优化**: 流式处理,降低内存占用
### 生成行为
- 支持按顺序合并多个 `swagger_urls`,后者覆盖前者(适合 v1→v2 升级)
- 版本化输出路径按版本分目录v2+ 类名自动追加 `V2`/`V3` 后缀,统一 ApiClient 聚合各版本
- Tag 过滤与模型裁剪:`included_tags`/`excluded_tags` 与依赖分析确保只生成实际使用的模型
- 分页识别:检测 `total/items` 模式并替换为 `BasePageResult<T>`,包装在 `BaseResult`
- 查询参数实体GET 查询参数超过 4 个时自动生成 parameters 类,集中导出
- 忽略列表与相对路径:`ignored_directories/files` 控制落盘,路径基于配置文件所在目录解析
- BaseResult/BasePageResult 导入可配置,模型导出自动补全统一的 index.dart
### 代码质量
- **严格类型检查**: 基于 OpenAPI Schema 的强类型生成
- **代码规范**: 统一的命名规范和代码风格
- **错误处理**: 详细的错误诊断和修复建议
- **测试覆盖**: 完整的单元测试和集成测试
### 质量保障
- 模型使用 `@JsonSerializable(checked: true, includeIfNull: false)`,字符串/非空列表默认值自动补齐(响应模型)
- Request/Response/Enums/Parameters 分类生成,分页模型避免重复定义
- 生成器提供基本语法校验与类型推断,保持 Dart 命名规范(类名 PascalCase字段 camelCase
- 文档生成器输出接口统计、控制器分组与示例,辅助对齐后端文档
### 企业级特性
- **配置管理**: 灵活的配置选项和预设模板
- **版本控制**: 支持 API 版本管理和向后兼容性
- **监控统计**: 详细的性能统计和生成报告
- **扩展性**: 插件化架构,支持自定义扩展
### 性能与观测
- CacheManager 缓存解析结果,避免重复请求 Swagger 源
- PerformanceMonitor 记录获取/解析耗时,生成流程有摘要 (SUMMARY.md)
- 文件写入前统一检查跳过策略,目录按需创建,减少无效 IO
## 📊 性能指标
### 解析性能
- **大型文档**: 支持 10MB+ 的 OpenAPI 文档
- **解析速度**: 平均 1000+ paths/second
- **内存效率**: 流式处理,内存占用 < 100MB
- **并发支持**: 最大 8 个并发解析任务
### 生成性能
- **代码生成**: 平均 500+ endpoints/second
- **文件操作**: 支持批量文件生成和原子操作
- **缓存命中率**: 智能缓存命中率 > 80%
- **增量更新**: 变更检测准确率 > 95%
## 📊 性能与可观测性
- 解析层通过 PerformanceMonitor 记录获取/解析耗时,并复用 CacheManager 结果避免重复网络请求
- 多文档合并时会输出模型/路径统计与覆盖提示,便于确认版本覆盖关系
- 生成结束会输出 SUMMARY.md 与控制台摘要,包含控制器、路径、模型数量
- 文件写入前的跳过策略减少无意义的 IO提升重复生成时的稳定性
## 🎯 应用场景
@ -133,44 +188,33 @@ XY Swagger Generator 是一个专为 Flutter 开发优化的 OpenAPI 3.0 代码
## 📈 发展路线
### 当前版本 (v2.0.x)
- ✅ 完整的 OpenAPI 3.0 支持
- ✅ 高性能解析和生成
- ✅ 企业级验证和错误处理
- ✅ Dio + Retrofit 完美集成
### 当前版本 (v2.1.x)
- ✅ dev dependency 场景的 CLI 入口与可执行别名
- ✅ 多 Swagger 顺序合并与版本化 API 输出
- ✅ Tag 过滤、忽略策略、BaseResult/BasePageResult 导入配置
- ✅ 示例项目与基础测试脚本tests/
### 下一版本 (v2.1.x)
- 🔄 GraphQL 支持
- 🔄 更多代码生成模板
- 🔄 可视化配置界面
- 🔄 CI/CD 集成工具
### 未来规划 (v3.0.x)
- 📋 多语言支持 (Kotlin, Swift)
- 📋 云端代码生成服务
- 📋 AI 辅助优化建议
- 📋 实时 API 监控
### 后续计划
- 🔄 提升自动化测试覆盖与生成结果校验
- 🔄 完善配置校验与错误提示体验
- 🔄 持续同步 README/示例与最新生成逻辑
## 🤝 社区与支持
### 文档资源
- [快速开始指南](../QUICK_REFERENCE.md)
- [项目主文档](../README.md)
- [使用指南](./USAGE_GUIDE.md)
- [API 参考文档](./API_REFERENCE.md)
- [最佳实践指南](./BEST_PRACTICES.md)
- [故障排除指南](./TROUBLESHOOTING.md)
- [快速参考](../QUICK_REFERENCE.md)
- [配置模板](../generator_config.template.yaml)
### 贡献方式
- [贡献指南](../CONTRIBUTING.md)
- [代码审查清单](../CODE_REVIEW_CHECKLIST.md)
- [开发环境搭建](./DEVELOPMENT_SETUP.md)
- [测试指南](./TESTING_GUIDE.md)
- 提交功能前运行 `dart run swagger_generator_flutter generate --all` 以及必要的 `build_runner`
- 在 Issue/PR 中附上配置片段与最小示例(可参考 example/
- 变更生成规则时同步更新 README 与 docs/
---
**项目维护者**: Max
**最后更新**: 2025-01-24
**文档版本**: v2.0
**项目维护者**: Max
**最后更新**: 2025-11-09
**文档版本**: v2.1

View File

@ -22,25 +22,16 @@ flutter pub get
### 基础使用
#### 命令行方式 (推荐新手)
在项目根目录下(`generator_config.yaml` 所在目录)运行以下命令:
```bash
# 克隆或下载项目
git clone <repository-url>
cd swagger_generator_flutter
# 步骤 1: 生成 API 定义和 Freezed 模型
# 这会根据 swagger.json 生成 *.dart 文件,但它们还不完整
dart run swagger_generator_flutter generate --all
# 安装依赖
flutter pub get
# 将你的 swagger.json 放在项目根目录
# 生成所有代码
sh run_swagger.sh all
# 或者分别生成
sh run_swagger.sh api # 只生成 API
sh run_swagger.sh models # 只生成模型
sh run_swagger.sh docs # 只生成文档
# 步骤 2: 运行 build_runner 完成代码生成
# 这会生成 *.freezed.dart 和 *.g.dart 文件,补全模型和序列化逻辑
dart run build_runner build --delete-conflicting-outputs
```
#### 编程方式 (推荐进阶用户)
@ -63,10 +54,9 @@ void main() async {
final document = await parser.parseDocument(jsonString);
// 3. 创建生成器
final generator = OptimizedRetrofitGenerator(
final generator = RetrofitApiGenerator(
className: 'ApiService',
generateModularApis: true,
generateBaseResult: true,
splitByTags: true,
);
// 4. 生成并保存代码
@ -90,21 +80,17 @@ dependencies:
# 网络请求
dio: ^5.4.0
retrofit: ^4.0.0
# JSON 序列化
# Freezed 模型
freezed_annotation: ^2.4.1
json_annotation: ^4.8.1
# 其他依赖
logging: ^1.2.0
dev_dependencies:
# 代码生成
build_runner: ^2.4.7
retrofit_generator: ^8.0.0
json_serializable: ^6.7.1
# 测试
test: ^1.24.0
freezed: ^2.4.7
```
### 2. 项目结构
@ -243,31 +229,6 @@ templates:
### 2. 代码生成最佳实践
#### 选择合适的生成器
```dart
// 小型项目 - 基础生成器
final generator = RetrofitApiGenerator(
className: 'ApiService',
splitByTags: true,
);
// 中型项目 - 优化生成器
final generator = OptimizedRetrofitGenerator(
className: 'ApiService',
generateModularApis: true,
generateBaseResult: true,
generatePagination: true,
);
// 大型项目 - 性能生成器
final generator = PerformanceGenerator(
maxConcurrency: 8,
enableCaching: true,
enableIncremental: true,
);
```
#### 配置合适的解析器
```dart
@ -517,13 +478,8 @@ class {{className}} {
使用自定义模板:
```dart
final generator = OptimizedRetrofitGenerator(
templatePath: 'templates/custom_api.mustache',
customVariables: {
'author': 'Your Name',
'generatedAt': DateTime.now().toIso8601String(),
},
);
// 使用自定义生成器时,请继承 BaseGenerator 并实现 generate()
// 或基于 RetrofitApiGenerator 的输出进行二次处理。
```
---
@ -616,12 +572,10 @@ final parser = PerformanceParser(
**解决方案**:
```dart
// 启用并行生成和缓存
final generator = PerformanceGenerator(
maxConcurrency: 4,
enableCaching: true,
enableIncremental: true,
cacheStrategy: CacheStrategy.smart,
// 在 CI 中按模块并行执行多个 RetrofitApiGenerator 任务
final generator = RetrofitApiGenerator(
className: 'ApiService',
splitByTags: true,
);
```
@ -778,6 +732,6 @@ Future<void> main() async {
---
**文档版本**: v2.0
**最后更新**: 2025-01-24
**文档版本**: v3.0
**最后更新**: 2025-11-21
**维护者**: Max

View File

@ -1,311 +0,0 @@
/// 使
///
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}');
}

View File

@ -0,0 +1,18 @@
include: package:very_good_analysis/analysis_options.yaml
analyzer:
exclude:
# 排除所有生成的文件
- "**/*.g.dart"
- "**/*.freezed.dart"
# 如果还有其他生成文件,也可以添加
# - "**/*.gr.dart" # auto_route 生成的文件
# - "**/*.config.dart" # injectable 生成的文件
linter:
rules:
# 关闭强制文档注释 (很多业务开发觉得这条太累赘)
public_member_api_docs: false
# 可选:如果你不喜欢强制构造函数必须写在最前面,也可以关掉
# sort_constructors_first: false

View File

@ -1,70 +0,0 @@
.PHONY: help install generate build clean run test
# 默认目标
help:
@echo "可用命令:"
@echo " make install - 安装依赖"
@echo " make generate - 生成 API 代码"
@echo " make build - 运行 build_runner"
@echo " make clean - 清理生成的文件"
@echo " make run - 运行应用"
@echo " make test - 运行测试"
# 安装依赖
install:
@echo "📦 安装依赖..."
@flutter pub get
@echo "✅ 依赖安装完成"
# 生成 API 代码
generate:
@echo "🚀 生成 API 代码..."
@dart run swagger_generator_flutter generate --all
@echo "✅ API 代码生成完成"
# 运行 build_runner
build: generate
@echo "🔧 运行 build_runner..."
@dart run build_runner build --delete-conflicting-outputs
@dart format lib/generated
@echo "✅ 构建完成"
# 监听模式
watch:
@echo "👀 启动监听模式..."
@dart run build_runner watch --delete-conflicting-outputs
# 清理生成的文件
clean:
@echo "🧹 清理生成的文件..."
@rm -rf lib/generated
@flutter clean
@echo "✅ 清理完成"
# 重新生成
regenerate: clean build
# 运行应用
run:
@echo "🚀 运行应用..."
@flutter run
# 运行测试
test:
@echo "🧪 运行测试..."
@flutter test
# 分析代码
analyze:
@echo "🔍 分析代码..."
@dart analyze
# 格式化代码
format:
@echo "📐 格式化代码..."
@dart format lib/
# 检查代码质量
check: analyze test
@echo "✅ 代码质量检查完成"

View File

@ -1,23 +0,0 @@
include: package:flutter_lints/flutter.yaml
linter:
rules:
- always_declare_return_types
- always_put_required_named_parameters_first
- avoid_print
- avoid_unnecessary_containers
- prefer_const_constructors
- prefer_const_literals_to_create_immutables
- prefer_final_fields
- prefer_single_quotes
- sort_child_properties_last
- use_key_in_widget_constructors
analyzer:
exclude:
- '**/*.g.dart'
- '**/*.freezed.dart'
- 'lib/generated/**'
errors:
invalid_annotation_target: ignore

View File

@ -1,70 +0,0 @@
@echo off
REM Swagger API 代码生成脚本Windows
REM 用于示例项目的 API 代码生成
setlocal enabledelayedexpansion
echo ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
echo 🚀 Swagger API 代码生成器
echo ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
echo.
REM 步骤 1: 生成 API 代码
echo 📝 步骤 1/4: 生成 API 代码...
dart run swagger_generator_flutter generate --all
if !errorlevel! neq 0 (
echo ❌ API 代码生成失败!
exit /b 1
)
echo ✅ API 代码生成成功
echo.
REM 步骤 2: 运行 build_runner
echo 🔧 步骤 2/4: 运行 build_runner...
dart run build_runner build --delete-conflicting-outputs
if !errorlevel! neq 0 (
echo ⚠️ build_runner 执行失败(如果是首次运行,可能需要先修复基础类型)
echo 请检查 lib/common/api_response.dart 和 paged_response.dart
) else (
echo ✅ build_runner 执行成功
)
echo.
REM 步骤 3: 格式化代码
echo 📐 步骤 3/4: 格式化代码...
dart format lib/generated
echo ✅ 代码格式化完成
echo.
REM 步骤 4: 分析代码
echo 🔍 步骤 4/4: 分析代码...
dart analyze lib/generated --fatal-infos
if !errorlevel! neq 0 (
echo ⚠️ 代码分析发现问题,请检查
) else (
echo ✅ 代码分析通过
)
echo.
echo ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
echo ✨ 代码生成完成!
echo ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
echo.
echo 📂 生成的文件位置:
echo lib/generated/api/
echo lib/generated/api_models/
echo.
echo 📚 下一步:
echo 1. 查看生成的代码lib/generated/
echo 2. 在 main.dart 中取消注释 import 语句
echo 3. 运行应用flutter run
echo.
pause

View File

@ -1,69 +0,0 @@
#!/bin/bash
# Swagger API 代码生成脚本
# 用于示例项目的 API 代码生成
set -e # 遇到错误立即退出
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🚀 Swagger API 代码生成器"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# 步骤 1: 生成 API 代码
echo "📝 步骤 1/4: 生成 API 代码..."
dart run swagger_generator_flutter generate --all
if [ $? -ne 0 ]; then
echo "❌ API 代码生成失败!"
exit 1
fi
echo "✅ API 代码生成成功"
echo ""
# 步骤 2: 运行 build_runner
echo "🔧 步骤 2/4: 运行 build_runner..."
dart run build_runner build --delete-conflicting-outputs
if [ $? -ne 0 ]; then
echo "⚠️ build_runner 执行失败(如果是首次运行,可能需要先修复基础类型)"
echo " 请检查 lib/common/api_response.dart 和 paged_response.dart"
else
echo "✅ build_runner 执行成功"
fi
echo ""
# 步骤 3: 格式化代码
echo "📐 步骤 3/4: 格式化代码..."
dart format lib/generated
echo "✅ 代码格式化完成"
echo ""
# 步骤 4: 分析代码
echo "🔍 步骤 4/4: 分析代码..."
dart analyze lib/generated --fatal-infos
if [ $? -ne 0 ]; then
echo "⚠️ 代码分析发现问题,请检查"
else
echo "✅ 代码分析通过"
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✨ 代码生成完成!"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "📂 生成的文件位置:"
echo " lib/generated/api/"
echo " lib/generated/api_models/"
echo ""
echo "📚 下一步:"
echo " 1. 查看生成的代码lib/generated/"
echo " 2. 在 main.dart 中取消注释 import 语句"
echo " 3. 运行应用flutter run"
echo ""

File diff suppressed because it is too large Load Diff

View File

@ -1,107 +0,0 @@
#!/bin/bash
# 测试示例项目脚本
# 用于快速验证 dev_dependencies 功能是否正常工作
set -e
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🧪 测试 Swagger Generator 示例项目"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# 检查当前目录
if [ ! -f "pubspec.yaml" ]; then
echo "❌ 错误:请在示例项目目录中运行此脚本"
echo " cd example/as_dev_dependency"
exit 1
fi
echo "📂 当前目录: $(pwd)"
echo ""
# 步骤 1: 清理
echo "🧹 步骤 1/5: 清理旧文件..."
rm -rf lib/generated .dart_tool build
echo "✅ 清理完成"
echo ""
# 步骤 2: 安装依赖
echo "📦 步骤 2/5: 安装依赖..."
flutter pub get
if [ $? -ne 0 ]; then
echo "❌ 依赖安装失败"
exit 1
fi
echo "✅ 依赖安装成功"
echo ""
# 步骤 3: 生成 API 代码
echo "🚀 步骤 3/5: 生成 API 代码..."
dart run swagger_generator_flutter generate --all
if [ $? -ne 0 ]; then
echo "❌ API 代码生成失败"
exit 1
fi
echo "✅ API 代码生成成功"
echo ""
# 步骤 4: 检查生成的文件
echo "🔍 步骤 4/5: 检查生成的文件..."
if [ ! -d "lib/generated" ]; then
echo "❌ 生成目录不存在"
exit 1
fi
if [ ! -d "lib/generated/api" ]; then
echo "❌ API 目录不存在"
exit 1
fi
if [ ! -d "lib/generated/api_models" ]; then
echo "❌ Models 目录不存在"
exit 1
fi
# 统计生成的文件
api_files=$(find lib/generated/api -name "*.dart" | wc -l | tr -d ' ')
model_files=$(find lib/generated/api_models -name "*.dart" | wc -l | tr -d ' ')
echo " 📄 生成的 API 文件: $api_files"
echo " 📄 生成的 Model 文件: $model_files"
echo "✅ 文件检查通过"
echo ""
# 步骤 5: 分析代码
echo "🔬 步骤 5/5: 分析生成的代码..."
dart analyze lib/generated 2>&1 | head -20
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✨ 测试完成!"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "📊 统计信息:"
echo " ✅ API 文件: $api_files"
echo " ✅ Model 文件: $model_files"
echo ""
echo "📁 生成的文件位置:"
echo " lib/generated/api/"
echo " lib/generated/api_models/"
echo ""
echo "🎯 下一步:"
echo " 1. 运行 build_runner:"
echo " dart run build_runner build --delete-conflicting-outputs"
echo ""
echo " 2. 运行应用:"
echo " flutter run"
echo ""
echo " 3. 查看生成的代码:"
echo " ls -la lib/generated/"
echo ""

View File

@ -0,0 +1,79 @@
# 枚举配置文件映射示例
# 演示如何使用配置文件为枚举值定义有意义的键名和描述
generation:
models:
# 枚举键名映射配置
enum_key_mappings:
# 任务类型枚举
SysTaskTypeEnums:
- value: 1
name: SPOT_CHECK
description: 抽查
- value: 2
name: CULTURAL
description: 文创建设
- value: 3
name: CLASS_CADRE_MEETING
description: 班干部会议
- value: 4
name: CULTURAL_PROJECT
description: 文创项目
- value: 5
name: TEACHER_AWARD
description: 教工评优
- value: 6
name: CLASS_EVALUATION
description: 班级评比
- value: 7
name: ORGANIZATION_LIFE
description: 组织生活
# 系统角色枚举
SysRoleEnum:
- value: 1
name: ADMIN
description: 系统管理员
- value: 2
name: TEACHER
description: 教师
- value: 3
name: STUDENT
description: 学生
- value: 4
name: PARENT
description: 家长
# 班级类型枚举(字符串类型)
ClassTypeEnum:
- value: "PRIMARY"
name: PRIMARY_SCHOOL
description: 小学
- value: "MIDDLE"
name: MIDDLE_SCHOOL
description: 初中
- value: "HIGH"
name: HIGH_SCHOOL
description: 高中
# 状态枚举
StatusEnum:
- value: "active"
name: ACTIVE
description: 活跃状态
- value: "inactive"
name: INACTIVE
description: 非活跃状态
- value: "banned"
name: BANNED
description: 已封禁
# 使用说明:
# 1. 将需要映射的枚举名称作为键(必须与 Swagger 文档中的枚举名称完全匹配)
# 2. 为每个枚举值配置 value、name 和 description
# 3. value 必须与 Swagger 文档中的枚举值类型和值完全匹配
# 4. name 必须是有效的 Dart 标识符(大写字母+下划线)
# 5. description 是可选的,会生成为注释
# 6. 可以只配置部分枚举值,未配置的会使用 x-enum-varnames 或智能生成
# 7. 优先级:配置文件映射 > x-enum-varnames > 智能生成

86
example/generate_api.bat Normal file
View File

@ -0,0 +1,86 @@
@echo off
REM Swagger API 代码生成脚本 (Windows)
REM 用于 Learning Officer OA 项目
setlocal enabledelayedexpansion
echo ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
echo 🚀 Swagger API 代码生成器
echo ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
echo.
REM 检查是否在项目根目录
if not exist "pubspec.yaml" (
echo ❌ 错误: 请在项目根目录运行此脚本
exit /b 1
)
REM 检查配置文件是否存在
if not exist "generator_config.yaml" (
echo ❌ 错误: 找不到 generator_config.yaml 配置文件
echo 请先创建配置文件
exit /b 1
)
REM 步骤 1: 运行代码生成器
echo 📝 步骤 1/4: 正在运行代码生成器...
dart run swagger_generator_flutter generate --all
if !errorlevel! neq 0 (
echo ❌ 代码生成失败!
exit /b 1
)
echo ✅ 代码生成成功
echo.
REM 步骤 2: 运行 build_runner 生成 .g.dart 文件
echo 🔨 步骤 2/4: 正在运行 build_runner 生成 .g.dart 文件...
flutter pub run build_runner build --delete-conflicting-outputs
if !errorlevel! neq 0 (
echo ⚠️ build_runner 执行失败,请检查错误信息
) else (
echo ✅ build_runner 执行成功
)
echo.
REM 步骤 3: 修复和排序 imports
echo 🔧 步骤 3/4: 修复和排序 imports...
dart fix --apply lib/common/api
dart fix --apply lib/common/api_models
if !errorlevel! neq 0 (
echo ⚠️ dart fix 执行失败,请检查错误信息
) else (
echo ✅ dart fix 执行成功
)
echo.
REM 步骤 4: 格式化代码
echo 📐 步骤 4/4: 格式化代码...
dart format lib/common/api lib/common/api_models --set-exit-if-changed
if !errorlevel! neq 0 (
echo ⚠️ 代码格式化失败,请检查错误信息
) else (
echo ✅ 代码格式化完成
)
echo.
echo ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
echo ✨ 代码生成完成!
echo ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
echo.
echo 📋 生成的文件位置:
echo - API 接口: lib/common/api/
echo - API 模型: lib/common/api_models/
echo.
echo 💡 提示:
echo - 如果生成的文件有错误,请检查并修复后重新运行 build_runner
echo - 建议在提交代码前检查生成的代码是否符合项目规范
echo.
pause

95
example/generate_api.sh Normal file
View File

@ -0,0 +1,95 @@
#!/bin/bash
# Swagger API 代码生成脚本
# 用于 Learning Officer OA 项目
set -e # 遇到错误立即退出
# 颜色定义
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo -e "${CYAN}🚀 Swagger API 代码生成器${NC}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# 检查是否在项目根目录
if [ ! -f "pubspec.yaml" ]; then
echo -e "${YELLOW}❌ 错误: 请在项目根目录运行此脚本${NC}"
exit 1
fi
# 检查配置文件是否存在
if [ ! -f "generator_config.yaml" ]; then
echo -e "${YELLOW}❌ 错误: 找不到 generator_config.yaml 配置文件${NC}"
echo "请先创建配置文件"
exit 1
fi
rm -rf lib/src/api/*.dart
rm -rf lib/src/models/*.dart
# 步骤 1: 运行代码生成器
echo -e "${CYAN}📝 步骤 1/4: 正在运行代码生成器...${NC}"
dart run swagger_generator_flutter generate --all
if [ $? -ne 0 ]; then
echo -e "${YELLOW}❌ 代码生成失败!${NC}"
exit 1
fi
echo -e "${GREEN}✅ 代码生成成功${NC}"
echo ""
# 步骤 2: 运行 build_runner 生成 .g.dart 文件
echo -e "${CYAN}🔨 步骤 2/4: 正在运行 build_runner 生成 .g.dart 文件...${NC}"
flutter pub run build_runner build --delete-conflicting-outputs
if [ $? -ne 0 ]; then
echo -e "${YELLOW}⚠️ build_runner 执行失败,请检查错误信息${NC}"
else
echo -e "${GREEN}✅ build_runner 执行成功${NC}"
fi
echo ""
# 步骤 3: 修复和排序 imports
echo -e "${CYAN}🔧 步骤 3/4: 修复和排序 imports...${NC}"
dart fix --apply lib/src/api
FIX_API_EXIT_CODE=$?
dart fix --apply lib/src/api_models
FIX_MODELS_EXIT_CODE=$?
if [ $FIX_API_EXIT_CODE -ne 0 ] || [ $FIX_MODELS_EXIT_CODE -ne 0 ]; then
echo -e "${YELLOW}⚠️ dart fix 执行失败,请检查错误信息${NC}"
else
echo -e "${GREEN}✅ dart fix 执行成功${NC}"
fi
echo ""
# 步骤 4: 格式化代码
echo -e "${CYAN}📐 步骤 4/4: 格式化代码...${NC}"
dart format lib/src/api lib/src/api_models --set-exit-if-changed
if [ $? -ne 0 ]; then
echo -e "${YELLOW}⚠️ 代码格式化失败,请检查错误信息${NC}"
else
echo -e "${GREEN}✅ 代码格式化完成${NC}"
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo -e "${GREEN}✨ 代码生成完成!${NC}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo -e "${CYAN}📋 生成的文件位置:${NC}"
echo " - API 接口: lib/src/api/"
echo " - API 模型: lib/src/api_models/"
echo ""
echo -e "${CYAN}💡 提示:${NC}"
echo " - 如果生成的文件有错误,请检查并修复后重新运行 build_runner"
echo " - 建议在提交代码前检查生成的代码是否符合项目规范"
echo ""

View File

@ -15,11 +15,11 @@ input:
# 因此建议将高版本(如 V2配置在低版本如 V1之后以确保高版本的模型覆盖低版本
# 例如V1 在前V2 在后,那么 V2 的模型会覆盖 V1 的同名模型
swagger_urls: # 完整形式:可以控制每个版本的启用状态
- url: "https://quanxue-test-api.w.23544.com:8843/swagger/v1/swagger.json"
- url: "http://192.168.2.7:17288/swagger/v1/swagger.json"
enabled: true
- url: "https://quanxue-test-api.w.23544.com:8843/swagger/v2/swagger.json"
- url: "http://192.168.2.7:17288/swagger/v2/swagger.json"
enabled: true
# 验证配置
validate_schema: true
strict_mode: false
@ -27,44 +27,55 @@ input:
# 输出配置
output:
# 输出目录
base_dir: "./lib/generated"
api_dir: "./lib/generated/api"
models_dir: "./lib/generated/api_models"
base_dir: "./lib/src"
api_dir: "./lib/src/api"
models_dir: "./lib/src/api_models"
# 文件命名
api_file_suffix: "_api.dart"
model_file_suffix: ".dart"
# 是否按 tag 分组
split_by_tags: true
excluded_tags:
# 通用
- "Login"
- "MyInfo"
# K8S
- "HealthCheck"
# H5 积分
- "Points"
# H5意见反馈
- "FeedBackInfo"
# 跳过的目录列表(这些目录下的文件将不会被生成)
# 支持相对路径和绝对路径,支持目录名或完整路径
ignored_directories:
- "class_type_enum.dart"
- "sys_role_enum.dart"
- "sys_task_type_enums.dart"
- "teaching_level_enum.dart"
- "base_task_add_result.dart"
- "school_tree.dart"
- "sys_parameter.dart"
- "task_checklist_cloud_school_result.dart"
# - "class_type_enum.dart"
# - "sys_role_enum.dart"
# - "sys_task_type_enums.dart"
# - "teaching_level_enum.dart"
# - "base_task_add_result.dart"
# - "school_tree.dart"
# - "sys_parameter.dart"
# - "task_checklist_cloud_school_result.dart"
# - "api/v1" # 跳过 v1 版本的 API
# - "api_models/request" # 跳过请求模型目录
# - "./lib/generated/api/v2" # 跳过特定路径
# 跳过的文件名列表(这些文件将不会被生成)
# 支持精确匹配、通配符匹配和模式匹配
ignored_files:
# 精确匹配文件名
# - "user_api.dart" # 跳过名为 user_api.dart 的文件
# - "mobile_manager_api.dart" # 跳过指定文件
# 通配符匹配(支持前缀和后缀)
# - "*_api.dart" # 跳过所有以 _api.dart 结尾的文件
# - "user*.dart" # 跳过所有以 user 开头的 .dart 文件
# - "*manager*" # 跳过所有包含 manager 的文件名
# 示例:跳过所有 v1 版本的 API 文件(如果文件名包含版本信息)
# - "*_api_v1.dart"
# - "*V1*.dart"
@ -77,41 +88,66 @@ generation:
use_retrofit: true
use_dio: true
parser: "JsonSerializable"
# 版本提取配置(多版本支持)
version_extraction:
# 版本提取正则表达式模式
pattern: "/api/v(\\d+)/"
# 默认版本(当无法从路径提取版本时使用)
default_version: "v1"
# 基础类型配置
base_result_type: "ApiResponse"
base_page_result_type: "PagedResponse"
base_result_import: "package:example_app/common/api_response.dart"
base_page_result_import: "package:example_app/common/paged_response.dart"
base_result_type: "BaseResult"
base_page_result_type: "BasePageResult"
base_result_import: "package:example_app/common/base_result.dart"
base_page_result_import: "package:example_app/common/base_page_result.dart"
# 方法命名
method_naming: "camelCase"
# 数据模型配置
models:
enabled: true
use_json_serializable: true
# JsonSerializable 配置
json_serializable:
checked: true
include_if_null: false
explicit_to_json: true
# 类命名
class_naming: "PascalCase"
field_naming: "camelCase"
# 构造函数配置
use_const_constructor: true
required_for_non_nullable: true
# 枚举键名映射配置(测试)
enum_key_mappings:
SysTaskTypeEnums:
- value: 1
name: SPOT_CHECK
description: 抽查
- value: 2
name: CULTURAL
description: 文创建设
- value: 3
name: CLASS_CADRE_MEETING
description: 班干部会议
- value: 4
name: CULTURAL_PROJECT
description: 文创项目
- value: 5
name: TEACHER_AWARD
description: 教工评优
- value: 6
name: CLASS_EVALUATION
description: 班级评比
- value: 7
name: ORGANIZATION_LIFE
description: 组织生活
# 类型映射配置
type_mapping:
@ -130,11 +166,11 @@ 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"
@ -165,46 +201,3 @@ debug:
performance_monitoring: false
generation_stats: true
# 模板配置
templates:
# 文件头模板
# 支持模板变量:
# {fileName} - 文件名(如 "user_api.dart"
# {fileType} - 文件类型描述(如 "API 接口定义"、"模型定义"
# {swaggerUrl} - Swagger 文档 URL
# {generatorName} - 生成器名称(从 generator.name 读取)
# {author} - 作者(从 generator.author 读取)
# {copyright} - 版权信息(从 generator.copyright 读取)
file_header: |
// {fileType}
// 基于 Swagger API 文档: {swaggerUrl}
// 由 {generatorName} by {author} 生成
// {copyright}
# API 类模板
# 支持模板变量:
# {tagName} - Tag 名称(如 "User"、"Task"
# {className} - 类名(如 "UserApi"、"TaskApi"
api_class: |
/// {tagName} API 接口
/// 负责处理 {tagName} 相关的接口
@RestApi(parser: Parser.JsonSerializable)
abstract class {className} {
factory {className}(Dio dio, {String? baseUrl}) = _{className};
}
# 模型类模板
# 支持模板变量:
# {className} - 类名(如 "User"、"Task"
# {constructorParams} - 构造函数参数列表
model_class: |
@JsonSerializable(checked: true, includeIfNull: false)
class {className} {
const {className}({constructorParams});
factory {className}.fromJson(Map<String, dynamic> json) =>
_${className}FromJson(json);
Map<String, dynamic> toJson() => _${className}ToJson(this);
}

View File

@ -0,0 +1,7 @@
abstract class BaseAbstract extends Object {
Map<String, dynamic> toJson();
}
abstract class BaseContainsParametersAbstract extends Object {
Map<String, dynamic> toJson(Object Function(dynamic value) toJsonT);
}

View File

@ -0,0 +1,26 @@
import 'package:json_annotation/json_annotation.dart';
part 'base_page_result.g.dart';
@JsonSerializable(
checked: true,
genericArgumentFactories: true,
fieldRename: FieldRename.snake,
)
class BasePageResult<T> extends Object {
BasePageResult({required this.items, required this.total});
factory BasePageResult.fromJson(
Map<String, dynamic> json,
T Function(dynamic json) fromJsonT,
) => _$BasePageResultFromJson(json, fromJsonT);
@JsonKey(name: 'items')
final List<T> items;
@JsonKey(name: 'total')
final int total;
Map<String, dynamic> toJson(Object Function(T value) toJsonT) =>
_$BasePageResultToJson(this, toJsonT);
}

View File

@ -0,0 +1,52 @@
import 'package:example_app/common/base_abstract.dart';
import 'package:json_annotation/json_annotation.dart';
part 'base_result.g.dart';
@JsonSerializable(
checked: true,
genericArgumentFactories: true,
fieldRename: FieldRename.snake,
)
class BaseResult<T> extends BaseContainsParametersAbstract {
BaseResult(this.code, this.message, this.data) {
success = successCodes.contains(code);
}
///
factory BaseResult.failure({required int code, String? message, T? data}) {
return BaseResult(code, message ?? '', data);
}
///
factory BaseResult.success({T? data, String? message, int code = 200}) {
return BaseResult(code, message ?? '', data);
}
factory BaseResult.fromJson(
Map<String, dynamic> json,
T Function(dynamic json) fromJsonT,
) => _$BaseResultFromJson(json, fromJsonT);
@JsonKey(name: 'code')
final int code;
///
@JsonKey(name: 'msg')
final String? message;
///
@JsonKey(name: 'data')
final T? data;
/// code
@JsonKey(includeFromJson: false, includeToJson: false)
late final bool success;
///
static List<int> successCodes = [200, 0];
@override
Map<String, dynamic> toJson(Object Function(dynamic value) toJsonT) =>
_$BaseResultToJson(this, toJsonT);
}

View File

@ -5,18 +5,26 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
sha256: f0bb5d1648339c8308cc0b9838d8456b3cfe5c91f9dc1a735b4d003269e5da9a
url: "https://pub.flutter-io.cn"
source: hosted
version: "67.0.0"
version: "88.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
sha256: "0b7b9c329d2879f8f05d6c05b32ee9ec025f39b077864bdb5ac9a7b63418a98f"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.4.1"
version: "8.1.1"
ansicolor:
dependency: transitive
description:
name: ansicolor
sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.3"
args:
dependency: transitive
description:
@ -45,18 +53,18 @@ packages:
dependency: transitive
description:
name: build
sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0"
sha256: c1668065e9ba04752570ad7e038288559d1e2ca5c6d0131c0f5f55e39e777413
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.1"
version: "4.0.3"
build_config:
dependency: transitive
description:
name: build_config
sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.2"
version: "1.2.0"
build_daemon:
dependency: transitive
description:
@ -65,30 +73,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.1.0"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.2"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d"
sha256: "110c56ef29b5eb367b4d17fc79375fa8c18a6cd7acd92c05bb3986c17a079057"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.13"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.3.2"
version: "2.10.4"
built_collection:
dependency: transitive
description:
@ -165,10 +157,10 @@ packages:
dependency: transitive
description:
name: dart_style
sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9"
sha256: c87dfe3d56f183ffe9106a18aebc6db431fc7c98c31a54b952a77f3d54a85697
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.6"
version: "3.1.2"
dio:
dependency: "direct main"
description:
@ -218,15 +210,31 @@ packages:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.2"
version: "6.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
freezed:
dependency: "direct dev"
description:
name: freezed
sha256: "13065f10e135263a4f5a4391b79a8efc5fb8106f8dd555a9e49b750b45393d77"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.2.3"
freezed_annotation:
dependency: "direct main"
description:
name: freezed_annotation
sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.0"
frontend_server_client:
dependency: transitive
description:
@ -251,6 +259,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.2"
hotreloader:
dependency: transitive
description:
name: hotreloader
sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.3.0"
http:
dependency: transitive
description:
@ -283,14 +299,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.5"
js:
dependency: transitive
description:
name: js
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.2"
json_annotation:
dependency: "direct main"
description:
@ -303,10 +311,10 @@ packages:
dependency: "direct dev"
description:
name: json_serializable
sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b
sha256: c5b2ee75210a0f263c6c7b9eeea80553dbae96ea1bf57f02484e806a3ffdffa3
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.8.0"
version: "6.11.2"
leak_tracker:
dependency: transitive
description:
@ -331,14 +339,22 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.2"
lean_builder:
dependency: transitive
description:
name: lean_builder
sha256: ef5cd5f907157eb7aa87d1704504b5a6386d2cbff88a3c2b3344477bab323ee9
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.1.2"
lints:
dependency: transitive
description:
name: lints
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.0"
version: "6.0.0"
logger:
dependency: "direct main"
description:
@ -387,6 +403,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.0"
mustache_template:
dependency: transitive
description:
name: mustache_template
sha256: daa42be75f2ccfb287c24a75e7ac594f2ea0b32bf9ebe7c15154aa45b2dfb2de
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.2"
package_config:
dependency: transitive
description:
@ -415,10 +439,10 @@ packages:
dependency: transitive
description:
name: protobuf
sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d"
sha256: "2fcc8a202ca7ec17dab7c97d6b6d91cf03aa07fe6f65f8afbb6dfa52cc5bd902"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.0"
version: "5.1.0"
pub_semver:
dependency: transitive
description:
@ -439,18 +463,18 @@ packages:
dependency: "direct main"
description:
name: retrofit
sha256: "7d78824afa6eeeaf6ac58220910ee7a97597b39e93360d4bda230b7c6df45089"
sha256: "84063c18a00d55af41d6b8401edf8473e8c215bd7068ef7ec5e34c60657ffdbe"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.9.0"
version: "4.9.1"
retrofit_generator:
dependency: "direct dev"
description:
name: retrofit_generator
sha256: "8dfc406cdfa171f33cbd21bf5bd8b6763548cc217de19cdeaa07a76727fac4ca"
sha256: "7ec323f3329ad2ca0bcdc96fe02ec7f2486ecfac6cd2d035b03c398ef6f42308"
url: "https://pub.flutter-io.cn"
source: hosted
version: "8.2.1"
version: "10.2.0"
shelf:
dependency: transitive
description:
@ -476,18 +500,18 @@ packages:
dependency: transitive
description:
name: source_gen
sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832"
sha256: "07b277b67e0096c45196cbddddf2d8c6ffc49342e88bf31d460ce04605ddac75"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.5.0"
version: "4.1.1"
source_helper:
dependency: transitive
description:
name: source_helper
sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c"
sha256: "6a3c6cc82073a8797f8c4dc4572146114a39652851c157db37e964d9c7038723"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.5"
version: "1.3.8"
source_span:
dependency: transitive
description:
@ -531,10 +555,10 @@ packages:
swagger_generator_flutter:
dependency: "direct dev"
description:
path: "../.."
path: ".."
relative: true
source: path
version: "2.1.1"
version: "3.0.0"
term_glyph:
dependency: transitive
description:
@ -551,22 +575,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.6"
timing:
dependency: transitive
description:
name: timing
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.2"
tuple:
dependency: transitive
description:
name: tuple
sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.2"
typed_data:
dependency: transitive
description:
@ -583,6 +591,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.0"
very_good_analysis:
dependency: "direct dev"
description:
name: very_good_analysis
sha256: "96245839dbcc45dfab1af5fa551603b5c7a282028a64746c19c547d21a7f1e3a"
url: "https://pub.flutter-io.cn"
source: hosted
version: "10.0.0"
vm_service:
dependency: transitive
description:
@ -623,6 +639,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.3"
xxh3:
dependency: transitive
description:
name: xxh3
sha256: "399a0438f5d426785723c99da6b16e136f4953fb1e9db0bf270bd41dd4619916"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.0"
yaml:
dependency: transitive
description:
@ -632,5 +656,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.8.0 <4.0.0"
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"

View File

@ -4,37 +4,40 @@ description: Example Flutter app using swagger_generator_flutter as dev_dependen
version: 1.0.0
environment:
sdk: '>=3.0.0 <4.0.0'
sdk: ^3.8.0
dependencies:
flutter:
sdk: flutter
# HTTP 客户端
dio: ^5.0.0
retrofit: ^4.0.0
dio: ^5.9.0
# API 客户端
retrofit: ^4.9.1
# JSON 序列化
json_annotation: ^4.8.0
json_annotation: ^4.9.0
freezed_annotation: ^3.1.0
# 其他依赖
logger: ^2.0.0
dev_dependencies:
flutter_test:
sdk: flutter
# Swagger 代码生成器(使用本地路径作为示例)
swagger_generator_flutter:
path: ../../
path: ../
# 代码生成工具
build_runner: ^2.4.7
retrofit_generator: ^8.0.0
json_serializable: ^6.7.1
build_runner: ^2.10.4
retrofit_generator: ^10.2.0
json_serializable: ^6.11.2
freezed: ^3.2.3
# 代码分析
flutter_lints: ^3.0.0
flutter_lints: 6.0.0
very_good_analysis: ^10.0.0
flutter:
uses-material-design: true

16921
example/swagger.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,39 @@
{
"openapi": "3.0.1",
"info": {
"title": "Enum Config Mapping Test API",
"version": "1.0"
},
"paths": {
"/api/v1/test/task-type": {
"get": {
"tags": ["Test"],
"summary": "Get task type",
"operationId": "getTaskType",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SysTaskTypeEnums"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"SysTaskTypeEnums": {
"type": "integer",
"description": "任务类型枚举",
"enum": [1, 2, 3, 4, 5, 6, 7],
"format": "int32"
}
}
}
}

View File

@ -0,0 +1,85 @@
{
"openapi": "3.0.1",
"info": {
"title": "枚举键名示例 API",
"version": "v1",
"description": "演示如何使用 x-enum-varnames 和 x-enum-descriptions 生成有意义的枚举键名"
},
"paths": {},
"components": {
"schemas": {
"SysTaskTypeEnums": {
"enum": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, -1],
"type": "integer",
"description": "任务类型枚举",
"format": "int32",
"x-enum-varnames": [
"SPOT_CHECK",
"CULTURAL",
"CLASS_CADRE_MEETING",
"STUDENT_TALK",
"FOLLOW_CLASS",
"TEACHER_BEHAVIOR_OBSERVATION",
"MEETING",
"COACH_SUBJECT",
"DATA_COLLECTION",
"CLASS_MEETING",
"TEACHER_TALK",
"OTHER_WORK",
"CLASS_ACTIVITY",
"UNKNOWN"
],
"x-enum-descriptions": [
"抽查",
"文创建设",
"班干部会议",
"学生谈话",
"双师跟课",
"教师行为观察",
"参加会议",
"学科辅助",
"数据采集",
"召开班会",
"教师谈话",
"其他工作",
"班级活动",
"未知类型"
]
},
"SysRoleEnum": {
"enum": [1, 2, 3, 4],
"type": "integer",
"description": "系统角色枚举",
"format": "int32",
"x-enum-varnames": [
"ADMIN",
"TEACHER",
"STUDENT",
"PARENT"
],
"x-enum-descriptions": [
"系统管理员",
"教师",
"学生",
"家长"
]
},
"ClassTypeEnum": {
"enum": ["PRIMARY", "MIDDLE", "HIGH"],
"type": "string",
"description": "班级类型枚举",
"x-enum-varnames": [
"PRIMARY_SCHOOL",
"MIDDLE_SCHOOL",
"HIGH_SCHOOL"
],
"x-enum-descriptions": [
"小学",
"初中",
"高中"
]
}
}
}
}

View File

@ -0,0 +1,52 @@
# 枚举配置文件映射测试配置
generator:
name: "test_generator"
version: "1.0"
author: "test"
input:
swagger_urls:
- "swagger_config_mapping_test.json"
output:
base_dir: "./test_output"
api_dir: "./test_output/api"
models_dir: "./test_output/models"
generation:
api:
enabled: true
use_retrofit: true
base_result_type: "BaseResult"
base_result_import: "package:example_app/common/base_result.dart"
models:
enabled: true
use_json_serializable: true
# 测试枚举键名映射
enum_key_mappings:
SysTaskTypeEnums:
- value: 1
name: SPOT_CHECK
description: 抽查
- value: 2
name: CULTURAL
description: 文创建设
- value: 3
name: CLASS_CADRE_MEETING
description: 班干部会议
- value: 4
name: CULTURAL_PROJECT
description: 文创项目
- value: 5
name: TEACHER_AWARD
description: 教工评优
- value: 6
name: CLASS_EVALUATION
description: 班级评比
- value: 7
name: ORGANIZATION_LIFE
description: 组织生活

View File

@ -1,130 +0,0 @@
# API Client 配置示例
# 演示如何自定义 API Client 的类名和文件名
# 示例 1: 使用默认配置
# 如果不配置,将使用默认值:
# - class_name: ApiClient
# - file_name: api_client
generation:
api:
enabled: true
use_retrofit: true
use_dio: true
# client 配置可以省略,使用默认值
---
# 示例 2: 自定义 API Client 名称
# 适用于多个项目或模块,避免命名冲突
generation:
api:
enabled: true
use_retrofit: true
use_dio: true
client:
class_name: "MyApiClient" # 自定义类名
file_name: "my_api_client" # 自定义文件名
# 生成的文件:
# - lib/generated/api/my_api_client.dart
#
# 生成的类:
# class MyApiClient {
# final Dio _dio;
# ...
# }
---
# 示例 3: 项目特定的命名
# 根据项目名称自定义
generation:
api:
enabled: true
use_retrofit: true
use_dio: true
client:
class_name: "ShopApiClient"
file_name: "shop_api_client"
# 生成的文件:
# - lib/generated/api/shop_api_client.dart
#
# 生成的类:
# class ShopApiClient {
# final Dio _dio;
# ...
# }
---
# 示例 4: 模块化命名
# 适用于大型项目,按模块划分
generation:
api:
enabled: true
use_retrofit: true
use_dio: true
client:
class_name: "UserModuleApi"
file_name: "user_module_api"
# 生成的文件:
# - lib/generated/api/user_module_api.dart
#
# 生成的类:
# class UserModuleApi {
# final Dio _dio;
# ...
# }
---
# 示例 5: 完整配置示例
# 包含所有相关配置
generator:
name: "my_project_generator"
version: "1.0"
author: "Your Name"
copyright: "Copyright (C) 2025 Your Company. All rights reserved."
input:
swagger_urls:
- "https://your-api.com/swagger/v2/swagger.json"
output:
base_dir: "./lib/generated"
api_dir: "./lib/generated/api"
models_dir: "./lib/generated/api_models"
split_by_tags: true
generation:
api:
enabled: true
use_retrofit: true
use_dio: true
parser: "JsonSerializable"
# 自定义 API Client
client:
class_name: "AppApiClient"
file_name: "app_api_client"
# 使用方式:
# 1. 复制此配置到项目根目录的 generator_config.yaml
# 2. 运行生成命令:
# dart run swagger_generator_flutter generate --all
# 3. 生成的主 API 文件:
# lib/generated/api/app_api_client.dart
# 4. 在代码中使用:
# import 'package:your_app/generated/api/app_api_client.dart';
#
# final dio = Dio();
# final apiClient = AppApiClient(dio);
#
# // 访问 V2 版本的 API
# final result = await apiClient.mobileManagerV2.getData();

View File

@ -1,196 +0,0 @@
# included_tags 使用示例
## 场景 1: 只生成移动端管理相关的 API
假设您的项目中有多个模块但您只想生成移动端管理MobileManager相关的代码
### CLI 方式
```bash
# 只生成 MobileManager 相关的 API 和模型
dart run swagger_generator_flutter generate --all --included-tags=MobileManager
# 查看生成的文件
ls -la generator/api/
ls -la generator/api_models/
```
### 配置文件方式
`generator_config.yaml` 中:
```yaml
output:
base_dir: "./lib/generated"
api_dir: "./lib/generated/api"
models_dir: "./lib/generated/api_models"
split_by_tags: true
# 只生成 MobileManager 相关代码
included_tags:
- "MobileManager"
```
然后运行:
```bash
dart run swagger_generator_flutter generate --all
```
## 场景 2: 生成多个模块
如果您需要生成多个模块的代码:
```bash
# 生成 MobileManager 和 TaskSummarize 两个模块
dart run swagger_generator_flutter generate --all --included-tags=MobileManager,TaskSummarize
```
或在配置文件中:
```yaml
output:
included_tags:
- "MobileManager"
- "TaskSummarize"
```
## 场景 3: 增量开发
在开发过程中,您可能想先实现部分功能:
### 第一阶段:只实现移动端管理
```bash
dart run swagger_generator_flutter generate --all --included-tags=MobileManager
```
### 第二阶段:添加任务汇总功能
```bash
dart run swagger_generator_flutter generate --all --included-tags=MobileManager,TaskSummarize
```
### 第三阶段:生成所有功能
```bash
# 不指定 included-tags生成所有
dart run swagger_generator_flutter generate --all
```
## 场景 4: 团队协作
不同团队负责不同模块:
### 移动端团队
```bash
# 只生成移动端相关代码
dart run swagger_generator_flutter generate --all --included-tags=MobileManager \
--output-dir=lib/api/mobile
```
### 任务管理团队
```bash
# 只生成任务相关代码
dart run swagger_generator_flutter generate --all --included-tags=TaskSummarize \
--output-dir=lib/api/task
```
## 场景 5: 测试和调试
在开发新功能时,快速生成特定模块的代码进行测试:
```bash
# 快速生成测试代码
dart run swagger_generator_flutter generate --all --included-tags=MobileManager \
--output-dir=test_output
# 测试完成后删除
rm -rf test_output
```
## 预期结果
### 使用 `--included-tags=MobileManager`
**生成的 API 文件:**
- `generator/api/mobile_manager_api.dart` - MobileManager 相关的 API 接口
**生成的 Model 文件:**
- 只包含 MobileManager API 引用的 models
- 自动包含这些 models 的依赖 models
**不会生成:**
- 其他 tags 的 API 文件
- 未被 MobileManager 引用的 models
### 使用 `--included-tags=MobileManager,TaskSummarize`
**生成的 API 文件:**
- `generator/api/mobile_manager_api.dart`
- `generator/api/task_summarize_api.dart`
**生成的 Model 文件:**
- MobileManager 和 TaskSummarize 引用的所有 models
- 以及它们的依赖 models
## 性能对比
假设完整的 Swagger 文档有 100 个 endpoints 和 200 个 models
| 场景 | Endpoints | Models | 生成时间 | 代码量 |
|------|-----------|--------|----------|--------|
| 全部生成 | 100 | 200 | ~10s | ~500KB |
| 只生成 MobileManager | ~20 | ~40 | ~2s | ~100KB |
| 生成 2 个 tags | ~40 | ~80 | ~4s | ~200KB |
**优势:**
- ⚡ 生成速度提升 5 倍
- 📦 代码量减少 80%
- 🎯 更专注于当前模块
- 🔧 更容易调试和维护
## 注意事项
1. **Tag 名称必须精确匹配**
```bash
# ✅ 正确
--included-tags=MobileManager
# ❌ 错误(大小写不匹配)
--included-tags=mobilemanager
--included-tags=mobile_manager
```
2. **多个 tags 用逗号分隔,不要有空格**
```bash
# ✅ 正确
--included-tags=MobileManager,TaskSummarize
# ❌ 错误(有空格)
--included-tags=MobileManager, TaskSummarize
```
3. **依赖会自动包含**
- 不需要手动指定依赖的 models
- 系统会自动追踪所有引用关系
4. **与 split-by-tags 兼容**
- `included_tags``split-by-tags` 可以同时使用
- `split-by-tags` 控制是否分文件
- `included_tags` 控制生成哪些 tags
## 查看可用的 Tags
如果不确定 Swagger 文档中有哪些 tags可以使用以下命令查看
```bash
# 使用 jq 查看所有 tags
cat swagger.json | jq -r '.paths | to_entries[] | .value | to_entries[] | .value.tags[]?' | sort -u
# 或者查看 tags 定义
cat swagger.json | jq '.tags'
```

130
fix_cascades.py Normal file
View File

@ -0,0 +1,130 @@
#!/usr/bin/env python3
"""
自动修复 Dart 代码中的 cascade_invocations lint 问题
将连续的 buffer.writeln() buffer.write() 调用转换为使用级联操作符 (..)
"""
import re
import sys
from pathlib import Path
def fix_cascade_invocations(content: str) -> str:
"""修复 cascade_invocations 问题"""
lines = content.split('\n')
result = []
i = 0
while i < len(lines):
line = lines[i]
# 检测是否是 buffer.writeln() 或 buffer.write() 调用
if re.match(r'\s+buffer\.(writeln|write)\(', line):
# 收集连续的 buffer 调用
buffer_calls = [line]
indent = len(line) - len(line.lstrip())
j = i + 1
# 查找连续的 buffer 调用
while j < len(lines):
next_line = lines[j]
# 跳过空行和注释
if not next_line.strip() or next_line.strip().startswith('//'):
j += 1
continue
# 检查是否是相同缩进的 buffer 调用
if re.match(r'\s+buffer\.(writeln|write)\(', next_line):
next_indent = len(next_line) - len(next_line.lstrip())
if next_indent == indent:
buffer_calls.append(next_line)
j += 1
else:
break
else:
break
# 如果有多个连续的 buffer 调用,转换为级联
if len(buffer_calls) >= 2:
# 第一行改为 buffer
first_line = buffer_calls[0]
indent_str = ' ' * indent
# 提取第一个调用
match = re.match(r'(\s+)buffer\.(writeln|write)\((.*)\);?', first_line)
if match:
method = match.group(2)
args = match.group(3)
result.append(f'{indent_str}buffer')
result.append(f'{indent_str} ..{method}({args})')
# 处理后续调用
for call in buffer_calls[1:]:
match = re.match(r'\s+buffer\.(writeln|write)\((.*)\);?', call)
if match:
method = match.group(1)
args = match.group(2)
result.append(f'{indent_str} ..{method}({args})')
# 添加分号
result[-1] = result[-1].rstrip() + ';'
i = j
continue
result.append(line)
i += 1
return '\n'.join(result)
def process_file(file_path: Path) -> bool:
"""处理单个文件"""
try:
content = file_path.read_text(encoding='utf-8')
fixed_content = fix_cascade_invocations(content)
if content != fixed_content:
file_path.write_text(fixed_content, encoding='utf-8')
print(f'✓ Fixed: {file_path}')
return True
else:
print(f'- Skipped: {file_path} (no changes needed)')
return False
except Exception as e:
print(f'✗ Error processing {file_path}: {e}')
return False
def main():
"""主函数"""
if len(sys.argv) < 2:
print('Usage: python fix_cascades.py <file_or_directory>')
sys.exit(1)
path = Path(sys.argv[1])
if not path.exists():
print(f'Error: {path} does not exist')
sys.exit(1)
files_to_process = []
if path.is_file():
files_to_process = [path]
else:
# 递归查找所有 .dart 文件
files_to_process = list(path.rglob('*.dart'))
print(f'Processing {len(files_to_process)} files...\n')
fixed_count = 0
for file_path in files_to_process:
if process_file(file_path):
fixed_count += 1
print(f'\n✓ Fixed {fixed_count} files')
if __name__ == '__main__':
main()

View File

@ -133,6 +133,56 @@ generation:
# 构造函数配置
use_const_constructor: true
required_for_non_nullable: true
# 枚举键名映射配置(可选)
# 用于为枚举值定义有意义的键名和描述
# 优先级:配置文件映射 > x-enum-varnames > 智能生成
#
# 使用场景:
# 1. 后端不支持 x-enum-varnames 扩展字段
# 2. 需要覆盖 Swagger 文档中的枚举键名
# 3. 需要为枚举值添加自定义描述
#
# 格式:
# enum_key_mappings:
# 枚举名称:
# - value: 枚举值(数字或字符串)
# name: 枚举键名(大写下划线命名)
# description: 枚举描述(可选)
#
# 示例:
# enum_key_mappings:
# SysTaskTypeEnums:
# - value: 1
# name: SPOT_CHECK
# description: 抽查
# - value: 2
# name: CULTURAL
# description: 文创建设
# - value: 3
# name: CLASS_CADRE_MEETING
# description: 班干部会议
#
# UserStatus:
# - value: "active"
# name: ACTIVE
# description: 活跃用户
# - value: "inactive"
# name: INACTIVE
# description: 非活跃用户
# - value: "banned"
# name: BANNED
# description: 已封禁
#
enum_key_mappings:
# 在此处添加您的枚举映射配置
# SysTaskTypeEnums:
# - value: 1
# name: SPOT_CHECK
# description: 抽查
# - value: 2
# name: CULTURAL
# description: 文创建设
# 类型映射配置
type_mapping:

View File

@ -1,4 +1,5 @@
import '../core/exceptions.dart';
import 'package:swagger_generator_flutter/core/exceptions.dart';
import 'package:swagger_generator_flutter/utils/logger.dart';
///
///
@ -23,32 +24,38 @@ abstract class BaseCommand {
///
void showHelp() {
print('');
print('命令: $name');
print('描述: $description');
print('用法: $usage');
final buffer = StringBuffer()
..writeln()
..writeln('命令: $name')
..writeln('描述: $description')
..writeln('用法: $usage');
if (arguments.isNotEmpty) {
print('');
print('参数:');
buffer
..writeln()
..writeln('参数:');
for (final arg in arguments) {
final required = arg.required ? '(必填)' : '(可选)';
print(' ${arg.name} ${arg.description} $required');
buffer.writeln(' ${arg.name} ${arg.description} $required');
}
}
if (options.isNotEmpty) {
print('');
print('选项:');
buffer
..writeln()
..writeln('选项:');
for (final option in options) {
final short = option.shortName != null ? '-${option.shortName}, ' : '';
final defaultValue =
option.defaultValue != null ? ' (默认: ${option.defaultValue})' : '';
print(' $short--${option.name} ${option.description}$defaultValue');
buffer.writeln(
' $short--${option.name} ${option.description}$defaultValue',
);
}
}
print('');
buffer.writeln();
appLogger.info(buffer.toString());
}
///
@ -87,74 +94,63 @@ abstract class BaseCommand {
///
void handleError(dynamic error, StackTrace stackTrace) {
if (error is CommandException) {
print('❌ 错误: ${error.message}');
if (error.details != null) {
print('详细信息: ${error.details}');
}
appLogger.severe(
'❌ 错误: ${error.message}',
error.details,
stackTrace,
);
} else if (error is SwaggerException) {
print('❌ Swagger错误: ${error.message}');
if (error.details != null) {
print('详细信息: ${error.details}');
}
} else {
print('❌ 未知错误: $error');
print('堆栈跟踪: $stackTrace');
appLogger.severe(
'❌ Swagger错误: ${error.message}',
error.details,
stackTrace,
);
}
}
///
void success(String message) {
print('$message');
}
void success(String message) => appLogger.info('$message');
///
void info(String message) {
print(' $message');
}
void info(String message) => appLogger.info(' $message');
///
void warning(String message) {
print('⚠️ $message');
}
void warning(String message) => appLogger.warning('⚠️ $message');
///
void progress(String message) {
print('🔄 $message');
}
void progress(String message) => appLogger.info('🔄 $message');
}
///
class CommandOption {
const CommandOption({
required this.name,
required this.description,
this.shortName,
this.required = false,
this.defaultValue,
this.type = OptionType.flag,
});
final String name;
final String? shortName;
final String description;
final bool required;
final dynamic defaultValue;
final OptionType type;
const CommandOption({
required this.name,
this.shortName,
required this.description,
this.required = false,
this.defaultValue,
this.type = OptionType.flag,
});
}
///
class CommandArgument {
final String name;
final String description;
final bool required;
final dynamic defaultValue;
const CommandArgument({
required this.name,
required this.description,
this.required = true,
this.defaultValue,
});
final String name;
final String description;
final bool required;
final dynamic defaultValue;
}
///
@ -204,16 +200,15 @@ class ParsedArguments {
///
class ArgumentParser {
final BaseCommand command;
ArgumentParser(this.command);
final BaseCommand command;
///
ParsedArguments parse(List<String> args) {
final result = ParsedArguments();
final argQueue = List<String>.from(args);
int argumentIndex = 0;
var argumentIndex = 0;
while (argQueue.isNotEmpty) {
final current = argQueue.removeAt(0);
@ -343,11 +338,10 @@ class ArgumentParser {
///
class CommandException implements Exception {
const CommandException(this.message, {this.details});
final String message;
final String? details;
const CommandException(this.message, {this.details});
@override
String toString() {
return 'CommandException: $message${details != null ? ' ($details)' : ''}';

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,211 @@
import 'package:swagger_generator_flutter/commands/services/service_typedefs.dart';
import 'package:swagger_generator_flutter/core/models.dart';
class DocumentFilterService {
SwaggerDocument filter({
required SwaggerDocument document,
List<String>? includedTags,
List<String>? excludedTags,
LogCallback? progress,
}) {
final hasIncludes = includedTags != null && includedTags.isNotEmpty;
final hasExcludes = excludedTags != null && excludedTags.isNotEmpty;
if (!hasIncludes && !hasExcludes) {
return document;
}
progress?.call('🔍 正在根据 tags 过滤文档...');
if (hasIncludes) {
progress?.call(' 只保留 tags: ${includedTags.join(", ")}');
}
if (hasExcludes) {
progress?.call(' 排除 tags: ${excludedTags.join(", ")}');
}
final filteredPaths = <ApiPathKey, ApiPath>{};
final usedModelNames = <String>{};
for (final entry in document.paths.entries) {
final path = entry.value;
final pathTags = path.tags;
final included =
!hasIncludes || pathTags.any((tag) => includedTags.contains(tag));
if (!included) {
continue;
}
final excluded = hasExcludes &&
pathTags.isNotEmpty &&
pathTags.every((tag) => excludedTags.contains(tag));
if (excluded) {
continue;
}
filteredPaths[entry.key] = path;
_collectUsedModels(path, usedModelNames);
}
progress
?.call(' 保留了 ${filteredPaths.length}/${document.paths.length} 个接口');
final filteredModels = _filterModels(document, usedModelNames);
progress
?.call(' 保留了 ${filteredModels.length}/${document.models.length} 个模型');
final filteredControllers = <String, ApiController>{};
for (final entry in document.controllers.entries) {
final tagName = entry.key;
var shouldKeep = true;
if (hasIncludes && !includedTags.contains(tagName)) {
shouldKeep = false;
}
if (hasExcludes && excludedTags.contains(tagName)) {
shouldKeep = false;
}
if (shouldKeep) {
filteredControllers[tagName] = entry.value;
}
}
progress?.call(
' 保留了 ${filteredControllers.length}/${document.controllers.length} 个控制器',
);
return SwaggerDocument(
title: document.title,
version: document.version,
description: document.description,
servers: document.servers,
components: document.components,
paths: filteredPaths,
models: filteredModels,
controllers: filteredControllers,
security: document.security,
);
}
Map<String, ApiModel> _filterModels(
SwaggerDocument document,
Set<String> usedModelNames,
) {
final filteredModels = <String, ApiModel>{};
final modelsToCheck = Set<String>.from(usedModelNames);
final checkedModels = <String>{};
while (modelsToCheck.isNotEmpty) {
final modelName = modelsToCheck.first;
modelsToCheck.remove(modelName);
if (checkedModels.contains(modelName)) {
continue;
}
checkedModels.add(modelName);
final model = document.models[modelName];
if (model != null) {
filteredModels[modelName] = model;
_collectModelDependencies(model, modelsToCheck, checkedModels);
}
}
return filteredModels;
}
void _collectUsedModels(ApiPath path, Set<String> usedModelNames) {
void extractModelsFromSchema(Map<String, dynamic> schema) {
if (schema.containsKey(r'$ref')) {
final modelName = _extractModelNameFromRef(schema[r'$ref'] as String);
if (modelName != null) {
usedModelNames.add(modelName);
}
return;
}
if (schema.containsKey('type')) {
final type = schema['type'];
if (type == 'array' && schema.containsKey('items')) {
extractModelsFromSchema(schema['items'] as Map<String, dynamic>);
} else if (type == 'object' && schema.containsKey('properties')) {
final properties = schema['properties'] as Map<String, dynamic>;
for (final propSchema in properties.values) {
extractModelsFromSchema(propSchema as Map<String, dynamic>);
}
}
}
for (final key in ['allOf', 'anyOf', 'oneOf']) {
if (schema.containsKey(key)) {
final subSchemas = schema[key] as List<dynamic>;
for (final subSchema in subSchemas) {
extractModelsFromSchema(subSchema as Map<String, dynamic>);
}
}
}
}
if (path.requestBody != null) {
for (final mediaType in path.requestBody!.content.values) {
if (mediaType.schema != null) {
extractModelsFromSchema(mediaType.schema!);
}
}
}
for (final response in path.responses.values) {
for (final mediaType in response.content.values) {
if (mediaType.schema != null) {
extractModelsFromSchema(mediaType.schema!);
}
}
}
}
void _collectModelDependencies(
ApiModel model,
Set<String> modelsToCheck,
Set<String> checkedModels,
) {
for (final property in model.properties.values) {
if (property.reference != null &&
!checkedModels.contains(property.reference)) {
modelsToCheck.add(property.reference!);
}
if (property.type == PropertyType.array && property.items != null) {
final itemsName = property.items!.name;
if (itemsName.isNotEmpty && !checkedModels.contains(itemsName)) {
modelsToCheck.add(itemsName);
}
}
for (final nestedProp in property.nestedProperties.values) {
if (nestedProp.reference != null &&
!checkedModels.contains(nestedProp.reference)) {
modelsToCheck.add(nestedProp.reference!);
}
}
}
for (final schema in [...model.allOf, ...model.oneOf, ...model.anyOf]) {
if (schema.reference != null) {
final modelName = _extractModelNameFromRef(schema.reference!);
if (modelName != null && !checkedModels.contains(modelName)) {
modelsToCheck.add(modelName);
}
}
}
}
String? _extractModelNameFromRef(String ref) {
if (ref.startsWith('#/components/schemas/')) {
return ref.substring('#/components/schemas/'.length);
}
if (ref.startsWith('#/definitions/')) {
return ref.substring('#/definitions/'.length);
}
return null;
}
}

View File

@ -0,0 +1,76 @@
import 'package:swagger_generator_flutter/commands/services/service_typedefs.dart';
import 'package:swagger_generator_flutter/core/models.dart';
import 'package:swagger_generator_flutter/pipeline/parse/swagger_data_parser.dart';
class DocumentMergeService {
DocumentMergeService({SwaggerDataParser? parser})
: _parser = parser ?? SwaggerDataParser();
final SwaggerDataParser _parser;
Future<SwaggerDocument?> fetchAndMerge(
List<String> urls, {
LogCallback? progress,
}) async {
SwaggerDocument? mergedDocument;
for (var i = 0; i < urls.length; i++) {
final url = urls[i];
progress?.call(' [${i + 1}/${urls.length}] 正在解析: $url');
final doc = await _parser.fetchAndParseSwaggerDocument(url);
progress?.call(
' 解析完成: ${doc.models.length} 个模型, ${doc.paths.length} 个路径',
);
if (mergedDocument == null) {
mergedDocument = doc;
progress?.call(' 初始文档: ${doc.models.length} 个模型');
continue;
}
final beforeModelCount = mergedDocument.models.length;
final currentModelCount = doc.models.length;
final overlappingModels = <String>[];
for (final key in doc.models.keys) {
if (mergedDocument.models.containsKey(key)) {
overlappingModels.add(key);
}
}
if (overlappingModels.isNotEmpty) {
progress?.call(
' 发现 ${overlappingModels.length} 个同名模型将被覆盖: '
'${overlappingModels.take(5).join(", ")}'
'${overlappingModels.length > 5 ? "..." : ""}',
);
}
mergedDocument = SwaggerDocument(
title: mergedDocument.title,
description: mergedDocument.description,
version: '${mergedDocument.version} + ${doc.version}',
paths: {...mergedDocument.paths, ...doc.paths},
models: {...mergedDocument.models, ...doc.models},
controllers: {...mergedDocument.controllers, ...doc.controllers},
);
final afterModelCount = mergedDocument.models.length;
progress?.call(
' 合并后: $beforeModelCount + $currentModelCount '
'-> $afterModelCount 个模型',
);
if (overlappingModels.isNotEmpty) {
progress?.call(
' 同名模型列表: '
'${overlappingModels.take(10).join(", ")}'
'${overlappingModels.length > 10 ? "..." : ""}',
);
}
}
return mergedDocument;
}
}

View File

@ -0,0 +1,4 @@
/// Backward-compat shim for GenerationOutputService
library;
export 'package:swagger_generator_flutter/pipeline/output/impl/generation_output_service.dart';

View File

@ -0,0 +1 @@
typedef LogCallback = void Function(String message);

View File

@ -1,4 +1,4 @@
import 'config_loader.dart';
import 'package:swagger_generator_flutter/core/config_repository.dart';
/// Swagger配置管理
/// Swagger相关的配置项
@ -13,7 +13,9 @@ class SwaggerConfig {
/// Swagger JSON URLs
/// 使
static List<String> get swaggerJsonUrls {
return ConfigLoader.getSwaggerUrls();
// Keep public API but delegate to ConfigRepository
final config = ConfigRepository.loadSync();
return config.swaggerUrls;
}
/// API URL
@ -32,19 +34,25 @@ class SwaggerConfig {
static const String defaultModelsDir = 'api_models';
///
static String get generatorDir => ConfigLoader.getBaseDir();
static String get generatorDir => ConfigRepository.loadSync().baseDir;
/// API文件目录
static String get apiDir => ConfigLoader.getApiDir();
static String get apiDir => ConfigRepository.loadSync().apiDir;
///
static String get modelsDir => ConfigLoader.getModelsDir();
static String get modelsDir => ConfigRepository.loadSync().modelsDir;
/// BaseResult
static String get baseResultImport => ConfigLoader.getBaseResultImport();
static String get baseResultImport =>
ConfigRepository.loadSync().baseResultImport;
/// BasePageResult
static String get basePageResultImport => ConfigLoader.getBasePageResultImport();
static String get basePageResultImport =>
ConfigRepository.loadSync().basePageResultImport;
///
static Map<String, Map<dynamic, EnumKeyMapping>>? get enumKeyMappings =>
ConfigRepository.loadSync().enumKeyMappings;
///
static const String defaultDocumentationFile =

View File

@ -0,0 +1,221 @@
rules:
# 基础结构错误
- id: MISSING_OPENAPI_VERSION
pattern: openapi
severity: critical
category: syntax
title: Missing OpenAPI Version
description: OpenAPI document must specify the OpenAPI version.
suggestions:
- 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
- id: INVALID_OPENAPI_VERSION
pattern: openapi
severity: error
category: compatibility
title: Invalid OpenAPI Version
description: OpenAPI version should be 3.0.x or 3.1.x for proper support.
suggestions:
- description: Use a supported OpenAPI version
codeExample: '"openapi": "3.0.3"'
# Info 对象错误
- id: MISSING_INFO_TITLE
pattern: info.title
severity: error
category: validation
title: Missing API Title
description: API title is required in the info object.
suggestions:
- description: Add a descriptive title for your API
codeExample: '"title": "My API"'
- id: MISSING_INFO_VERSION
pattern: info.version
severity: error
category: validation
title: Missing API Version
description: API version is required in the info object.
suggestions:
- description: Add a version number using semantic versioning
codeExample: '"version": "1.0.0"'
documentationUrl: https://semver.org/
# Paths 错误
- id: EMPTY_PATHS
pattern: paths
severity: error
category: validation
title: Empty Paths Object
description: OpenAPI document must contain at least one path.
suggestions:
- description: Add at least one API endpoint
codeExample: '"/users": { "get": { "responses": { "200": { "description": "Success" } } } }'
- id: INVALID_PATH_FORMAT
pattern: paths.*
severity: error
category: syntax
title: Invalid Path Format
description: Path must start with a forward slash.
suggestions:
- description: Ensure path starts with /
codeExample: '"/users" instead of "users"'
- id: MISSING_OPERATION_RESPONSES
pattern: paths.*.*.responses
severity: error
category: validation
title: Missing Operation Responses
description: Every operation must define at least one response.
suggestions:
- description: Add at least a default response
codeExample: '"responses": { "200": { "description": "Success" } }'
# 参数错误
- id: PATH_PARAMETER_NOT_REQUIRED
pattern: paths.*.*.parameters.*
severity: error
category: validation
title: Path Parameter Not Required
description: Path parameters must be marked as required.
suggestions:
- description: Set required: true for path parameters
codeExample: '"required": true'
- id: MISSING_PARAMETER_NAME
pattern: paths.*.*.parameters.*.name
severity: error
category: validation
title: Missing Parameter Name
description: Parameter must have a name.
suggestions:
- description: Add a name for the parameter
codeExample: '"name": "userId"'
# Schema 错误
- id: MISSING_SCHEMA_TYPE
pattern: components.schemas.*.type
severity: warning
category: schema
title: Missing Schema Type
description: Schema should specify a type for better code generation.
suggestions:
- description: Add a type field to the schema
codeExample: '"type": "object"'
- id: CIRCULAR_REFERENCE
pattern: components.schemas.*
severity: warning
category: schema
title: Circular Reference Detected
description: Circular references can cause issues in code generation.
suggestions:
- description: Consider using allOf or breaking the circular dependency
codeExample: '"allOf": [{ "$ref": "#/components/schemas/BaseModel" }]'
# 安全方案错误
- id: MISSING_SECURITY_SCHEME_TYPE
pattern: components.securitySchemes.*.type
severity: error
category: security
title: Missing Security Scheme Type
description: Security scheme must specify a type.
suggestions:
- description: Add a type field (apiKey, http, oauth2, openIdConnect)
codeExample: '"type": "apiKey"'
- id: MISSING_API_KEY_NAME
pattern: components.securitySchemes.*.name
severity: error
category: security
title: Missing API Key Name
description: API Key security scheme must specify a parameter name.
suggestions:
- description: Add name field for API key parameter
codeExample: '"name": "X-API-Key"'
- id: MISSING_API_KEY_LOCATION
pattern: components.securitySchemes.*.in
severity: error
category: security
title: Missing API Key Location
description: API Key security scheme must specify where the key is located.
suggestions:
- description: Add in field (query, header, cookie)
codeExample: '"in": "header"'
# 响应错误
- id: MISSING_RESPONSE_DESCRIPTION
pattern: paths.*.*.responses.*.description
severity: warning
category: bestPractice
title: Missing Response Description
description: Response should have a description.
suggestions:
- description: Add a description for the response
codeExample: '"description": "Successful operation"'
- id: NO_SUCCESS_RESPONSE
pattern: paths.*.*.responses
severity: warning
category: bestPractice
title: No Success Response
description: Operation should define at least one success response (2xx).
suggestions:
- description: Add a success response
codeExample: '"200": { "description": "Success" }'
# 性能和最佳实践
- id: MISSING_OPERATION_ID
pattern: paths.*.*.operationId
severity: warning
category: bestPractice
title: Missing Operation ID
description: Operation should have an operationId for better code generation.
suggestions:
- description: Add a unique operationId
codeExample: '"operationId": "getUsers"'
- id: MISSING_OPERATION_SUMMARY
pattern: paths.*.*.summary
severity: info
category: bestPractice
title: Missing Operation Summary
description: Operation should have a summary for better documentation.
suggestions:
- description: Add a brief summary
codeExample: '"summary": "Get all users"'
- id: LARGE_SCHEMA_OBJECT
pattern: components.schemas.*
severity: info
category: performance
title: Large Schema Object
description: Schema has many properties, consider breaking it down.
suggestions:
- description: Consider using composition with allOf
codeExample: '"allOf": [{ "$ref": "#/components/schemas/BaseModel" }, { "type": "object", "properties": {...} }]'
# 媒体类型错误
- id: MISSING_CONTENT_TYPE
pattern: paths.*.*.requestBody.content
severity: warning
category: validation
title: Missing Content Type
description: Request body should specify at least one content type.
suggestions:
- description: Add a content type
codeExample: '"application/json": { "schema": {...} }'
- id: INCONSISTENT_CONTENT_TYPES
pattern: paths.*
severity: info
category: bestPractice
title: Inconsistent Content Types
description: API uses different content types across operations.
suggestions:
- description: Consider standardizing on common content types
codeExample: Use application/json consistently

View File

@ -1,576 +0,0 @@
import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:yaml/yaml.dart';
import 'config.dart';
///
/// generator_config.yaml
class ConfigLoader {
static Map<String, dynamic>? _cachedConfig;
static String? _configPath;
/// YAML Dart Map
static Map<String, dynamic> _yamlToMap(dynamic yaml) {
if (yaml is YamlMap) {
final result = <String, dynamic>{};
yaml.forEach((key, value) {
final keyStr = key.toString();
if (value is YamlMap) {
result[keyStr] = _yamlToMap(value);
} else if (value is YamlList) {
result[keyStr] = _yamlToList(value);
} else {
result[keyStr] = value;
}
});
return result;
}
return {};
}
/// YAML Dart List
static List<dynamic> _yamlToList(YamlList yamlList) {
return yamlList.map((item) {
if (item is YamlMap) {
return _yamlToMap(item);
} else if (item is YamlList) {
return _yamlToList(item);
} else {
return item;
}
}).toList();
}
///
/// [configPath] generator_config.yaml
static Map<String, dynamic>? loadConfig([String? configPath]) {
// 使
if (_cachedConfig != null && configPath == _configPath) {
return _cachedConfig;
}
_configPath = configPath ?? _findConfigFile();
if (_configPath == null) {
return null;
}
try {
final file = File(_configPath!);
if (!file.existsSync()) {
return null;
}
final content = file.readAsStringSync();
final yaml = loadYaml(content);
_cachedConfig = _yamlToMap(yaml);
return _cachedConfig;
} catch (e) {
print('⚠️ 配置文件解析失败: $e');
return null;
}
}
///
/// generator_config.yaml
static String? _findConfigFile() {
var currentDir = Directory.current;
final maxDepth = 10; // 10
var depth = 0;
while (depth < maxDepth) {
final configFile =
File(path.join(currentDir.path, 'generator_config.yaml'));
if (configFile.existsSync()) {
return configFile.path;
}
final parent = currentDir.parent;
if (parent.path == currentDir.path) {
//
break;
}
currentDir = parent;
depth++;
}
return null;
}
///
static void clearCache() {
_cachedConfig = null;
_configPath = null;
}
/// Swagger URLs
/// swagger_urls ()
/// : ["url1", "url2"]
/// : [{url: "...", enabled: true}]
///
/// URL
/// Swagger
/// V2 V1
static List<String> getSwaggerUrls([Map<String, dynamic>? config]) {
final cfg = config ?? loadConfig();
if (cfg == null) {
return SwaggerConfig.defaultSwaggerJsonUrls;
}
final input = cfg['input'] as Map<String, dynamic>?;
if (input == null) {
return SwaggerConfig.defaultSwaggerJsonUrls;
}
// swagger_urls ()
if (!input.containsKey('swagger_urls')) {
return SwaggerConfig.defaultSwaggerJsonUrls;
}
final urls = input['swagger_urls'];
if (urls is! List) {
return SwaggerConfig.defaultSwaggerJsonUrls;
}
final result = <String>[];
for (final item in urls) {
if (item is String) {
// : ["url1", "url2"]
result.add(item);
} else if (item is Map) {
// : [{url: "...", enabled: true}]
final enabled = item['enabled'] as bool? ?? true;
if (enabled) {
final url = item['url'] as String?;
if (url != null && url.isNotEmpty) {
result.add(url);
}
}
}
}
return result.isNotEmpty ? result : SwaggerConfig.defaultSwaggerJsonUrls;
}
///
///
static List<String> getIgnoredDirectories([Map<String, dynamic>? config]) {
final cfg = config ?? loadConfig();
if (cfg == null) {
return [];
}
final output = cfg['output'] as Map<String, dynamic>?;
if (output == null) {
return [];
}
final ignoredDirs = output['ignored_directories'];
if (ignoredDirs is List) {
return ignoredDirs.map((item) => item.toString()).toList();
}
return [];
}
///
///
static List<String> getIgnoredFiles([Map<String, dynamic>? config]) {
final cfg = config ?? loadConfig();
if (cfg == null) {
return [];
}
final output = cfg['output'] as Map<String, dynamic>?;
if (output == null) {
return [];
}
final ignoredFiles = output['ignored_files'];
if (ignoredFiles is List) {
return ignoredFiles.map((item) => item.toString()).toList();
}
return [];
}
///
///
static bool shouldSkipFile(String filePath, [Map<String, dynamic>? config]) {
// 1.
final ignoredDirs = getIgnoredDirectories(config);
if (ignoredDirs.isNotEmpty) {
// 使
final normalizedPath = filePath.replaceAll('\\', '/');
for (final ignoredDir in ignoredDirs) {
//
var normalizedDir = ignoredDir.toString().replaceAll('\\', '/');
//
if (normalizedDir.endsWith('/')) {
normalizedDir = normalizedDir.substring(0, normalizedDir.length - 1);
}
//
if (normalizedDir.isEmpty) {
continue;
}
//
//
if (normalizedPath.contains(normalizedDir)) {
//
final dirWithSlash = '$normalizedDir/';
if (normalizedPath.startsWith(dirWithSlash) ||
normalizedPath.contains('/$dirWithSlash') ||
normalizedPath == normalizedDir) {
return true;
}
}
}
}
// 2.
final ignoredFiles = getIgnoredFiles(config);
if (ignoredFiles.isNotEmpty) {
final fileName = path.basename(filePath);
for (final ignoredFile in ignoredFiles) {
final ignoredFileName = ignoredFile.toString();
//
if (fileName == ignoredFileName) {
return true;
}
//
if (ignoredFileName.startsWith('*')) {
// *user_api.dart user_api.dart
final suffix = ignoredFileName.substring(1);
if (fileName.endsWith(suffix)) {
return true;
}
} else if (ignoredFileName.endsWith('*')) {
// user_api.dart* user_api.dart
final prefix =
ignoredFileName.substring(0, ignoredFileName.length - 1);
if (fileName.startsWith(prefix)) {
return true;
}
}
//
if (ignoredFileName.contains('*') &&
ignoredFileName.startsWith('*') &&
ignoredFileName.endsWith('*')) {
// *user* user
final pattern =
ignoredFileName.substring(1, ignoredFileName.length - 1);
if (fileName.contains(pattern)) {
return true;
}
}
}
}
return false;
}
///
/// : {fileName}, {fileType}, {swaggerUrl}, {generatorName}, {author}, {copyright}
static String? getFileHeaderTemplate([Map<String, dynamic>? config]) {
final cfg = config ?? loadConfig();
if (cfg == null) {
return null;
}
final templates = cfg['templates'] as Map<String, dynamic>?;
if (templates == null) {
return null;
}
return templates['file_header'] as String?;
}
///
static String getGeneratorName([Map<String, dynamic>? config]) {
final cfg = config ?? loadConfig();
if (cfg == null) {
return 'xy_swagger_generator';
}
final generator = cfg['generator'] as Map<String, dynamic>?;
if (generator == null) {
return 'xy_swagger_generator';
}
return generator['name'] as String? ?? 'xy_swagger_generator';
}
///
static String getAuthor([Map<String, dynamic>? config]) {
final cfg = config ?? loadConfig();
if (cfg == null) {
return 'max';
}
final generator = cfg['generator'] as Map<String, dynamic>?;
if (generator == null) {
return 'max';
}
return generator['author'] as String? ?? 'max';
}
///
static String getCopyright([Map<String, dynamic>? config]) {
final cfg = config ?? loadConfig();
if (cfg == null) {
return 'Copyright (C) 2025 YuanXuan. All rights reserved.';
}
final generator = cfg['generator'] as Map<String, dynamic>?;
if (generator == null) {
return 'Copyright (C) 2025 YuanXuan. All rights reserved.';
}
return generator['copyright'] as String? ??
'Copyright (C) 2025 YuanXuan. All rights reserved.';
}
///
static String getBaseDir([Map<String, dynamic>? config]) {
final cfg = config ?? loadConfig();
if (cfg == null) {
return SwaggerConfig.defaultGeneratorDir;
}
final output = cfg['output'] as Map<String, dynamic>?;
if (output == null) {
return SwaggerConfig.defaultGeneratorDir;
}
return output['base_dir'] as String? ?? SwaggerConfig.defaultGeneratorDir;
}
/// API
static String getApiDir([Map<String, dynamic>? config]) {
final cfg = config ?? loadConfig();
if (cfg == null) {
return SwaggerConfig.defaultApiDir;
}
final output = cfg['output'] as Map<String, dynamic>?;
if (output == null) {
return SwaggerConfig.defaultApiDir;
}
return output['api_dir'] as String? ?? SwaggerConfig.defaultApiDir;
}
///
static String getModelsDir([Map<String, dynamic>? config]) {
final cfg = config ?? loadConfig();
if (cfg == null) {
return SwaggerConfig.defaultModelsDir;
}
final output = cfg['output'] as Map<String, dynamic>?;
if (output == null) {
return SwaggerConfig.defaultModelsDir;
}
return output['models_dir'] as String? ?? SwaggerConfig.defaultModelsDir;
}
///
static String getVersionExtractionPattern([Map<String, dynamic>? config]) {
final cfg = config ?? loadConfig();
if (cfg == null) {
return r'/api/v(\d+)/'; //
}
final generation = cfg['generation'] as Map<String, dynamic>?;
if (generation == null) {
return r'/api/v(\d+)/';
}
final api = generation['api'] as Map<String, dynamic>?;
if (api == null) {
return r'/api/v(\d+)/';
}
final versionExtraction =
api['version_extraction'] as Map<String, dynamic>?;
if (versionExtraction == null) {
return r'/api/v(\d+)/';
}
return versionExtraction['pattern'] as String? ?? r'/api/v(\d+)/';
}
///
static String getDefaultVersion([Map<String, dynamic>? config]) {
final cfg = config ?? loadConfig();
if (cfg == null) {
return 'v1';
}
final generation = cfg['generation'] as Map<String, dynamic>?;
if (generation == null) {
return 'v1';
}
final api = generation['api'] as Map<String, dynamic>?;
if (api == null) {
return 'v1';
}
final versionExtraction =
api['version_extraction'] as Map<String, dynamic>?;
if (versionExtraction == null) {
return 'v1';
}
return versionExtraction['default_version'] as String? ?? 'v1';
}
/// BaseResult
static String getBaseResultImport([Map<String, dynamic>? config]) {
final cfg = config ?? loadConfig();
final generation = cfg?['generation'] as Map<String, dynamic>?;
final api = generation?['api'] as Map<String, dynamic>?;
return api?['base_result_import'] as String? ?? '';
}
/// BasePageResult
static String getBasePageResultImport([Map<String, dynamic>? config]) {
final cfg = config ?? loadConfig();
final generation = cfg?['generation'] as Map<String, dynamic>?;
final api = generation?['api'] as Map<String, dynamic>?;
return api?['base_page_result_import'] as String? ?? '';
}
/// API Client
/// : ApiClient
static String getApiClientClassName([Map<String, dynamic>? config]) {
final cfg = config ?? loadConfig();
if (cfg == null) {
return 'ApiClient';
}
final generation = cfg['generation'] as Map<String, dynamic>?;
if (generation == null) {
return 'ApiClient';
}
final api = generation['api'] as Map<String, dynamic>?;
if (api == null) {
return 'ApiClient';
}
final client = api['client'] as Map<String, dynamic>?;
if (client == null) {
return 'ApiClient';
}
return client['class_name'] as String? ?? 'ApiClient';
}
/// API Client .dart
/// : api_client
static String getApiClientFileName([Map<String, dynamic>? config]) {
final cfg = config ?? loadConfig();
if (cfg == null) {
return 'api_client';
}
final generation = cfg['generation'] as Map<String, dynamic>?;
if (generation == null) {
return 'api_client';
}
final api = generation['api'] as Map<String, dynamic>?;
if (api == null) {
return 'api_client';
}
final client = api['client'] as Map<String, dynamic>?;
if (client == null) {
return 'api_client';
}
return client['file_name'] as String? ?? 'api_client';
}
/// tags
/// output.included_tags
/// null tags
static List<String>? getIncludedTags([Map<String, dynamic>? config]) {
final cfg = config ?? loadConfig();
if (cfg == null) {
return null;
}
final output = cfg['output'] as Map<String, dynamic>?;
if (output == null) {
return null;
}
final includedTags = output['included_tags'];
if (includedTags is! List) {
return null;
}
final result = includedTags
.map((tag) => tag.toString().trim())
.where((tag) => tag.isNotEmpty)
.toList();
return result.isEmpty ? null : result;
}
/// tags
/// output.excluded_tags
/// null
static List<String>? getExcludedTags([Map<String, dynamic>? config]) {
final cfg = config ?? loadConfig();
if (cfg == null) {
return null;
}
final output = cfg['output'] as Map<String, dynamic>?;
if (output == null) {
return null;
}
final excludedTags = output['excluded_tags'];
if (excludedTags is! List) {
return null;
}
final result = excludedTags
.map((tag) => tag.toString().trim())
.where((tag) => tag.isNotEmpty)
.toList();
return result.isEmpty ? null : result;
}
/// tags API
/// output.split_by_tags
/// : true
static bool getSplitByTags([Map<String, dynamic>? config]) {
final cfg = config ?? loadConfig();
if (cfg == null) {
return true;
}
final output = cfg['output'] as Map<String, dynamic>?;
if (output == null) {
return true;
}
return output['split_by_tags'] as bool? ?? true;
}
}

View File

@ -0,0 +1,388 @@
import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:swagger_generator_flutter/core/config.dart';
import 'package:swagger_generator_flutter/utils/logger.dart';
import 'package:swagger_generator_flutter/utils/path_resolver.dart';
import 'package:yaml/yaml.dart';
///
class EnumKeyMapping {
const EnumKeyMapping({
required this.name,
this.description,
});
final String name;
final String? description;
}
///
///
class ConfigRepository {
ConfigRepository(this._config);
final Map<String, dynamic> _config;
///
static Future<ConfigRepository> load([String? configPath]) async {
final file = File(configPath ?? PathResolver.findConfigFile() ?? '');
if (!file.existsSync()) {
return ConfigRepository({});
}
try {
final content = await file.readAsString();
final yaml = loadYaml(content);
final map = _yamlToMap(yaml);
return ConfigRepository(map);
} on Exception catch (e) {
appLogger.warning('⚠️ 配置文件解析失败: $e');
return ConfigRepository({});
}
}
///
static ConfigRepository loadSync([String? configPath]) {
final file = File(configPath ?? PathResolver.findConfigFile() ?? '');
if (!file.existsSync()) {
return ConfigRepository({});
}
try {
final content = file.readAsStringSync();
final yaml = loadYaml(content);
final map = _yamlToMap(yaml);
return ConfigRepository(map);
} on Exception catch (e) {
appLogger.warning('⚠️ 配置文件解析失败: $e');
return ConfigRepository({});
}
}
/// YAML Dart Map
static Map<String, dynamic> _yamlToMap(dynamic yaml) {
if (yaml is YamlMap) {
final result = <String, dynamic>{};
yaml.forEach((key, value) {
final keyStr = key.toString();
if (value is YamlMap) {
result[keyStr] = _yamlToMap(value);
} else if (value is YamlList) {
result[keyStr] = _yamlToList(value);
} else {
result[keyStr] = value;
}
});
return result;
}
return {};
}
/// YAML Dart List
static List<dynamic> _yamlToList(YamlList yamlList) {
return yamlList.map((item) {
if (item is YamlMap) {
return _yamlToMap(item);
} else if (item is YamlList) {
return _yamlToList(item);
} else {
return item;
}
}).toList();
}
/// Swagger URLs
List<String> get swaggerUrls {
final input = _config['input'] as Map<String, dynamic>?;
if (input == null) {
return SwaggerConfig.defaultSwaggerJsonUrls;
}
if (!input.containsKey('swagger_urls')) {
return SwaggerConfig.defaultSwaggerJsonUrls;
}
final urls = input['swagger_urls'];
if (urls is! List) {
return SwaggerConfig.defaultSwaggerJsonUrls;
}
final result = <String>[];
for (final item in urls) {
if (item is String) {
result.add(_normalizeSwaggerUrl(item));
} else if (item is Map) {
final enabled = item['enabled'] as bool? ?? true;
if (enabled) {
final url = item['url'] as String?;
if (url != null && url.isNotEmpty) {
result.add(_normalizeSwaggerUrl(url));
}
}
}
}
return result.isNotEmpty ? result : SwaggerConfig.defaultSwaggerJsonUrls;
}
String _normalizeSwaggerUrl(String raw) {
final value = raw.trim();
if (value.startsWith('http://') || value.startsWith('https://')) {
return value;
}
if (value.startsWith('file://')) {
return value;
}
var p = value;
if (!path.isAbsolute(p)) {
final cfgDir = PathResolver.getConfigDirectory();
if (cfgDir != null) {
p = path.normalize(path.join(cfgDir, p));
} else {
p = path.normalize(path.join(Directory.current.path, p));
}
}
return 'file://$p';
}
///
List<String> get ignoredDirectories {
final output = _config['output'] as Map<String, dynamic>?;
final ignoredDirs = output?['ignored_directories'];
if (ignoredDirs is List) {
return ignoredDirs.map((item) => item.toString()).toList();
}
return [];
}
///
List<String> get ignoredFiles {
final output = _config['output'] as Map<String, dynamic>?;
final ignoredFiles = output?['ignored_files'];
if (ignoredFiles is List) {
return ignoredFiles.map((item) => item.toString()).toList();
}
return [];
}
///
bool shouldSkipFile(String filePath) {
// 1.
final ignoredDirs = ignoredDirectories;
if (ignoredDirs.isNotEmpty) {
final normalizedPath = filePath.replaceAll(r'\', '/');
for (final ignoredDir in ignoredDirs) {
var normalizedDir = ignoredDir.replaceAll(r'\', '/');
if (normalizedDir.endsWith('/')) {
normalizedDir = normalizedDir.substring(0, normalizedDir.length - 1);
}
if (normalizedDir.isEmpty) continue;
if (normalizedPath.contains(normalizedDir)) {
final dirWithSlash = '$normalizedDir/';
if (normalizedPath.startsWith(dirWithSlash) ||
normalizedPath.contains('/$dirWithSlash') ||
normalizedPath == normalizedDir) {
return true;
}
}
}
}
// 2.
final ignoredFilesList = ignoredFiles;
if (ignoredFilesList.isNotEmpty) {
final fileName = path.basename(filePath);
for (final ignoredFile in ignoredFilesList) {
if (fileName == ignoredFile) return true;
if (ignoredFile.startsWith('*')) {
if (fileName.endsWith(ignoredFile.substring(1))) return true;
} else if (ignoredFile.endsWith('*')) {
if (fileName
.startsWith(ignoredFile.substring(0, ignoredFile.length - 1))) {
return true;
}
}
if (ignoredFile.contains('*') &&
ignoredFile.startsWith('*') &&
ignoredFile.endsWith('*')) {
final pattern = ignoredFile.substring(1, ignoredFile.length - 1);
if (fileName.contains(pattern)) return true;
}
}
}
return false;
}
///
String? get fileHeaderTemplate {
final templates = _config['templates'] as Map<String, dynamic>?;
return templates?['file_header'] as String?;
}
///
String get generatorName {
final generator = _config['generator'] as Map<String, dynamic>?;
return generator?['name'] as String? ?? 'xy_swagger_generator';
}
///
String get author {
final generator = _config['generator'] as Map<String, dynamic>?;
return generator?['author'] as String? ?? 'max';
}
///
String get copyright {
final generator = _config['generator'] as Map<String, dynamic>?;
return generator?['copyright'] as String? ??
'Copyright (C) 2025 YuanXuan. All rights reserved.';
}
///
String get baseDir {
final output = _config['output'] as Map<String, dynamic>?;
return output?['base_dir'] as String? ?? SwaggerConfig.defaultGeneratorDir;
}
/// API
String get apiDir {
final output = _config['output'] as Map<String, dynamic>?;
return output?['api_dir'] as String? ?? SwaggerConfig.defaultApiDir;
}
///
String get modelsDir {
final output = _config['output'] as Map<String, dynamic>?;
return output?['models_dir'] as String? ?? SwaggerConfig.defaultModelsDir;
}
///
String get versionExtractionPattern {
final generation = _config['generation'] as Map<String, dynamic>?;
final api = generation?['api'] as Map<String, dynamic>?;
final versionExtraction =
api?['version_extraction'] as Map<String, dynamic>?;
return versionExtraction?['pattern'] as String? ?? r'/api/v(\d+)/';
}
///
String get defaultVersion {
final generation = _config['generation'] as Map<String, dynamic>?;
final api = generation?['api'] as Map<String, dynamic>?;
final versionExtraction =
api?['version_extraction'] as Map<String, dynamic>?;
return versionExtraction?['default_version'] as String? ?? 'v1';
}
/// BaseResult
String get baseResultImport {
final generation = _config['generation'] as Map<String, dynamic>?;
final api = generation?['api'] as Map<String, dynamic>?;
return api?['base_result_import'] as String? ?? '';
}
/// BasePageResult
String get basePageResultImport {
final generation = _config['generation'] as Map<String, dynamic>?;
final api = generation?['api'] as Map<String, dynamic>?;
return api?['base_page_result_import'] as String? ?? '';
}
///
/// : { "EnumName": { value: { "name": "KEY_NAME", "description": "描述" } } }
Map<String, Map<dynamic, EnumKeyMapping>>? get enumKeyMappings {
final generation = _config['generation'] as Map<String, dynamic>?;
final models = generation?['models'] as Map<String, dynamic>?;
final mappings = models?['enum_key_mappings'] as Map<String, dynamic>?;
if (mappings == null) return null;
final result = <String, Map<dynamic, EnumKeyMapping>>{};
mappings.forEach((enumName, enumMappings) {
if (enumMappings is! List) return;
final valueMap = <dynamic, EnumKeyMapping>{};
for (final mapping in enumMappings) {
if (mapping is! Map) continue;
final value = mapping['value'];
final name = mapping['name'] as String?;
final description = mapping['description'] as String?;
if (value != null && name != null) {
valueMap[value] = EnumKeyMapping(
name: name,
description: description,
);
}
}
if (valueMap.isNotEmpty) {
result[enumName.toString()] = valueMap;
}
});
return result.isEmpty ? null : result;
}
/// API Client
String get apiClientClassName {
final generation = _config['generation'] as Map<String, dynamic>?;
final api = generation?['api'] as Map<String, dynamic>?;
final client = api?['client'] as Map<String, dynamic>?;
return client?['class_name'] as String? ?? 'ApiClient';
}
/// API Client
String get apiClientFileName {
final generation = _config['generation'] as Map<String, dynamic>?;
final api = generation?['api'] as Map<String, dynamic>?;
final client = api?['client'] as Map<String, dynamic>?;
return client?['file_name'] as String? ?? 'api_client';
}
/// tags
List<String>? get includedTags {
final output = _config['output'] as Map<String, dynamic>?;
final tags = output?['included_tags'];
if (tags is! List) return null;
final result = tags
.map((tag) => tag.toString().trim())
.where((tag) => tag.isNotEmpty)
.toList();
return result.isEmpty ? null : result;
}
/// tags
List<String>? get excludedTags {
final output = _config['output'] as Map<String, dynamic>?;
final tags = output?['excluded_tags'];
if (tags is! List) return null;
final result = tags
.map((tag) => tag.toString().trim())
.where((tag) => tag.isNotEmpty)
.toList();
return result.isEmpty ? null : result;
}
/// tags API
bool get splitByTags {
final output = _config['output'] as Map<String, dynamic>?;
return output?['split_by_tags'] as bool? ?? true;
}
///
List<String> get packageImports {
final imports = _config['imports'] as Map<String, dynamic>?;
final packageImports = imports?['package_imports'];
if (packageImports is List) {
return packageImports.map((e) => e.toString()).toList();
}
return [];
}
}

View File

@ -1,445 +1,7 @@
///
///
/// Enhanced error reporting system
/// Provides detailed error location, context, and fix suggestions
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);
}
}
export 'error_reporter/models.dart';
export 'error_reporter/renderers.dart';
export 'error_reporter/reporter.dart';

View File

@ -0,0 +1,234 @@
/// Error reporter data models
library;
/// Error severity levels
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 '🚨';
}
}
}
/// Error categories
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';
}
}
}
/// Error location information
class ErrorLocation {
const ErrorLocation({
required this.jsonPath,
this.line,
this.column,
this.offset,
this.snippet,
});
/// JSON path (e.g., "paths./users.get.responses.200")
final String jsonPath;
/// Line number (if available)
final int? line;
/// Column number (if available)
final int? column;
/// Character offset
final int? offset;
/// Related JSON snippet
final String? snippet;
@override
String toString() {
final buffer = StringBuffer()..write(jsonPath);
if (line != null) {
buffer
..write(' (line $line')
..write(column != null ? ', column $column' : '')
..write(')');
}
return buffer.toString();
}
}
/// Fix suggestion
class FixSuggestion {
const FixSuggestion({
required this.description,
this.codeExample,
this.documentationUrl,
this.autoFix,
});
factory FixSuggestion.fromJson(Map<String, dynamic> json) {
return FixSuggestion(
description: json['description'] as String,
codeExample: json['codeExample'] as String?,
documentationUrl: json['documentationUrl'] as String?,
);
}
/// Suggestion description
final String description;
/// Fix code example
final String? codeExample;
/// Related documentation link
final String? documentationUrl;
/// Auto-fix function (if supported)
final String Function(String original)? autoFix;
}
/// Detailed error report
class DetailedError {
DetailedError({
required this.id,
required this.title,
required this.description,
required this.severity,
required this.category,
required this.location,
this.suggestions = const [],
this.relatedErrors = const [],
DateTime? timestamp,
}) : timestamp = timestamp ?? DateTime.now();
/// Error ID (for lookup and categorization)
final String id;
/// Error title
final String title;
/// Error description
final String description;
/// Error severity
final ErrorSeverity severity;
/// Error category
final ErrorCategory category;
/// Error location
final ErrorLocation location;
/// Fix suggestions
final List<FixSuggestion> suggestions;
/// Related errors (if any)
final List<String> relatedErrors;
/// Error timestamp
final DateTime timestamp;
@override
String toString() {
final buffer = StringBuffer()
// Error header
..writeln('${severity.emoji} ${severity.displayName}: $title')
..writeln('Category: ${category.displayName}')
..writeln('Location: $location')
..writeln()
// Error description
..writeln('Description:')
..writeln(' $description')
..writeln();
// Code snippet (if available)
if (location.snippet != null) {
buffer
..writeln('Code snippet:')
..writeln(' ${location.snippet}')
..writeln();
}
// Fix suggestions
if (suggestions.isNotEmpty) {
buffer.writeln('Suggestions:');
for (var i = 0; i < suggestions.length; i++) {
final suggestion = suggestions[i];
buffer.writeln(' ${i + 1}. ${suggestion.description}');
if (suggestion.codeExample != null) {
buffer
..writeln(' Example:')
..writeln(' ${suggestion.codeExample}');
}
if (suggestion.documentationUrl != null) {
buffer.writeln(' See: ${suggestion.documentationUrl}');
}
buffer.writeln();
}
}
// Related errors
if (relatedErrors.isNotEmpty) {
buffer.writeln('Related errors: ${relatedErrors.join(', ')}');
}
return buffer.toString();
}
}

View File

@ -0,0 +1,204 @@
/// Error report renderers
library;
import 'dart:convert';
import 'package:swagger_generator_flutter/core/error_reporter/models.dart';
/// Base renderer interface
abstract class ErrorRenderer {
String render(
List<DetailedError> errors, {
bool includeStatistics = true,
});
}
/// Text renderer for console output
class TextErrorRenderer implements ErrorRenderer {
const TextErrorRenderer({this.groupByCategory = false});
final bool groupByCategory;
@override
String render(
List<DetailedError> errors, {
bool includeStatistics = true,
}) {
final buffer = StringBuffer();
if (errors.isEmpty) {
buffer.writeln('✅ No errors found!');
return buffer.toString();
}
// Statistics
if (includeStatistics) {
buffer
..writeln('📊 Error Summary')
..writeln('=' * 50);
final stats = _getStatistics(errors);
for (final entry in stats.entries) {
buffer.writeln(
'${entry.key.emoji} ${entry.key.displayName}: ${entry.value}',
);
}
buffer.writeln();
}
// Error details
if (groupByCategory) {
_renderByCategory(buffer, errors);
} else {
_renderByOrder(buffer, errors);
}
return buffer.toString();
}
Map<ErrorSeverity, int> _getStatistics(List<DetailedError> errors) {
final stats = <ErrorSeverity, int>{};
for (final error in errors) {
stats[error.severity] = (stats[error.severity] ?? 0) + 1;
}
return stats;
}
void _renderByCategory(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}')
..writeln('-' * 30);
for (final error in categoryErrors) {
buffer
..writeln(error.toString())
..writeln();
}
});
}
void _renderByOrder(StringBuffer buffer, List<DetailedError> errors) {
buffer
..writeln('🔍 Detailed Error Report')
..writeln('=' * 50);
for (var i = 0; i < errors.length; i++) {
buffer
..writeln('Error ${i + 1}/${errors.length}:')
..writeln(errors[i].toString());
if (i < errors.length - 1) {
buffer.writeln('-' * 50);
}
}
}
}
/// JSON renderer for machine-readable output
class JsonErrorRenderer implements ErrorRenderer {
const JsonErrorRenderer();
@override
String render(
List<DetailedError> errors, {
bool includeStatistics = true,
}) {
final report = {
'timestamp': DateTime.now().toIso8601String(),
if (includeStatistics)
'summary': {
'total': errors.length,
'by_severity': _getStatistics(errors).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);
}
Map<ErrorSeverity, int> _getStatistics(List<DetailedError> errors) {
final stats = <ErrorSeverity, int>{};
for (final error in errors) {
stats[error.severity] = (stats[error.severity] ?? 0) + 1;
}
return stats;
}
}
/// CI-friendly renderer (GitHub Actions, GitLab CI, etc.)
class CiErrorRenderer implements ErrorRenderer {
const CiErrorRenderer();
@override
String render(
List<DetailedError> errors, {
bool includeStatistics = true,
}) {
final buffer = StringBuffer();
for (final error in errors) {
// GitHub Actions format: ::error file={name},line={line}::{message}
final level = _severityToLevel(error.severity);
buffer.write('::$level ');
if (error.location.line != null) {
buffer.write('line=${error.location.line}');
if (error.location.column != null) {
buffer.write(',col=${error.location.column}');
}
buffer.write('::');
}
buffer.writeln('${error.title}: ${error.description}');
}
return buffer.toString();
}
String _severityToLevel(ErrorSeverity severity) {
switch (severity) {
case ErrorSeverity.critical:
case ErrorSeverity.error:
return 'error';
case ErrorSeverity.warning:
return 'warning';
case ErrorSeverity.info:
return 'notice';
}
}
}

View File

@ -0,0 +1,99 @@
/// Error reporter core logic
library;
import 'package:swagger_generator_flutter/core/error_reporter/models.dart';
/// Error reporter for collecting and managing errors
class ErrorReporter {
ErrorReporter();
final List<DetailedError> _errors = [];
final Map<String, int> _errorCounts = {};
/// Add an error
void addError(DetailedError error) {
_errors.add(error);
_errorCounts[error.id] = (_errorCounts[error.id] ?? 0) + 1;
}
/// Create and add an error
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);
}
/// Get all errors
List<DetailedError> get errors => List.unmodifiable(_errors);
/// Get errors by severity
List<DetailedError> getErrorsBySeverity(ErrorSeverity severity) {
return _errors.where((error) => error.severity == severity).toList();
}
/// Get errors by category
List<DetailedError> getErrorsByCategory(ErrorCategory category) {
return _errors.where((error) => error.category == category).toList();
}
/// Get error statistics
Map<ErrorSeverity, int> getErrorStatistics() {
final stats = <ErrorSeverity, int>{};
for (final error in _errors) {
stats[error.severity] = (stats[error.severity] ?? 0) + 1;
}
return stats;
}
/// Check if there are any errors
bool get hasErrors => _errors.isNotEmpty;
/// Check if there are critical errors
bool get hasCriticalErrors =>
_errors.any((e) => e.severity == ErrorSeverity.critical);
/// Check if there are errors (excluding warnings and info)
bool get hasErrorsOrCritical => _errors.any(
(e) =>
e.severity == ErrorSeverity.error ||
e.severity == ErrorSeverity.critical,
);
/// Clear all errors
void clear() {
_errors.clear();
_errorCounts.clear();
}
/// Get error count by ID
int getErrorCount(String id) => _errorCounts[id] ?? 0;
/// Get total error count
int get totalErrors => _errors.length;
}

View File

@ -2,18 +2,12 @@
///
library;
import 'error_reporter.dart';
import 'package:swagger_generator_flutter/core/error_reporter.dart';
import 'package:swagger_generator_flutter/utils/file_utils.dart';
import 'package:yaml/yaml.dart';
///
class ErrorRule {
final String id;
final String pattern;
final ErrorSeverity severity;
final ErrorCategory category;
final String title;
final String description;
final List<FixSuggestion> suggestions;
const ErrorRule({
required this.id,
required this.pattern,
@ -23,346 +17,96 @@ class ErrorRule {
required this.description,
this.suggestions = const [],
});
factory ErrorRule.fromJson(Map<String, dynamic> json) {
return ErrorRule(
id: json['id'] as String,
pattern: json['pattern'] as String,
severity: _parseSeverity(json['severity'] as String),
category: _parseCategory(json['category'] as String),
title: json['title'] as String,
description: json['description'] as String,
suggestions: (json['suggestions'] as List<dynamic>?)
?.map((e) => FixSuggestion.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
);
}
final String id;
final String pattern;
final ErrorSeverity severity;
final ErrorCategory category;
final String title;
final String description;
final List<FixSuggestion> suggestions;
static ErrorSeverity _parseSeverity(String severity) {
switch (severity.toLowerCase()) {
case 'critical':
return ErrorSeverity.critical;
case 'error':
return ErrorSeverity.error;
case 'warning':
return ErrorSeverity.warning;
case 'info':
return ErrorSeverity.info;
default:
return ErrorSeverity.error;
}
}
static ErrorCategory _parseCategory(String category) {
switch (category.toLowerCase()) {
case 'syntax':
return ErrorCategory.syntax;
case 'validation':
return ErrorCategory.validation;
case 'compatibility':
return ErrorCategory.compatibility;
case 'security':
return ErrorCategory.security;
case 'bestpractice':
case 'best_practice':
return ErrorCategory.bestPractice;
case 'performance':
return ErrorCategory.performance;
case 'schema':
return ErrorCategory.schema;
case 'reference':
return ErrorCategory.reference;
default:
return ErrorCategory.validation;
}
}
}
///
class RuleLoader {
static Future<List<ErrorRule>> loadRules(String configPath) async {
try {
final content = await FileUtils.safeReadFile(configPath);
final yaml = loadYaml(content) as Map;
final rules = yaml['rules'] as List;
return rules.map((rule) {
return ErrorRule.fromJson(Map<String, dynamic>.from(rule as Map));
}).toList();
} on Object catch (e) {
throw Exception('Failed to load error rules from $configPath: $e');
}
}
}
/// 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',
),
],
),
static List<ErrorRule> _rules = [];
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"',
),
],
),
static List<ErrorRule> get rules => _rules;
// 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 Future<void> load(String configPath) async {
_rules = await RuleLoader.loadRules(configPath);
}
///
static List<ErrorRule> getRulesByCategory(ErrorCategory category) {
@ -376,11 +120,12 @@ class OpenApiErrorRules {
/// ID
static ErrorRule? getRuleById(String id) {
try {
return rules.firstWhere((rule) => rule.id == id);
} catch (e) {
return null;
for (final rule in rules) {
if (rule.id == id) {
return rule;
}
}
return null;
}
/// ID
@ -390,7 +135,9 @@ class OpenApiErrorRules {
///
static DetailedError createMissingFieldError(
String fieldPath, String fieldName) {
String fieldPath,
String fieldName,
) {
return DetailedError(
id: 'MISSING_FIELD',
title: 'Missing Required Field',
@ -408,7 +155,10 @@ class OpenApiErrorRules {
}
static DetailedError createInvalidTypeError(
String fieldPath, String expectedType, String actualType) {
String fieldPath,
String expectedType,
String actualType,
) {
return DetailedError(
id: 'INVALID_TYPE',
title: 'Invalid Field Type',
@ -427,7 +177,10 @@ class OpenApiErrorRules {
}
static DetailedError createUnknownFieldError(
String fieldPath, String fieldName, List<String> validFields) {
String fieldPath,
String fieldName,
List<String> validFields,
) {
return DetailedError(
id: 'UNKNOWN_FIELD',
title: 'Unknown Field',
@ -437,8 +190,8 @@ class OpenApiErrorRules {
location: ErrorLocation(jsonPath: fieldPath),
suggestions: [
FixSuggestion(
description:
'Remove the unknown field or use one of: ${validFields.join(", ")}',
description: 'Remove the unknown field or use one of: '
'${validFields.join(", ")}',
codeExample: 'Valid fields: ${validFields.join(", ")}',
),
],
@ -446,7 +199,9 @@ class OpenApiErrorRules {
}
static DetailedError createReferenceError(
String fieldPath, String reference) {
String fieldPath,
String reference,
) {
return DetailedError(
id: 'INVALID_REFERENCE',
title: 'Invalid Reference',
@ -461,7 +216,7 @@ class OpenApiErrorRules {
),
const FixSuggestion(
description: 'Verify the reference path is correct',
codeExample: '"\$ref": "#/components/schemas/ComponentName"',
codeExample: r'"$ref": "#/components/schemas/ComponentName"',
),
],
);

View File

@ -1,608 +1,17 @@
import 'dart:io';
/// Swagger CLI
abstract class SwaggerException implements Exception {
final String message;
final String? details;
final DateTime timestamp;
SwaggerException(this.message, {this.details}) : timestamp = DateTime.now();
@override
String toString() {
if (details != null) {
return '$runtimeType: $message\n详细信息: $details';
}
return '$runtimeType: $message';
}
}
/// Swagger解析异常
class SwaggerParseException extends SwaggerException {
final String? url;
final int? statusCode;
final String? operation;
SwaggerParseException(
super.message, {
super.details,
this.url,
this.statusCode,
this.operation,
});
@override
String toString() {
final buffer = StringBuffer();
buffer.writeln('SwaggerParseException: $message');
if (url != null) {
buffer.writeln('URL: $url');
}
if (statusCode != null) {
buffer.writeln('状态码: $statusCode');
}
if (operation != null) {
buffer.writeln('操作: $operation');
}
if (details != null) {
buffer.writeln('详细信息: $details');
}
return buffer.toString().trim();
}
}
///
class CodeGenerationException extends SwaggerException {
final String? generatorType;
final String? modelName;
final String? phase;
CodeGenerationException(
super.message, {
super.details,
this.generatorType,
this.modelName,
this.phase,
});
@override
String toString() {
final buffer = StringBuffer();
buffer.writeln('CodeGenerationException: $message');
if (generatorType != null) {
buffer.writeln('生成器类型: $generatorType');
}
if (modelName != null) {
buffer.writeln('模型名称: $modelName');
}
if (phase != null) {
buffer.writeln('生成阶段: $phase');
}
if (details != null) {
buffer.writeln('详细信息: $details');
}
return buffer.toString().trim();
}
}
///
class FileOperationException extends SwaggerException {
final String? filePath;
final String? operation;
final int? errorCode;
FileOperationException(
super.message, {
super.details,
this.filePath,
this.operation,
this.errorCode,
});
@override
String toString() {
final buffer = StringBuffer();
buffer.writeln('FileOperationException: $message');
if (filePath != null) {
buffer.writeln('文件路径: $filePath');
}
if (operation != null) {
buffer.writeln('操作: $operation');
}
if (errorCode != null) {
buffer.writeln('错误代码: $errorCode');
}
if (details != null) {
buffer.writeln('详细信息: $details');
}
return buffer.toString().trim();
}
}
///
class CommandException extends SwaggerException {
final String? commandName;
final List<String>? arguments;
final int? exitCode;
CommandException(
super.message, {
super.details,
this.commandName,
this.arguments,
this.exitCode,
});
@override
String toString() {
final buffer = StringBuffer();
buffer.writeln('CommandException: $message');
if (commandName != null) {
buffer.writeln('命令: $commandName');
}
if (arguments != null && arguments!.isNotEmpty) {
buffer.writeln('参数: ${arguments!.join(' ')}');
}
if (exitCode != null) {
buffer.writeln('退出代码: $exitCode');
}
if (details != null) {
buffer.writeln('详细信息: $details');
}
return buffer.toString().trim();
}
}
///
class ValidationException extends SwaggerException {
final String? field;
final dynamic value;
final String? rule;
ValidationException(
super.message, {
super.details,
this.field,
this.value,
this.rule,
});
@override
String toString() {
final buffer = StringBuffer();
buffer.writeln('ValidationException: $message');
if (field != null) {
buffer.writeln('字段: $field');
}
if (value != null) {
buffer.writeln('值: $value');
}
if (rule != null) {
buffer.writeln('验证规则: $rule');
}
if (details != null) {
buffer.writeln('详细信息: $details');
}
return buffer.toString().trim();
}
}
///
class ConfigurationException extends SwaggerException {
final String? configKey;
final dynamic configValue;
final String? source;
ConfigurationException(
super.message, {
super.details,
this.configKey,
this.configValue,
this.source,
});
@override
String toString() {
final buffer = StringBuffer();
buffer.writeln('ConfigurationException: $message');
if (configKey != null) {
buffer.writeln('配置键: $configKey');
}
if (configValue != null) {
buffer.writeln('配置值: $configValue');
}
if (source != null) {
buffer.writeln('来源: $source');
}
if (details != null) {
buffer.writeln('详细信息: $details');
}
return buffer.toString().trim();
}
}
///
class NetworkException extends SwaggerException {
final String? url;
final int? statusCode;
final String? method;
final Duration? timeout;
NetworkException(
super.message, {
super.details,
this.url,
this.statusCode,
this.method,
this.timeout,
});
@override
String toString() {
final buffer = StringBuffer();
buffer.writeln('NetworkException: $message');
if (url != null) {
buffer.writeln('URL: $url');
}
if (method != null) {
buffer.writeln('方法: $method');
}
if (statusCode != null) {
buffer.writeln('状态码: $statusCode');
}
if (timeout != null) {
buffer.writeln('超时: ${timeout!.inSeconds}');
}
if (details != null) {
buffer.writeln('详细信息: $details');
}
return buffer.toString().trim();
}
}
///
class CacheException extends SwaggerException {
final String? cacheKey;
final String? operation;
final String? cacheType;
CacheException(
super.message, {
super.details,
this.cacheKey,
this.operation,
this.cacheType,
});
@override
String toString() {
final buffer = StringBuffer();
buffer.writeln('CacheException: $message');
if (cacheKey != null) {
buffer.writeln('缓存键: $cacheKey');
}
if (operation != null) {
buffer.writeln('操作: $operation');
}
if (cacheType != null) {
buffer.writeln('缓存类型: $cacheType');
}
if (details != null) {
buffer.writeln('详细信息: $details');
}
return buffer.toString().trim();
}
}
///
class PerformanceException extends SwaggerException {
final String? operation;
final Duration? duration;
final Duration? threshold;
PerformanceException(
super.message, {
super.details,
this.operation,
this.duration,
this.threshold,
});
@override
String toString() {
final buffer = StringBuffer();
buffer.writeln('PerformanceException: $message');
if (operation != null) {
buffer.writeln('操作: $operation');
}
if (duration != null) {
buffer.writeln('耗时: ${duration!.inMilliseconds}ms');
}
if (threshold != null) {
buffer.writeln('阈值: ${threshold!.inMilliseconds}ms');
}
if (details != null) {
buffer.writeln('详细信息: $details');
}
return buffer.toString().trim();
}
}
///
class TypeException extends SwaggerException {
final String? propertyName;
final String? expectedType;
final String? actualType;
final dynamic value;
TypeException(
super.message, {
super.details,
this.propertyName,
this.expectedType,
this.actualType,
this.value,
});
@override
String toString() {
final buffer = StringBuffer();
buffer.writeln('TypeException: $message');
if (propertyName != null) {
buffer.writeln('属性名: $propertyName');
}
if (expectedType != null) {
buffer.writeln('期望类型: $expectedType');
}
if (actualType != null) {
buffer.writeln('实际类型: $actualType');
}
if (value != null) {
buffer.writeln('值: $value');
}
if (details != null) {
buffer.writeln('详细信息: $details');
}
return buffer.toString().trim();
}
}
///
class ExceptionHandler {
static final Map<Type, void Function(SwaggerException)> _handlers = {};
///
static void register<T extends SwaggerException>(
void Function(T exception) handler,
) {
_handlers[T] = (exception) => handler(exception as T);
}
///
static void handle(SwaggerException exception) {
final handler = _handlers[exception.runtimeType];
if (handler != null) {
handler(exception);
} else {
//
_defaultHandler(exception);
}
}
///
static void _defaultHandler(SwaggerException exception) {
print('🚨 异常: ${exception.toString()}');
print('时间: ${exception.timestamp.toIso8601String()}');
print('');
}
///
static Future<void> logException(
SwaggerException exception, {
String? logFilePath,
}) async {
try {
final logFile = logFilePath != null
? File(logFilePath)
: File('swagger_cli_errors.log');
final logEntry = [
'[${'=' * 50}]',
'时间: ${exception.timestamp.toIso8601String()}',
'类型: ${exception.runtimeType}',
'消息: ${exception.message}',
if (exception.details != null) '详细信息: ${exception.details}',
'堆栈跟踪: ${StackTrace.current}',
'',
].join('\n');
await logFile.writeAsString(logEntry, mode: FileMode.append);
} catch (e) {
print('记录异常到文件失败: $e');
}
}
///
static void clear() {
_handlers.clear();
}
}
///
class ExceptionFactory {
///
static SwaggerParseException createParseException(
String message, {
String? url,
int? statusCode,
String? operation,
dynamic cause,
}) {
return SwaggerParseException(
message,
details: cause?.toString(),
url: url,
statusCode: statusCode,
operation: operation,
);
}
///
static CodeGenerationException createCodeGenerationException(
String message, {
String? generatorType,
String? modelName,
String? phase,
dynamic cause,
}) {
return CodeGenerationException(
message,
details: cause?.toString(),
generatorType: generatorType,
modelName: modelName,
phase: phase,
);
}
///
static FileOperationException createFileOperationException(
String message, {
String? filePath,
String? operation,
int? errorCode,
dynamic cause,
}) {
return FileOperationException(
message,
details: cause?.toString(),
filePath: filePath,
operation: operation,
errorCode: errorCode,
);
}
///
static ValidationException createValidationException(
String message, {
String? field,
dynamic value,
String? rule,
dynamic cause,
}) {
return ValidationException(
message,
details: cause?.toString(),
field: field,
value: value,
rule: rule,
);
}
///
static NetworkException createNetworkException(
String message, {
String? url,
int? statusCode,
String? method,
Duration? timeout,
dynamic cause,
}) {
return NetworkException(
message,
details: cause?.toString(),
url: url,
statusCode: statusCode,
method: method,
timeout: timeout,
);
}
///
static SwaggerException fromStandardException(
Exception exception, {
String? context,
}) {
if (exception is FileSystemException) {
return FileOperationException(
'文件系统错误',
details: exception.message,
filePath: exception.path,
operation: context,
);
} else if (exception is SocketException) {
return NetworkException(
'网络连接错误',
details: exception.message,
url: context,
);
} else if (exception is FormatException) {
return SwaggerParseException(
'格式错误',
details: exception.message,
operation: context,
);
} else {
return GeneralSwaggerException(
'未知错误',
details: exception.toString(),
);
}
}
}
/// Swagger异常使
class GeneralSwaggerException extends SwaggerException {
GeneralSwaggerException(super.message, {super.details});
}
/// Swagger CLI exceptions
///
/// This library provides a comprehensive exception hierarchy for the
/// Swagger code generator,
/// including parsing, generation, IO, and runtime errors.
library;
// Base exceptions and utilities
export 'exceptions/base.dart';
// Handler and factory
export 'exceptions/factory.dart';
// Specific exception types
export 'exceptions/generation_exceptions.dart';
export 'exceptions/handler.dart';
export 'exceptions/io_exceptions.dart';
export 'exceptions/parse_exceptions.dart';
export 'exceptions/runtime_exceptions.dart';

View File

@ -0,0 +1,45 @@
/// Base exception classes and formatting utilities
library;
/// Format exception details into a readable string
String formatExceptionDetails(
String header,
Map<String, Object?> fields,
) {
final buffer = StringBuffer()..writeln(header);
fields.forEach((label, value) {
if (value != null) {
buffer.writeln('$label: $value');
}
});
return buffer.toString().trim();
}
/// Mixin for exception formatting
mixin ExceptionFormattingMixin on SwaggerException {
/// Get the fields to display in the formatted output
Map<String, Object?> get formattingFields;
@override
String toString() => formatExceptionDetails(
'$runtimeType: $message',
formattingFields,
);
}
/// Swagger CLI base exception class
abstract class SwaggerException implements Exception {
SwaggerException(this.message, {this.details}) : timestamp = DateTime.now();
final String message;
final String? details;
final DateTime timestamp;
@override
String toString() {
if (details != null) {
return '$runtimeType: $message\n详细信息: $details';
}
return '$runtimeType: $message';
}
}

View File

@ -0,0 +1,131 @@
/// Exception factory for creating exceptions
library;
import 'dart:io';
import 'package:swagger_generator_flutter/core/exceptions/generation_exceptions.dart';
import 'package:swagger_generator_flutter/core/exceptions/io_exceptions.dart';
import 'package:swagger_generator_flutter/core/exceptions/parse_exceptions.dart';
import 'package:swagger_generator_flutter/core/exceptions/runtime_exceptions.dart';
/// Factory for creating exceptions
class ExceptionFactory {
/// Create a parse exception
static SwaggerParseException createParseException(
String message, {
String? url,
int? statusCode,
String? operation,
dynamic cause,
}) {
return SwaggerParseException(
message,
details: cause?.toString(),
url: url,
statusCode: statusCode,
operation: operation,
);
}
/// Create a code generation exception
static CodeGenerationException createCodeGenerationException(
String message, {
String? generatorType,
String? modelName,
String? phase,
dynamic cause,
}) {
return CodeGenerationException(
message,
details: cause?.toString(),
generatorType: generatorType,
modelName: modelName,
phase: phase,
);
}
/// Create a file operation exception
static FileOperationException createFileOperationException(
String message, {
String? filePath,
String? operation,
int? errorCode,
dynamic cause,
}) {
return FileOperationException(
message,
details: cause?.toString(),
filePath: filePath,
operation: operation,
errorCode: errorCode,
);
}
/// Create a validation exception
static ValidationException createValidationException(
String message, {
String? field,
dynamic value,
String? rule,
dynamic cause,
}) {
return ValidationException(
message,
details: cause?.toString(),
field: field,
value: value,
rule: rule,
);
}
/// Create a network exception
static NetworkException createNetworkException(
String message, {
String? url,
int? statusCode,
String? method,
Duration? timeout,
dynamic cause,
}) {
return NetworkException(
message,
details: cause?.toString(),
url: url,
statusCode: statusCode,
method: method,
timeout: timeout,
);
}
/// Create exception from standard Dart exception
static dynamic fromStandardException(
Exception exception, {
String? context,
}) {
if (exception is FileSystemException) {
return FileOperationException(
'文件系统错误',
details: exception.message,
filePath: exception.path,
operation: context,
);
} else if (exception is SocketException) {
return NetworkException(
'网络连接错误',
details: exception.message,
url: context,
);
} else if (exception is FormatException) {
return SwaggerParseException(
'格式错误',
details: exception.message,
operation: context,
);
} else {
return GeneralSwaggerException(
'未知错误',
details: exception.toString(),
);
}
}
}

View File

@ -0,0 +1,28 @@
/// Code generation exceptions
library;
import 'package:swagger_generator_flutter/core/exceptions/base.dart';
/// Code generation exception
class CodeGenerationException extends SwaggerException
with ExceptionFormattingMixin {
CodeGenerationException(
super.message, {
super.details,
this.generatorType,
this.modelName,
this.phase,
});
final String? generatorType;
final String? modelName;
final String? phase;
@override
Map<String, Object?> get formattingFields => {
'生成器类型': generatorType,
'模型名称': modelName,
'生成阶段': phase,
'详细信息': details,
};
}

View File

@ -0,0 +1,96 @@
/// Exception handler with hierarchical matching support
library;
import 'dart:io';
import 'package:swagger_generator_flutter/core/exceptions/base.dart';
import 'package:swagger_generator_flutter/utils/logger.dart';
/// Exception handler with hierarchical type matching
class ExceptionHandler {
static final Map<Type, void Function(SwaggerException)> _handlers = {};
/// Register an exception handler for a specific type
static void register<T extends SwaggerException>(
void Function(T exception) handler,
) {
_handlers[T] = (exception) => handler(exception as T);
}
/// Unregister a handler for a specific type
static void unregister<T extends SwaggerException>() {
_handlers.remove(T);
}
/// Handle an exception with hierarchical matching
/// First tries exact type match, then walks up the type hierarchy
static void handle(SwaggerException exception) {
// Try exact type match first
final handler = _handlers[exception.runtimeType];
if (handler != null) {
handler(exception);
return;
}
// Try hierarchical matching
for (final entry in _handlers.entries) {
if (_isSubtype(exception.runtimeType, entry.key)) {
entry.value(exception);
return;
}
}
// Fall back to default handler
_defaultHandler(exception);
}
/// Check if a type is a subtype of another
static bool _isSubtype(Type subtype, Type supertype) {
// This is a simplified check - in production you might want
// to use reflection or maintain a type hierarchy map
return subtype.toString().contains(supertype.toString());
}
/// Default exception handler
static void _defaultHandler(SwaggerException exception) {
appLogger.severe(
'🚨 异常: $exception',
exception,
StackTrace.current,
);
}
/// Log exception to file
static Future<void> logException(
SwaggerException exception, {
String? logFilePath,
}) async {
try {
final logFile = logFilePath != null
? File(logFilePath)
: File('swagger_cli_errors.log');
final logEntry = [
'[${'=' * 50}]',
'时间: ${exception.timestamp.toIso8601String()}',
'类型: ${exception.runtimeType}',
'消息: ${exception.message}',
if (exception.details != null) '详细信息: ${exception.details}',
'堆栈跟踪: ${StackTrace.current}',
'',
].join('\n');
await logFile.writeAsString(logEntry, mode: FileMode.append);
} on Exception catch (e, stackTrace) {
appLogger.severe('记录异常到文件失败', e, stackTrace);
}
}
/// Clear all registered handlers
static void clear() {
_handlers.clear();
}
/// Get count of registered handlers
static int get handlerCount => _handlers.length;
}

View File

@ -0,0 +1,77 @@
/// IO-related exceptions
library;
import 'package:swagger_generator_flutter/core/exceptions/base.dart';
/// File operation exception
class FileOperationException extends SwaggerException
with ExceptionFormattingMixin {
FileOperationException(
super.message, {
super.details,
this.filePath,
this.operation,
this.errorCode,
});
final String? filePath;
final String? operation;
final int? errorCode;
@override
Map<String, Object?> get formattingFields => {
'文件路径': filePath,
'操作': operation,
'错误代码': errorCode,
'详细信息': details,
};
}
/// Network exception
class NetworkException extends SwaggerException with ExceptionFormattingMixin {
NetworkException(
super.message, {
super.details,
this.url,
this.statusCode,
this.method,
this.timeout,
});
final String? url;
final int? statusCode;
final String? method;
final Duration? timeout;
@override
Map<String, Object?> get formattingFields => {
'URL': url,
'方法': method,
'状态码': statusCode,
'超时': timeout != null ? '${timeout!.inSeconds}' : null,
'详细信息': details,
};
}
/// Cache exception
class CacheException extends SwaggerException with ExceptionFormattingMixin {
CacheException(
super.message, {
super.details,
this.cacheKey,
this.operation,
this.cacheType,
});
final String? cacheKey;
final String? operation;
final String? cacheType;
@override
Map<String, Object?> get formattingFields => {
'缓存键': cacheKey,
'操作': operation,
'缓存类型': cacheType,
'详细信息': details,
};
}

View File

@ -0,0 +1,78 @@
/// Parse-related exceptions
library;
import 'package:swagger_generator_flutter/core/exceptions/base.dart';
/// Swagger parsing exception
class SwaggerParseException extends SwaggerException
with ExceptionFormattingMixin {
SwaggerParseException(
super.message, {
super.details,
this.url,
this.statusCode,
this.operation,
});
final String? url;
final int? statusCode;
final String? operation;
@override
Map<String, Object?> get formattingFields => {
'URL': url,
'状态码': statusCode,
'操作': operation,
'详细信息': details,
};
}
/// Validation exception
class ValidationException extends SwaggerException
with ExceptionFormattingMixin {
ValidationException(
super.message, {
super.details,
this.field,
this.value,
this.rule,
});
final String? field;
final dynamic value;
final String? rule;
@override
Map<String, Object?> get formattingFields => {
'字段': field,
'': value,
'验证规则': rule,
'详细信息': details,
};
}
/// Type exception
class TypeException extends SwaggerException with ExceptionFormattingMixin {
TypeException(
super.message, {
super.details,
this.propertyName,
this.expectedType,
this.actualType,
this.value,
});
final String? propertyName;
final String? expectedType;
final String? actualType;
final dynamic value;
@override
Map<String, Object?> get formattingFields => {
'属性名': propertyName,
'期望类型': expectedType,
'实际类型': actualType,
'': value,
'详细信息': details,
};
}

View File

@ -0,0 +1,80 @@
/// Runtime exceptions
library;
import 'package:swagger_generator_flutter/core/exceptions/base.dart';
/// Command exception
class CommandException extends SwaggerException with ExceptionFormattingMixin {
CommandException(
super.message, {
super.details,
this.commandName,
this.arguments,
this.exitCode,
});
final String? commandName;
final List<String>? arguments;
final int? exitCode;
@override
Map<String, Object?> get formattingFields => {
'命令': commandName,
'参数': arguments?.join(' '),
'退出代码': exitCode,
'详细信息': details,
};
}
/// Configuration exception
class ConfigurationException extends SwaggerException
with ExceptionFormattingMixin {
ConfigurationException(
super.message, {
super.details,
this.configKey,
this.configValue,
this.source,
});
final String? configKey;
final dynamic configValue;
final String? source;
@override
Map<String, Object?> get formattingFields => {
'配置键': configKey,
'配置值': configValue,
'来源': source,
'详细信息': details,
};
}
/// Performance exception
class PerformanceException extends SwaggerException
with ExceptionFormattingMixin {
PerformanceException(
super.message, {
super.details,
this.operation,
this.duration,
this.threshold,
});
final String? operation;
final Duration? duration;
final Duration? threshold;
@override
Map<String, Object?> get formattingFields => {
'操作': operation,
'耗时': duration != null ? '${duration!.inMilliseconds}ms' : null,
'阈值': threshold != null ? '${threshold!.inMilliseconds}ms' : null,
'详细信息': details,
};
}
/// General Swagger exception (when specific type cannot be determined)
class GeneralSwaggerException extends SwaggerException {
GeneralSwaggerException(super.message, {super.details});
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,495 @@
part of 'package:swagger_generator_flutter/core/models.dart';
class ApiComponents {
const ApiComponents({
this.schemas = const {},
this.responses = const {},
this.parameters = const {},
this.examples = const {},
this.requestBodies = const {},
this.headers = const {},
this.securitySchemes = const {},
this.links = const {},
this.callbacks = const {},
});
/// JSON创建ApiComponents
factory ApiComponents.fromJson(Map<String, dynamic> json) {
// schemas
final schemasJson = json['schemas'] as Map<String, dynamic>? ?? {};
final schemas = <String, ApiModel>{};
schemasJson.forEach((key, value) {
if (value is Map<String, dynamic>) {
schemas[key] = ApiModel.fromJson(key, value);
}
});
// responses
final responsesJson = json['responses'] as Map<String, dynamic>? ?? {};
final responses = <String, ApiResponse>{};
responsesJson.forEach((key, value) {
if (value is Map<String, dynamic>) {
responses[key] = ApiResponse.fromJson(key, value);
}
});
// parameters
final parametersJson = json['parameters'] as Map<String, dynamic>? ?? {};
final parameters = <String, ApiParameter>{};
parametersJson.forEach((key, value) {
if (value is Map<String, dynamic>) {
parameters[key] = ApiParameter.fromJson(value);
}
});
// examples
final examplesJson = json['examples'] as Map<String, dynamic>? ?? {};
final examples = <String, ApiExample>{};
examplesJson.forEach((key, value) {
if (value is Map<String, dynamic>) {
examples[key] = ApiExample.fromJson(value);
}
});
// requestBodies
final requestBodiesJson =
json['requestBodies'] as Map<String, dynamic>? ?? {};
final requestBodies = <String, ApiRequestBody>{};
requestBodiesJson.forEach((key, value) {
if (value is Map<String, dynamic>) {
requestBodies[key] = ApiRequestBody.fromJson(value);
}
});
// headers
final headersJson = json['headers'] as Map<String, dynamic>? ?? {};
final headers = <String, ApiHeader>{};
headersJson.forEach((key, value) {
if (value is Map<String, dynamic>) {
headers[key] = ApiHeader.fromJson(value);
}
});
// securitySchemes
final securitySchemesJson =
json['securitySchemes'] as Map<String, dynamic>? ?? {};
final securitySchemes = <String, ApiSecurityScheme>{};
securitySchemesJson.forEach((key, value) {
if (value is Map<String, dynamic>) {
securitySchemes[key] = ApiSecurityScheme.fromJson(value);
}
});
// links
final linksJson = json['links'] as Map<String, dynamic>? ?? {};
final links = <String, ApiLink>{};
linksJson.forEach((key, value) {
if (value is Map<String, dynamic>) {
links[key] = ApiLink.fromJson(value);
}
});
// callbacks
final callbacksJson = json['callbacks'] as Map<String, dynamic>? ?? {};
final callbacks = <String, ApiCallback>{};
callbacksJson.forEach((key, value) {
if (value is Map<String, dynamic>) {
callbacks[key] = ApiCallback.fromJson(value);
}
});
return ApiComponents(
schemas: schemas,
responses: responses,
parameters: parameters,
examples: examples,
requestBodies: requestBodies,
headers: headers,
securitySchemes: securitySchemes,
links: links,
callbacks: callbacks,
);
}
/// Schema
final Map<String, ApiModel> schemas;
///
final Map<String, ApiResponse> responses;
///
final Map<String, ApiParameter> parameters;
///
final Map<String, ApiExample> examples;
///
final Map<String, ApiRequestBody> requestBodies;
///
final Map<String, ApiHeader> headers;
///
final Map<String, ApiSecurityScheme> securitySchemes;
///
final Map<String, ApiLink> links;
///
final Map<String, ApiCallback> callbacks;
}
/// API安全方案信息 (OpenAPI 3.0)
class ApiSecurityScheme {
const ApiSecurityScheme({
required this.type,
this.description = '',
this.name,
this.location,
this.scheme,
this.bearerFormat,
this.flows,
this.openIdConnectUrl,
});
/// JSON创建ApiSecurityScheme
factory ApiSecurityScheme.fromJson(Map<String, dynamic> json) {
final type = SecuritySchemeTypeExtension.fromString(
json['type'] as String? ?? 'apiKey',
);
return ApiSecurityScheme(
type: type,
description: json['description'] as String? ?? '',
name: json['name'] as String?,
location: json['in'] != null
? ApiKeyLocationExtension.fromString(json['in'] as String)
: null,
scheme: json['scheme'] as String?,
bearerFormat: json['bearerFormat'] as String?,
flows: json['flows'] != null
? OAuth2Flows.fromJson(json['flows'] as Map<String, dynamic>)
: null,
openIdConnectUrl: json['openIdConnectUrl'] as String?,
);
}
///
final SecuritySchemeType type;
///
final String description;
/// ( apiKey)
final String? name;
/// ( apiKey)
final ApiKeyLocation? location;
/// ( http)
final String? scheme;
/// Bearer ( http bearer)
final String? bearerFormat;
/// OAuth2 ( oauth2)
final OAuth2Flows? flows;
/// OpenID Connect URL ( openIdConnect)
final String? openIdConnectUrl;
/// API Key
bool get isApiKey => type == SecuritySchemeType.apiKey;
/// HTTP
bool get isHttp => type == SecuritySchemeType.http;
/// OAuth2
bool get isOAuth2 => type == SecuritySchemeType.oauth2;
/// OpenID Connect
bool get isOpenIdConnect => type == SecuritySchemeType.openIdConnect;
/// Bearer
bool get isBearer => isHttp && scheme?.toLowerCase() == 'bearer';
/// Basic
bool get isBasic => isHttp && scheme?.toLowerCase() == 'basic';
/// OAuth2
bool get hasOAuth2Flows => flows?.hasAnyFlow ?? false;
/// API Key
String get apiKeyInfo {
if (!isApiKey || name == null || location == null) return '';
return '${location!.value}:$name';
}
/// HTTP
String get httpAuthInfo {
if (!isHttp || scheme == null) return '';
if (bearerFormat != null) {
return '$scheme ($bearerFormat)';
}
return scheme!;
}
}
///
enum SecuritySchemeType {
apiKey,
http,
oauth2,
openIdConnect,
}
extension SecuritySchemeTypeExtension on SecuritySchemeType {
String get value {
switch (this) {
case SecuritySchemeType.apiKey:
return 'apiKey';
case SecuritySchemeType.http:
return 'http';
case SecuritySchemeType.oauth2:
return 'oauth2';
case SecuritySchemeType.openIdConnect:
return 'openIdConnect';
}
}
static SecuritySchemeType fromString(String value) {
switch (value.toLowerCase()) {
case 'apikey':
return SecuritySchemeType.apiKey;
case 'http':
return SecuritySchemeType.http;
case 'oauth2':
return SecuritySchemeType.oauth2;
case 'openidconnect':
return SecuritySchemeType.openIdConnect;
default:
return SecuritySchemeType.apiKey;
}
}
}
/// API Key
enum ApiKeyLocation {
query,
header,
cookie,
}
extension ApiKeyLocationExtension on ApiKeyLocation {
String get value {
switch (this) {
case ApiKeyLocation.query:
return 'query';
case ApiKeyLocation.header:
return 'header';
case ApiKeyLocation.cookie:
return 'cookie';
}
}
static ApiKeyLocation fromString(String value) {
switch (value.toLowerCase()) {
case 'query':
return ApiKeyLocation.query;
case 'header':
return ApiKeyLocation.header;
case 'cookie':
return ApiKeyLocation.cookie;
default:
return ApiKeyLocation.header;
}
}
}
/// OAuth2
enum OAuth2FlowType {
authorizationCode,
implicit,
password,
clientCredentials,
}
extension OAuth2FlowTypeExtension on OAuth2FlowType {
String get value {
switch (this) {
case OAuth2FlowType.authorizationCode:
return 'authorizationCode';
case OAuth2FlowType.implicit:
return 'implicit';
case OAuth2FlowType.password:
return 'password';
case OAuth2FlowType.clientCredentials:
return 'clientCredentials';
}
}
static OAuth2FlowType fromString(String value) {
switch (value.toLowerCase()) {
case 'authorizationcode':
return OAuth2FlowType.authorizationCode;
case 'implicit':
return OAuth2FlowType.implicit;
case 'password':
return OAuth2FlowType.password;
case 'clientcredentials':
return OAuth2FlowType.clientCredentials;
default:
return OAuth2FlowType.authorizationCode;
}
}
}
/// OAuth2
class OAuth2Flow {
const OAuth2Flow({
this.authorizationUrl,
this.tokenUrl,
this.refreshUrl,
this.scopes = const {},
});
/// JSON OAuth2Flow
factory OAuth2Flow.fromJson(Map<String, dynamic> json) {
final scopesData = json['scopes'];
final Map<String, String> scopes;
if (scopesData is Map) {
scopes = scopesData
.map((key, value) => MapEntry(key.toString(), value.toString()));
} else {
scopes = {};
}
return OAuth2Flow(
authorizationUrl: json['authorizationUrl'] as String?,
tokenUrl: json['tokenUrl'] as String?,
refreshUrl: json['refreshUrl'] as String?,
scopes: scopes,
);
}
/// URL ( authorizationCode implicit )
final String? authorizationUrl;
/// URL ( authorizationCode, password clientCredentials )
final String? tokenUrl;
/// URL ()
final String? refreshUrl;
///
final Map<String, String> scopes;
bool get hasAuthorizationUrl =>
authorizationUrl != null && authorizationUrl!.isNotEmpty;
bool get hasTokenUrl => tokenUrl != null && tokenUrl!.isNotEmpty;
bool get hasRefreshUrl => refreshUrl != null && refreshUrl!.isNotEmpty;
bool get hasScopes => scopes.isNotEmpty;
}
/// OAuth2
class OAuth2Flows {
const OAuth2Flows({
this.authorizationCode,
this.implicit,
this.password,
this.clientCredentials,
});
/// JSON OAuth2Flows
factory OAuth2Flows.fromJson(Map<String, dynamic> json) {
return OAuth2Flows(
authorizationCode: json['authorizationCode'] != null
? OAuth2Flow.fromJson(
json['authorizationCode'] as Map<String, dynamic>,
)
: null,
implicit: json['implicit'] != null
? OAuth2Flow.fromJson(json['implicit'] as Map<String, dynamic>)
: null,
password: json['password'] != null
? OAuth2Flow.fromJson(json['password'] as Map<String, dynamic>)
: null,
clientCredentials: json['clientCredentials'] != null
? OAuth2Flow.fromJson(
json['clientCredentials'] as Map<String, dynamic>,
)
: null,
);
}
final OAuth2Flow? authorizationCode;
final OAuth2Flow? implicit;
final OAuth2Flow? password;
final OAuth2Flow? clientCredentials;
List<OAuth2FlowType> get availableFlows {
final flows = <OAuth2FlowType>[];
if (authorizationCode != null) flows.add(OAuth2FlowType.authorizationCode);
if (implicit != null) flows.add(OAuth2FlowType.implicit);
if (password != null) flows.add(OAuth2FlowType.password);
if (clientCredentials != null) flows.add(OAuth2FlowType.clientCredentials);
return flows;
}
bool get hasAnyFlow => availableFlows.isNotEmpty;
OAuth2Flow? getFlow(OAuth2FlowType type) {
switch (type) {
case OAuth2FlowType.authorizationCode:
return authorizationCode;
case OAuth2FlowType.implicit:
return implicit;
case OAuth2FlowType.password:
return password;
case OAuth2FlowType.clientCredentials:
return clientCredentials;
}
}
}
/// ()
class ApiSecurityRequirement {
const ApiSecurityRequirement({
this.requirements = const {},
});
/// JSON ApiSecurityRequirement
factory ApiSecurityRequirement.fromJson(Map<String, dynamic> json) {
final requirements = <String, List<String>>{};
json.forEach((schemeName, scopes) {
if (scopes is List) {
requirements[schemeName] = List<String>.from(scopes);
} else {
requirements[schemeName] = [];
}
});
return ApiSecurityRequirement(requirements: requirements);
}
///
final Map<String, List<String>> requirements;
///
bool get isEmpty => requirements.isEmpty;
///
bool get isNotEmpty => requirements.isNotEmpty;
///
Iterable<String> get schemeNames => requirements.keys;
///
bool hasScheme(String schemeName) => requirements.containsKey(schemeName);
///
List<String> getScopesForScheme(String schemeName) =>
List<String>.unmodifiable(requirements[schemeName] ?? <String>[]);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,749 @@
part of 'package:swagger_generator_flutter/core/models.dart';
class ApiDiscriminator {
const ApiDiscriminator({
required this.propertyName,
this.mapping = const {},
});
/// JSON创建ApiDiscriminator
factory ApiDiscriminator.fromJson(Map<String, dynamic> json) {
final mappingJson = json['mapping'] as Map<String, dynamic>? ?? {};
final mapping = <String, String>{};
mappingJson.forEach((key, value) {
if (value is String) {
mapping[key] = value;
}
});
return ApiDiscriminator(
propertyName: json['propertyName'] as String? ?? '',
mapping: mapping,
);
}
///
final String propertyName;
/// ( -> schema )
final Map<String, String> mapping;
///
bool get hasMapping => mapping.isNotEmpty;
/// schema
String? getSchemaForValue(String value) => mapping[value];
}
/// API Schema (OpenAPI 3.0)
/// JSON Schema
class ApiSchema {
const ApiSchema({
this.type,
this.format,
this.description = '',
this.properties = const {},
this.required = const [],
this.items,
this.reference,
this.enumValues = const [],
this.allOf = const [],
this.oneOf = const [],
this.anyOf = const [],
this.not,
this.discriminator,
this.additionalProperties,
this.patternProperties = const {},
this.propertyNames,
this.dependencies = const {},
this.constValue,
this.ifSchema,
this.thenSchema,
this.elseSchema,
this.minimum,
this.maximum,
this.exclusiveMinimum,
this.exclusiveMaximum,
this.minLength,
this.maxLength,
this.pattern,
this.minItems,
this.maxItems,
this.uniqueItems,
this.nullable = false,
this.example,
this.defaultValue,
});
/// JSON创建ApiSchema
factory ApiSchema.fromJson(Map<String, dynamic> json) {
// properties
final propertiesJson = json['properties'] as Map<String, dynamic>? ?? {};
final properties = <String, ApiProperty>{};
final requiredFields = (json['required'] as List<dynamic>?)
?.map((e) => e.toString())
.toList() ??
[];
propertiesJson.forEach((propName, propData) {
if (propData is Map<String, dynamic>) {
properties[propName] = ApiProperty.fromJson(
propName,
propData,
requiredFields,
);
}
});
// items ()
final itemsJson = json['items'] as Map<String, dynamic>?;
final items = itemsJson != null ? ApiSchema.fromJson(itemsJson) : null;
//
final allOfJson = json['allOf'] as List<dynamic>? ?? [];
final allOf = allOfJson
.map((schema) => ApiSchema.fromJson(schema as Map<String, dynamic>))
.toList();
final oneOfJson = json['oneOf'] as List<dynamic>? ?? [];
final oneOf = oneOfJson
.map((schema) => ApiSchema.fromJson(schema as Map<String, dynamic>))
.toList();
final anyOfJson = json['anyOf'] as List<dynamic>? ?? [];
final anyOf = anyOfJson
.map((schema) => ApiSchema.fromJson(schema as Map<String, dynamic>))
.toList();
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;
// patternProperties
final patternPropertiesJson =
json['patternProperties'] as Map<String, dynamic>? ?? {};
final patternProperties = <String, ApiSchema>{};
patternPropertiesJson.forEach((pattern, schemaData) {
if (schemaData is Map<String, dynamic>) {
patternProperties[pattern] = ApiSchema.fromJson(schemaData);
}
});
// propertyNames
final propertyNamesJson = json['propertyNames'] as Map<String, dynamic>?;
final propertyNames = propertyNamesJson != null
? ApiSchema.fromJson(propertyNamesJson)
: null;
// dependencies
final dependencies = json['dependencies'] as Map<String, dynamic>? ?? {};
// Schema (if/then/else)
final ifJson = json['if'] as Map<String, dynamic>?;
final ifSchema = ifJson != null ? ApiSchema.fromJson(ifJson) : null;
final thenJson = json['then'] as Map<String, dynamic>?;
final thenSchema = thenJson != null ? ApiSchema.fromJson(thenJson) : null;
final elseJson = json['else'] as Map<String, dynamic>?;
final elseSchema = elseJson != null ? ApiSchema.fromJson(elseJson) : null;
//
String? reference;
if (json[r'$ref'] != null) {
final ref = json[r'$ref'] as String;
reference = ref.split('/').last;
}
return ApiSchema(
type: json['type'] as String?,
format: json['format'] as String?,
description: json['description'] as String? ?? '',
properties: properties,
required: requiredFields,
items: items,
reference: reference,
enumValues: (json['enum'] as List<dynamic>?) ?? [],
allOf: allOf,
oneOf: oneOf,
anyOf: anyOf,
not: not,
discriminator: discriminator,
additionalProperties: json['additionalProperties'],
patternProperties: patternProperties,
propertyNames: propertyNames,
dependencies: dependencies,
constValue: json['const'],
ifSchema: ifSchema,
thenSchema: thenSchema,
elseSchema: elseSchema,
minimum: json['minimum'] as num?,
maximum: json['maximum'] as num?,
exclusiveMinimum: json['exclusiveMinimum'] as bool?,
exclusiveMaximum: json['exclusiveMaximum'] as bool?,
minLength: json['minLength'] as int?,
maxLength: json['maxLength'] as int?,
pattern: json['pattern'] as String?,
minItems: json['minItems'] as int?,
maxItems: json['maxItems'] as int?,
uniqueItems: json['uniqueItems'] as bool?,
nullable: json['nullable'] as bool? ?? false,
example: json['example'],
defaultValue: json['default'],
);
}
/// Schema
final String? type;
/// Schema
final String? format;
/// Schema
final String description;
/// ( object )
final Map<String, ApiProperty> properties;
///
final List<String> required;
/// ( array )
final ApiSchema? items;
/// ($ref)
final String? reference;
///
final List<dynamic> enumValues;
///
final List<ApiSchema> allOf;
final List<ApiSchema> oneOf;
final List<ApiSchema> anyOf;
final ApiSchema? not;
/// (OpenAPI 3.0)
final ApiDiscriminator? discriminator;
/// ( boolean Schema)
final dynamic additionalProperties;
/// (patternProperties)
final Map<String, ApiSchema> patternProperties;
///
final ApiSchema? propertyNames;
///
final Map<String, dynamic> dependencies;
///
final dynamic constValue;
/// Schema (if/then/else)
final ApiSchema? ifSchema;
final ApiSchema? thenSchema;
final ApiSchema? elseSchema;
/// / ()
final num? minimum;
final num? maximum;
final bool? exclusiveMinimum;
final bool? exclusiveMaximum;
///
final int? minLength;
final int? maxLength;
final String? pattern;
///
final int? minItems;
final int? maxItems;
final bool? uniqueItems;
///
final bool nullable;
///
final dynamic example;
///
final dynamic defaultValue;
///
bool get isComposition =>
allOf.isNotEmpty || oneOf.isNotEmpty || anyOf.isNotEmpty;
/// allOf
bool get isAllOf => allOf.isNotEmpty;
/// oneOf
bool get isOneOf => oneOf.isNotEmpty;
/// anyOf
bool get isAnyOf => anyOf.isNotEmpty;
/// not
bool get hasNot => not != null;
///
bool get hasDiscriminator => discriminator != null;
///
bool get isReference => reference != null;
///
bool get isEnum => enumValues.isNotEmpty;
///
bool get isArray => type == 'array';
///
bool get isObject => type == 'object' || properties.isNotEmpty;
///
bool get hasPatternProperties => patternProperties.isNotEmpty;
///
bool get hasPropertyNames => propertyNames != null;
///
bool get hasDependencies => dependencies.isNotEmpty;
///
bool get hasConstValue => constValue != null;
/// Schema
bool get hasConditionalSchema =>
ifSchema != null || thenSchema != null || elseSchema != null;
///
bool get allowsAdditionalProperties {
if (additionalProperties == null) return true; //
if (additionalProperties is bool) return additionalProperties as bool;
return true; // Schema
}
/// Schema additionalProperties Schema
ApiSchema? get additionalPropertiesSchema {
final additionalProps = additionalProperties;
if (additionalProps is Map<String, dynamic>) {
return ApiSchema.fromJson(additionalProps);
}
return null;
}
}
/// API模型信息
class ApiModel {
const ApiModel({
required this.name,
required this.description,
required this.properties,
required this.required,
this.isEnum = false,
this.enumValues = const [],
this.enumType,
this.enumVarNames,
this.enumDescriptions,
this.allOf = const [],
this.oneOf = const [],
this.anyOf = const [],
this.not,
this.discriminator,
this.usageType = ModelUsageType.unknown,
this.type,
});
/// JSON创建ApiModel
factory ApiModel.fromJson(
String name,
Map<String, dynamic> json, {
ModelUsageType usageType = ModelUsageType.unknown,
}) {
final isEnum = json['enum'] != null;
final enumValues =
isEnum ? (json['enum'] as List<dynamic>?) ?? <dynamic>[] : <dynamic>[];
// OpenAPI x-enum-varnames x-enum-descriptions
final enumVarNames = json['x-enum-varnames'] != null
? (json['x-enum-varnames'] as List<dynamic>?)
?.map((e) => e.toString())
.toList()
: null;
final enumDescriptions = json['x-enum-descriptions'] != null
? (json['x-enum-descriptions'] as List<dynamic>?)
?.map((e) => e.toString())
.toList()
: null;
final properties = json['properties'] as Map<String, dynamic>? ?? {};
List<String> required;
if (json.containsKey('required')) {
required = (json['required'] as List<dynamic>?)
?.map((e) => e.toString())
.toList() ??
[];
} else {
// required nullable != true required
required = properties.entries
.where((entry) {
final value = entry.value;
if (value is Map<String, dynamic>) {
return !(value['nullable'] as bool? ?? false);
}
return true;
})
.map((entry) => entry.key)
.toList();
}
//
final allOfJson = json['allOf'] as List<dynamic>? ?? [];
final allOf = allOfJson
.map((schema) => ApiSchema.fromJson(schema as Map<String, dynamic>))
.toList();
final oneOfJson = json['oneOf'] as List<dynamic>? ?? [];
final oneOf = oneOfJson
.map((schema) => ApiSchema.fromJson(schema as Map<String, dynamic>))
.toList();
final anyOfJson = json['anyOf'] as List<dynamic>? ?? [];
final anyOf = anyOfJson
.map((schema) => ApiSchema.fromJson(schema as Map<String, dynamic>))
.toList();
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;
return ApiModel(
type: json['type'] as String?,
name: name,
description: json['description'] as String? ?? '',
required: required,
isEnum: isEnum,
enumValues: enumValues,
enumType: isEnum
? PropertyType.fromString(json['type'] as String? ?? 'string')
: null,
enumVarNames: enumVarNames,
enumDescriptions: enumDescriptions,
allOf: allOf,
oneOf: oneOf,
anyOf: anyOf,
not: not,
discriminator: discriminator,
usageType: usageType,
properties: properties.map(
(propName, propData) => MapEntry(
propName,
ApiProperty.fromJson(
propName,
propData as Map<String, dynamic>,
required,
),
),
),
);
}
final String name;
final String description;
final Map<String, ApiProperty> properties;
final List<String> required;
final bool isEnum;
final List<dynamic> enumValues;
final PropertyType? enumType;
/// OpenAPI extension: x-enum-varnames
/// enumValues
final List<String>? enumVarNames;
/// OpenAPI extension: x-enum-descriptions
/// enumValues
final List<String>? enumDescriptions;
/// (OpenAPI 3.0)
final List<ApiSchema> allOf;
final List<ApiSchema> oneOf;
final List<ApiSchema> anyOf;
final ApiSchema? not;
/// (OpenAPI 3.0)
final ApiDiscriminator? discriminator;
///
/// API ///
final ModelUsageType usageType;
final String? type;
/// 使
bool get isComposition =>
allOf.isNotEmpty || oneOf.isNotEmpty || anyOf.isNotEmpty;
/// allOf
bool get isAllOf => allOf.isNotEmpty;
/// oneOf
bool get isOneOf => oneOf.isNotEmpty;
/// anyOf
bool get isAnyOf => anyOf.isNotEmpty;
/// not
bool get hasNot => not != null;
///
bool get hasDiscriminator => discriminator != null;
///
/// schema 使
ApiModel copyWithUsageType(ModelUsageType newUsageType) {
return ApiModel(
name: name,
description: description,
properties: properties,
required: required,
isEnum: isEnum,
enumValues: enumValues,
enumType: enumType,
allOf: allOf,
oneOf: oneOf,
anyOf: anyOf,
not: not,
discriminator: discriminator,
usageType: newUsageType,
type: type,
);
}
}
/// API属性信息
class ApiProperty {
const ApiProperty({
required this.name,
required this.type,
required this.description,
required this.required,
this.format,
this.nullable = false,
this.example,
this.defaultValue,
this.reference,
this.items,
this.nestedProperties = const {},
this.nestedRequired = const [],
this.schema,
});
/// JSON创建ApiProperty
factory ApiProperty.fromJson(
String name,
Map<String, dynamic> json,
List<String> requiredFields, {
int maxDepth = 10,
int currentDepth = 0,
}) {
//
if (currentDepth >= maxDepth) {
return ApiProperty(
name: name,
type: PropertyType.object,
description: '达到最大嵌套深度的对象',
required: requiredFields.contains(name),
);
}
final type = PropertyType.fromString(json['type'] as String? ?? 'string');
String? reference;
ApiModel? items;
final nestedProperties = <String, ApiProperty>{};
var nestedRequired = <String>[];
ApiSchema? schema;
//
if (json[r'$ref'] != null) {
final ref = json[r'$ref'] as String;
reference = ref.split('/').last;
}
// schema
if (json['allOf'] != null ||
json['oneOf'] != null ||
json['anyOf'] != null) {
schema = ApiSchema.fromJson(json);
}
//
if (type == PropertyType.object && json['properties'] != null) {
final propertiesJson = json['properties'] as Map<String, dynamic>;
nestedRequired = (json['required'] as List<dynamic>?)
?.map((e) => e.toString())
.toList() ??
[];
propertiesJson.forEach((propName, propData) {
if (propData is Map<String, dynamic>) {
try {
final nestedProperty = ApiProperty.fromJson(
propName,
propData,
nestedRequired,
maxDepth: maxDepth,
currentDepth: currentDepth + 1,
);
nestedProperties[propName] = nestedProperty;
} on Exception {
//
nestedProperties[propName] = ApiProperty(
name: propName,
type: PropertyType.string,
description: '解析失败的嵌套属性',
required: nestedRequired.contains(propName),
);
}
}
});
}
// items
if (type == PropertyType.array && json['items'] != null) {
final itemsJson = json['items'] as Map<String, dynamic>;
// items
if (itemsJson[r'$ref'] != null) {
final itemRef = itemsJson[r'$ref'] as String;
final itemRefName = itemRef.split('/').last;
items = ApiModel(
name: itemRefName,
description: '',
properties: {},
required: [],
);
} else if (itemsJson['type'] == 'object' &&
itemsJson['properties'] != null) {
// items
final itemProperties = <String, ApiProperty>{};
final itemRequired = (itemsJson['required'] as List<dynamic>?)
?.map((e) => e.toString())
.toList() ??
[];
final itemPropertiesJson =
itemsJson['properties'] as Map<String, dynamic>;
for (final entry in itemPropertiesJson.entries) {
final propName = entry.key;
final propData = entry.value;
if (propData is Map<String, dynamic>) {
ApiProperty itemProperty;
try {
itemProperty = ApiProperty.fromJson(
propName,
propData,
itemRequired,
maxDepth: maxDepth,
currentDepth: currentDepth + 1,
);
} on Exception {
//
itemProperty = ApiProperty(
name: propName,
type: PropertyType.string,
description: '解析失败的数组项属性',
required: itemRequired.contains(propName),
);
}
itemProperties[propName] = itemProperty;
}
}
items = ApiModel(
name: '${name}Item',
description: itemsJson['description'] as String? ?? '',
properties: itemProperties,
required: itemRequired,
);
} else {
// items
final itemType =
PropertyType.fromString(itemsJson['type'] as String? ?? 'string');
items = ApiModel(
name: itemType.value,
description: '',
properties: {},
required: [],
);
}
}
return ApiProperty(
name: name,
type: reference != null ? PropertyType.reference : type,
format: json['format'] as String?,
description: json['description'] as String? ?? '',
required: requiredFields.contains(name),
nullable: json['nullable'] as bool? ?? false,
example: json['example'],
defaultValue: json['default'],
reference: reference,
items: items,
nestedProperties: nestedProperties,
nestedRequired: nestedRequired,
schema: schema,
);
}
final String name;
final PropertyType type;
final String? format;
final String description;
final bool required;
final bool nullable;
final dynamic example;
final dynamic defaultValue;
final String? reference;
final ApiModel? items; //
/// ( object )
final Map<String, ApiProperty> nestedProperties;
///
final List<String> nestedRequired;
/// Schema ()
final ApiSchema? schema;
///
bool get hasNestedProperties => nestedProperties.isNotEmpty;
/// schema
bool get hasComplexSchema => schema != null;
}
/// API控制器信息
class ApiController {
const ApiController({
required this.name,
required this.description,
required this.paths,
});
/// ApiController
factory ApiController.fromPaths(String name, List<ApiPath> paths) {
return ApiController(name: name, description: name, paths: paths);
}
final String name;
final String description;
final List<ApiPath> paths;
}

View File

@ -0,0 +1,70 @@
part of 'package:swagger_generator_flutter/core/models.dart';
/// API服务器信息 (OpenAPI 3.0)
class ApiServer {
const ApiServer({
required this.url,
this.description = '',
this.variables = const {},
});
/// JSON创建ApiServer
factory ApiServer.fromJson(Map<String, dynamic> json) {
final variablesJson = json['variables'];
final variables = <String, ApiServerVariable>{};
if (variablesJson != null && variablesJson is Map) {
variablesJson.forEach((key, value) {
if (value is Map) {
variables[key.toString()] = ApiServerVariable.fromJson(
Map<String, dynamic>.from(value),
);
}
});
}
return ApiServer(
url: json['url'] as String? ?? '',
description: json['description'] as String? ?? '',
variables: variables,
);
}
/// URL
final String url;
///
final String description;
///
final Map<String, ApiServerVariable> variables;
}
/// API服务器变量 (OpenAPI 3.0)
class ApiServerVariable {
const ApiServerVariable({
required this.defaultValue,
this.enumValues = const [],
this.description = '',
});
/// JSON创建ApiServerVariable
factory ApiServerVariable.fromJson(Map<String, dynamic> json) {
return ApiServerVariable(
enumValues:
(json['enum'] as List<dynamic>?)?.map((e) => e.toString()).toList() ??
[],
defaultValue: json['default'] as String? ?? '',
description: json['description'] as String? ?? '',
);
}
///
final List<String> enumValues;
///
final String defaultValue;
///
final String description;
}

142
lib/core/models/enums.dart Normal file
View File

@ -0,0 +1,142 @@
part of 'package:swagger_generator_flutter/core/models.dart';
/// HTTP方法枚举
/// RESTful API
enum HttpMethod {
/// GET
get('GET'),
/// POST
post('POST'),
/// PUT
put('PUT'),
/// DELETE
delete('DELETE'),
/// PATCH
patch('PATCH'),
/// HEAD
head('HEAD'),
/// OPTIONS
options('OPTIONS');
///
const HttpMethod(this.value);
final String value;
/// HttpMethod
static HttpMethod fromString(String value) {
return HttpMethod.values.firstWhere(
(method) => method.value == value.toUpperCase(),
orElse: () => HttpMethod.get,
);
}
}
///
/// API 使
enum ModelUsageType {
/// - requestBody parameters
/// defaultValue
request,
/// - response
/// defaultValue
response,
/// -
/// defaultValue
common,
/// - API 使
/// defaultValue
unknown,
}
///
enum PropertyType {
///
string('string'),
///
integer('integer'),
///
number('number'),
///
boolean('boolean'),
///
array('array'),
///
object('object'),
///
bytes('string'),
///
date('string'),
///
dateTime('string'),
///
file('file'),
///
enumType('enum'),
///
reference('reference'),
///
unknown('unknown');
const PropertyType(this.value);
final String value;
static PropertyType fromString(String? type) {
if (type == null) return PropertyType.unknown;
return PropertyType.values.firstWhere(
(value) => value.value == type,
orElse: () => PropertyType.unknown,
);
}
}
///
enum ParameterLocation {
///
query('query'),
///
body('body'),
///
path('path'),
///
header('header'),
/// Cookie
cookie('cookie');
const ParameterLocation(this.value);
final String value;
static ParameterLocation fromString(String? value) {
if (value == null) return ParameterLocation.query;
return ParameterLocation.values.firstWhere(
(location) => location.value == value,
orElse: () => ParameterLocation.query,
);
}
}

View File

@ -0,0 +1,137 @@
part of 'package:swagger_generator_flutter/core/models.dart';
/// Swagger pathsschemas
class SwaggerDocument {
const SwaggerDocument({
required this.title,
required this.version,
required this.description,
required this.paths,
required this.models,
required this.controllers,
this.servers = const [],
this.components = const ApiComponents(),
this.security = const [],
});
factory SwaggerDocument.fromJson(Map<String, dynamic> json) {
final info = json['info'] as Map<String, dynamic>? ?? {};
if (info.isEmpty) {
throw const FormatException('info object is required');
}
// servers (OpenAPI 3.0)
final serversJson = json['servers'] as List<dynamic>? ?? [];
final servers = serversJson
.map((server) => ApiServer.fromJson(server as Map<String, dynamic>))
.toList();
if (servers.isEmpty) {
servers.add(const ApiServer(url: '/'));
}
// components
final componentsJson = json['components'];
final components = componentsJson != null && componentsJson is Map
? ApiComponents.fromJson(Map<String, dynamic>.from(componentsJson))
: const ApiComponents();
//
final securityJson = json['security'] as List<dynamic>? ?? [];
final security = securityJson
.whereType<Map<dynamic, dynamic>>()
.map(
(s) => ApiSecurityRequirement.fromJson(
Map<String, dynamic>.from(s),
),
)
.toList();
final rawPathsValue = json['paths'];
final rawPaths = rawPathsValue is Map
? Map<String, dynamic>.from(rawPathsValue)
: <String, dynamic>{};
final parsedPaths = _parsePaths(rawPaths);
return SwaggerDocument(
title: info['title'] as String? ?? 'API',
version: info['version'] as String? ?? '1.0.0',
description: info['description'] as String? ?? '',
servers: servers,
components: components,
paths: parsedPaths,
models: components.schemas,
controllers: {},
security: security,
);
}
final String title;
final String version;
final String description;
final List<ApiServer> servers;
final ApiComponents components;
final Map<ApiPathKey, ApiPath> paths;
final Map<String, ApiModel> models;
final Map<String, ApiController> controllers;
final List<ApiSecurityRequirement> security;
/// key Map
static ApiPathKey buildPathKey(String pattern, HttpMethod method) =>
ApiPathKey.from(pattern, method);
/// + ApiPath
ApiPath? findPath(String pattern, HttpMethod method) {
return paths[buildPathKey(pattern, method)];
}
///
Iterable<ApiPath> operationsFor(String pattern) sync* {
for (final entry in paths.entries) {
if (entry.key.pattern == pattern) {
yield entry.value;
}
}
}
/// 使 path + method
static Map<ApiPathKey, ApiPath> _parsePaths(
Map<String, dynamic> pathsJson,
) {
final paths = <ApiPathKey, ApiPath>{};
final methodLookup = {
for (final method in HttpMethod.values) method.name: method,
};
pathsJson.forEach((pattern, pathEntry) {
if (pathEntry is! Map) return;
final pathData = Map<String, dynamic>.from(pathEntry);
for (final methodEntry in pathData.entries) {
final methodName = methodEntry.key.toLowerCase();
final httpMethod = methodLookup[methodName];
final operationData = methodEntry.value;
if (httpMethod == null || operationData is! Map) {
continue;
}
final apiPath = ApiPath.fromJson(
pattern,
httpMethod,
Map<String, dynamic>.from(operationData),
);
final key = ApiPathKey.from(pattern, httpMethod);
if (paths.containsKey(key)) {
//
paths[key] = apiPath;
} else {
paths[key] = apiPath;
}
}
});
return paths;
}
}

View File

@ -4,19 +4,12 @@ library;
import 'dart:async';
import 'dart:convert';
import 'models.dart';
import 'dart:isolate';
import 'package:swagger_generator_flutter/core/models.dart';
///
class ParsePerformanceStats {
final Duration totalTime;
final Duration parseTime;
final Duration validationTime;
final Duration modelCreationTime;
final int memoryUsage;
final int documentSize;
final int pathCount;
final int schemaCount;
const ParsePerformanceStats({
required this.totalTime,
required this.parseTime,
@ -27,6 +20,14 @@ class ParsePerformanceStats {
required this.pathCount,
required this.schemaCount,
});
final Duration totalTime;
final Duration parseTime;
final Duration validationTime;
final Duration modelCreationTime;
final int memoryUsage;
final int documentSize;
final int pathCount;
final int schemaCount;
double get pathsPerSecond => pathCount / totalTime.inMilliseconds * 1000;
double get schemasPerSecond => schemaCount / totalTime.inMilliseconds * 1000;
@ -51,6 +52,17 @@ Performance Statistics:
///
class ParseConfig {
const ParseConfig({
this.enableParallelParsing = true,
this.enableStreamParsing = false,
this.enableIncrementalParsing = false,
this.enableCaching = true,
this.maxConcurrency = 4,
this.streamBufferSize = 8192,
this.enablePerformanceStats = false,
this.enableMemoryOptimization = true,
});
///
final bool enableParallelParsing;
@ -74,28 +86,16 @@ class ParseConfig {
///
final bool enableMemoryOptimization;
const ParseConfig({
this.enableParallelParsing = true,
this.enableStreamParsing = false,
this.enableIncrementalParsing = false,
this.enableCaching = true,
this.maxConcurrency = 4,
this.streamBufferSize = 8192,
this.enablePerformanceStats = false,
this.enableMemoryOptimization = true,
});
}
///
class PerformanceParser {
PerformanceParser({ParseConfig? config})
: _config = config ?? const ParseConfig();
final ParseConfig _config;
final Map<String, dynamic> _cache = {};
ParsePerformanceStats? _lastStats;
PerformanceParser({ParseConfig? config})
: _config = config ?? const ParseConfig();
///
ParsePerformanceStats? get lastStats => _lastStats;
@ -165,7 +165,7 @@ class PerformanceParser {
//
final chunks = <String>[];
for (int i = 0; i < jsonString.length; i += _config.streamBufferSize) {
for (var i = 0; i < jsonString.length; i += _config.streamBufferSize) {
final end = (i + _config.streamBufferSize).clamp(0, jsonString.length);
chunks.add(jsonString.substring(i, end));
}
@ -186,142 +186,188 @@ class PerformanceParser {
);
//
for (final chunk in chunks) {
controller.add(chunk);
}
controller.close();
chunks.forEach(controller.add);
await controller.close();
return completer.future;
}
///
Future<SwaggerDocument> _parseDocumentParallel(
Map<String, dynamic> json) async {
final futures = <Future>[];
Map<String, dynamic> json,
) async {
final futures = <Future<void>>[];
final results = <String, dynamic>{};
//
if (json.containsKey('paths')) {
futures.add(_parsePathsParallel(json['paths'] as Map<String, dynamic>)
.then((paths) => results['paths'] = paths));
futures.add(
_parsePathsParallel(json['paths'] as Map<String, dynamic>)
.then((paths) => results['paths'] = paths),
);
}
if (json.containsKey('components')) {
futures.add(
_parseComponentsParallel(json['components'] as Map<String, dynamic>)
.then((components) => results['components'] = components));
_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));
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);
final info = json['info'] as Map<String, dynamic>? ?? {};
final title = info['title'] as String? ?? 'API';
final version = info['version'] as String? ?? '1.0.0';
final description = info['description'] as String? ?? '';
return SwaggerDocument.fromJson(mergedJson);
final servers = (results['servers'] as List<ApiServer>?) ??
_parseServersSequential(json['servers'] as List<dynamic>? ?? []);
if (servers.isEmpty) {
servers.add(const ApiServer(url: '/'));
}
final components = (results['components'] as ApiComponents?) ??
(json['components'] is Map
? ApiComponents.fromJson(
Map<String, dynamic>.from(json['components'] as Map),
)
: const ApiComponents());
final securityJson = json['security'] as List<dynamic>? ?? [];
final security = securityJson
.whereType<Map<dynamic, dynamic>>()
.map(
(s) => ApiSecurityRequirement.fromJson(
Map<String, dynamic>.from(s),
),
)
.toList();
final parsedPaths = (results['paths'] as Map<ApiPathKey, ApiPath>?) ??
_parsePathsSequential(json['paths'] as Map<String, dynamic>? ?? {});
return SwaggerDocument(
title: title,
version: version,
description: description,
servers: servers,
components: components,
paths: parsedPaths,
models: components.schemas,
controllers: {},
security: security,
);
}
///
Future<Map<String, ApiPath>> _parsePathsParallel(
Map<String, dynamic> pathsJson) async {
Future<Map<ApiPathKey, 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 futures = chunks.map(
(chunk) => Isolate.run(() => _parsePathsSequential(chunk)),
);
final results = await Future.wait(futures);
//
final mergedPaths = <String, ApiPath>{};
for (final pathMap in results) {
mergedPaths.addAll(pathMap);
}
final mergedPaths = <ApiPathKey, ApiPath>{};
results.forEach(mergedPaths.addAll);
return mergedPaths;
}
///
Future<ApiComponents> _parseComponentsParallel(
Map<String, dynamic> componentsJson) async {
final futures = <Future>[];
Map<String, dynamic> componentsJson,
) async {
final futures = <Future<void>>[];
final results = <String, dynamic>{};
if (componentsJson.containsKey('schemas')) {
futures.add(_parseSchemasParallel(
componentsJson['schemas'] as Map<String, dynamic>)
.then((schemas) => results['schemas'] = schemas));
futures.add(
_parseSchemasParallel(
componentsJson['schemas'] as Map<String, dynamic>,
).then((schemas) => results['schemas'] = schemas),
);
}
if (componentsJson.containsKey('securitySchemes')) {
futures.add(_parseSecuritySchemesParallel(
componentsJson['securitySchemes'] as Map<String, dynamic>)
.then((schemes) => results['securitySchemes'] = schemes));
futures.add(
_parseSecuritySchemesParallel(
componentsJson['securitySchemes'] as Map<String, dynamic>,
).then((schemes) => results['securitySchemes'] = schemes),
);
}
await Future.wait(futures);
final mergedComponents = Map<String, dynamic>.from(componentsJson);
mergedComponents.addAll(results);
final mergedComponents = Map<String, dynamic>.from(componentsJson)
..addAll(results);
return ApiComponents.fromJson(mergedComponents);
}
///
Future<List<ApiServer>> _parseServersParallel(
List<dynamic> serversJson) async {
List<dynamic> serversJson,
) async {
if (serversJson.length <= _config.maxConcurrency) {
return serversJson
.map((json) => ApiServer.fromJson(json as Map<String, dynamic>))
.toList();
return _parseServersSequential(serversJson);
}
final chunks = _chunkList(serversJson, _config.maxConcurrency);
final futures = chunks.map((chunk) => _parseServerChunk(chunk));
final futures = chunks.map(
(chunk) => Isolate.run(() => _parseServersSequential(chunk)),
);
final results = await Future.wait(futures);
//
final mergedServers = <ApiServer>[];
for (final serverList in results) {
mergedServers.addAll(serverList);
}
results.forEach(mergedServers.addAll);
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
/// Isolate 使
static List<ApiServer> _parseServersSequential(List<dynamic> serversJson) {
return serversJson
.map((json) => ApiServer.fromJson(json as Map<String, dynamic>))
.toList();
}
///
Map<String, ApiPath> _parsePathsSequential(Map<String, dynamic> pathsJson) {
final paths = <String, ApiPath>{};
/// Isolate 使
static Map<ApiPathKey, ApiPath> _parsePathsSequential(
Map<String, dynamic> pathsJson,
) {
final paths = <ApiPathKey, 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) {
final httpMethod = HttpMethod.fromString(method);
final apiPath = ApiPath.fromJson(
pathPattern,
httpMethod,
operationData,
);
final key = ApiPathKey.from(pathPattern, httpMethod);
paths[key] = apiPath;
} on Exception {
//
}
}
@ -334,27 +380,29 @@ class PerformanceParser {
/// Schemas
Future<Map<String, ApiModel>> _parseSchemasParallel(
Map<String, dynamic> schemasJson) async {
Map<String, dynamic> schemasJson,
) async {
if (schemasJson.length <= _config.maxConcurrency) {
return _parseSchemasSequential(schemasJson);
}
final chunks = _chunkMap(schemasJson, _config.maxConcurrency);
final futures = chunks.map((chunk) => _parseSchemaChunk(chunk));
final futures = chunks.map(
(chunk) => Isolate.run(() => _parseSchemasSequential(chunk)),
);
final results = await Future.wait(futures);
//
final mergedSchemas = <String, ApiModel>{};
for (final schemaMap in results) {
mergedSchemas.addAll(schemaMap);
}
results.forEach(mergedSchemas.addAll);
return mergedSchemas;
}
///
Future<Map<String, ApiSecurityScheme>> _parseSecuritySchemesParallel(
Map<String, dynamic> schemesJson) async {
Map<String, dynamic> schemesJson,
) async {
final schemes = <String, ApiSecurityScheme>{};
schemesJson.forEach((name, schemeData) {
@ -362,7 +410,7 @@ class PerformanceParser {
try {
final scheme = ApiSecurityScheme.fromJson(schemeData);
schemes[name] = scheme;
} catch (e) {
} on Exception {
//
}
}
@ -371,15 +419,10 @@ class PerformanceParser {
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) {
/// Schemas Isolate 使
static Map<String, ApiModel> _parseSchemasSequential(
Map<String, dynamic> schemasJson,
) {
final schemas = <String, ApiModel>{};
schemasJson.forEach((name, schemaData) {
@ -387,7 +430,7 @@ class PerformanceParser {
try {
final model = ApiModel.fromJson(name, schemaData);
schemas[name] = model;
} catch (e) {
} on Exception {
// schema
}
}
@ -400,12 +443,14 @@ class PerformanceParser {
void _validateBasicStructure(Map<String, dynamic> json) {
if (!json.containsKey('openapi') && !json.containsKey('swagger')) {
throw const FormatException(
'Invalid OpenAPI document: missing version field');
'Invalid OpenAPI document: missing version field',
);
}
if (!json.containsKey('info')) {
throw const FormatException(
'Invalid OpenAPI document: missing info object');
'Invalid OpenAPI document: missing info object',
);
}
final info = json['info'] as Map<String, dynamic>?;
@ -413,21 +458,24 @@ class PerformanceParser {
!info.containsKey('title') ||
!info.containsKey('version')) {
throw const FormatException(
'Invalid OpenAPI document: info object must contain title and version');
'Invalid OpenAPI document: info object must contain title and version',
);
}
}
/// Map
List<Map<String, dynamic>> _chunkMap(
Map<String, dynamic> map, int chunkSize) {
Map<String, dynamic> map,
int chunkSize,
) {
final chunks = <Map<String, dynamic>>[];
final entries = map.entries.toList();
for (int i = 0; i < entries.length; i += chunkSize) {
for (var i = 0; i < entries.length; i += chunkSize) {
final end = (i + chunkSize).clamp(0, entries.length);
final chunk = <String, dynamic>{};
for (int j = i; j < end; j++) {
for (var j = i; j < end; j++) {
final entry = entries[j];
chunk[entry.key] = entry.value;
}
@ -442,7 +490,7 @@ class PerformanceParser {
List<List<dynamic>> _chunkList(List<dynamic> list, int chunkSize) {
final chunks = <List<dynamic>>[];
for (int i = 0; i < list.length; i += chunkSize) {
for (var i = 0; i < list.length; i += chunkSize) {
final end = (i + chunkSize).clamp(0, list.length);
chunks.add(list.sublist(i, end));
}

View File

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

View File

@ -0,0 +1,4 @@
/// Backward-compat shim for TemplateRenderer
library;
export 'package:swagger_generator_flutter/pipeline/render/impl/template_renderer.dart';

View File

@ -1,711 +0,0 @@
import '../core/models.dart';
import '../utils/string_utils.dart';
import 'base_generator.dart';
///
/// API文档
class DocumentationGenerator extends BaseGenerator {
final SwaggerDocument document;
final bool includeExamples;
final bool includeSchemas;
final bool includeResponses;
final String? customTitle;
DocumentationGenerator(
this.document, {
this.includeExamples = true,
this.includeSchemas = true,
this.includeResponses = true,
this.customTitle,
});
@override
String get generatorType => 'DocumentationGenerator';
@override
String generate() {
final buffer = StringBuffer();
//
_generateHeader(buffer);
//
_generateTableOfContents(buffer);
// API概述
_generateApiOverview(buffer);
//
_generateAuthenticationInfo(buffer);
// API端点文档
_generateEndpointsDocumentation(buffer);
//
if (includeSchemas) {
_generateSchemasDocumentation(buffer);
}
//
_generateErrorCodesDocumentation(buffer);
//
if (includeExamples) {
_generateExamplesDocumentation(buffer);
}
//
_generateChangeLog(buffer);
return generateTypeCheckedCode(buffer.toString());
}
///
void _generateHeader(StringBuffer buffer) {
final title = customTitle ?? document.title;
buffer.writeln('# $title');
buffer.writeln('');
if (document.description.isNotEmpty) {
buffer.writeln('${document.description}');
buffer.writeln('');
}
buffer.writeln('**版本**: ${document.version}');
buffer.writeln('**基础URL**: ${_getBaseUrl()}');
buffer.writeln('**生成时间**: ${DateTime.now().toIso8601String()}');
buffer.writeln('');
//
buffer.writeln(
'![API版本](https://img.shields.io/badge/API-${document.version}-blue.svg)');
buffer.writeln('![状态](https://img.shields.io/badge/状态-活跃-green.svg)');
buffer.writeln('');
}
///
void _generateTableOfContents(StringBuffer buffer) {
buffer.writeln('## 📋 目录');
buffer.writeln('');
buffer.writeln('- [API概述](#api概述)');
buffer.writeln('- [认证](#认证)');
buffer.writeln('- [API端点](#api端点)');
//
final controllerGroups = _groupPathsByController();
for (final controllerName in controllerGroups.keys) {
final anchor = controllerName.toLowerCase().replaceAll(' ', '-');
buffer.writeln(' - [$controllerName](#$anchor)');
}
if (includeSchemas) {
buffer.writeln('- [数据模型](#数据模型)');
}
buffer.writeln('- [错误代码](#错误代码)');
if (includeExamples) {
buffer.writeln('- [示例代码](#示例代码)');
}
buffer.writeln('- [更新日志](#更新日志)');
buffer.writeln('');
}
/// API概述
void _generateApiOverview(StringBuffer buffer) {
buffer.writeln('## 🚀 API概述');
buffer.writeln('');
//
final stats = _generateStats();
buffer.writeln('### 📊 统计信息');
buffer.writeln('');
buffer.writeln('- **总端点数**: ${stats['totalEndpoints']}');
buffer.writeln('- **控制器数**: ${stats['controllersCount']}');
buffer.writeln('- **数据模型数**: ${stats['modelsCount']}');
buffer.writeln('');
// HTTP方法统计
final methodStats = stats['methodStats'] as Map<String, int>;
buffer.writeln('### 🔗 HTTP方法分布');
buffer.writeln('');
for (final entry in methodStats.entries) {
final method = entry.key;
final count = entry.value;
final percentage =
((count / stats['totalEndpoints']) * 100).toStringAsFixed(1);
buffer.writeln('- **$method**: $count个 ($percentage%)');
}
buffer.writeln('');
//
buffer.writeln('### 🌐 服务器配置');
buffer.writeln('');
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('');
}
} else {
buffer.writeln('**服务器**: 相对路径 `/`');
buffer.writeln('');
}
}
///
void _generateAuthenticationInfo(StringBuffer buffer) {
buffer.writeln('## 🔐 认证');
buffer.writeln('');
buffer.writeln('本API使用以下认证方式');
buffer.writeln('');
buffer.writeln('### Bearer Token');
buffer.writeln('');
buffer.writeln('在请求头中包含Authorization字段');
buffer.writeln('');
buffer.writeln('```');
buffer.writeln('Authorization: Bearer YOUR_TOKEN_HERE');
buffer.writeln('```');
buffer.writeln('');
buffer.writeln('### 获取Token');
buffer.writeln('');
buffer.writeln('请使用登录接口获取访问令牌。');
buffer.writeln('');
}
///
void _generateEndpointsDocumentation(StringBuffer buffer) {
buffer.writeln('## 📡 API端点');
buffer.writeln('');
final controllerGroups = _groupPathsByController();
for (final entry in controllerGroups.entries) {
final controllerName = entry.key;
final paths = entry.value;
buffer.writeln('### $controllerName');
buffer.writeln('');
// HTTP方法和路径排序
paths.sort((a, b) {
final methodOrder = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
final aIndex = methodOrder.indexOf(a.method.value);
final bIndex = methodOrder.indexOf(b.method.value);
if (aIndex != bIndex) {
return aIndex.compareTo(bIndex);
}
return a.path.compareTo(b.path);
});
for (final path in paths) {
_generateEndpointDocumentation(buffer, path);
}
buffer.writeln('');
}
}
///
void _generateEndpointDocumentation(StringBuffer buffer, ApiPath path) {
//
final title = path.summary.isNotEmpty ? path.summary : path.operationId;
buffer.writeln('#### ${path.method.value} ${path.path}');
buffer.writeln('');
if (title.isNotEmpty) {
buffer.writeln('**$title**');
buffer.writeln('');
}
//
if (path.description.isNotEmpty) {
buffer.writeln(path.description);
buffer.writeln('');
}
//
if (path.tags.isNotEmpty) {
buffer.writeln('**标签**: ${path.tags.join(', ')}');
buffer.writeln('');
}
//
if (path.parameters.isNotEmpty) {
buffer.writeln('**参数**:');
buffer.writeln('');
//
final paramGroups = <ParameterLocation, List<ApiParameter>>{};
for (final param in path.parameters) {
paramGroups.putIfAbsent(param.location, () => []).add(param);
}
for (final entry in paramGroups.entries) {
final location = entry.key;
final params = entry.value;
buffer.writeln('*${_getLocationName(location)}参数*:');
buffer.writeln('');
buffer.writeln('| 参数名 | 类型 | 必填 | 描述 | 示例 |');
buffer.writeln('|--------|------|------|------|------|');
for (final param in params) {
final required = param.required ? '' : '';
final example = param.example?.toString() ?? '-';
final description =
param.description.isNotEmpty ? param.description : '-';
buffer.writeln(
'| ${param.name} | ${param.type.value} | $required | $description | $example |');
}
buffer.writeln('');
}
}
//
if (includeResponses && path.responses.isNotEmpty) {
buffer.writeln('**响应**:');
buffer.writeln('');
for (final entry in path.responses.entries) {
final code = entry.key;
final response = entry.value;
buffer.writeln('*HTTP $code*:');
if (response.description.isNotEmpty) {
buffer.writeln('- ${response.description}');
}
buffer.writeln('');
}
}
//
if (includeExamples) {
_generateEndpointExample(buffer, path);
}
buffer.writeln('---');
buffer.writeln('');
}
///
void _generateEndpointExample(StringBuffer buffer, ApiPath path) {
buffer.writeln('**示例**:');
buffer.writeln('');
// cURL示例
buffer.writeln('```bash');
buffer.write('curl -X ${path.method.value} ');
buffer.write('${_getBaseUrl()}${path.path}');
if (path.parameters.any((p) => p.location == ParameterLocation.header)) {
buffer.write(' \\');
buffer.writeln('');
buffer.write(' -H "Authorization: Bearer YOUR_TOKEN"');
}
if (path.method == HttpMethod.post || path.method == HttpMethod.put) {
buffer.write(' \\');
buffer.writeln('');
buffer.write(' -H "Content-Type: application/json"');
buffer.write(' \\');
buffer.writeln('');
buffer.write(' -d \'{"key": "value"}\'');
}
buffer.writeln('');
buffer.writeln('```');
buffer.writeln('');
// Dart示例
buffer.writeln('```dart');
buffer.writeln('import \'dart:convert\';');
buffer.writeln('import \'package:http/http.dart\' as http;');
buffer.writeln('');
buffer.writeln('class ApiClient {');
buffer.writeln(' static const String baseUrl = \'${_getBaseUrl()}\';');
buffer.writeln(' String? _token;');
buffer.writeln('');
buffer.writeln(' void setToken(String token) {');
buffer.writeln(' _token = token;');
buffer.writeln(' }');
buffer.writeln('');
buffer.writeln(' Map<String, String> get _headers => {');
buffer.writeln(' \'Content-Type\': \'application/json\',');
buffer.writeln(
' if (_token != null) \'Authorization\': \'Bearer \$_token\',');
buffer.writeln(' };');
buffer.writeln('');
buffer
.writeln(' Future<Map<String, dynamic>> get(String endpoint) async {');
buffer.writeln(' final response = await http.get(');
buffer.writeln(' Uri.parse(\'\$baseUrl\$endpoint\'),');
buffer.writeln(' headers: _headers,');
buffer.writeln(' );');
buffer.writeln('');
buffer.writeln(' if (response.statusCode == 200) {');
buffer.writeln(' return jsonDecode(response.body);');
buffer.writeln(' } else {');
buffer.writeln(
' throw Exception(\'Failed to load data: \${response.statusCode}\');');
buffer.writeln(' }');
buffer.writeln(' }');
buffer.writeln('');
buffer.writeln(
' Future<Map<String, dynamic>> post(String endpoint, Map<String, dynamic> data) async {');
buffer.writeln(' final response = await http.post(');
buffer.writeln(' Uri.parse(\'\$baseUrl\$endpoint\'),');
buffer.writeln(' headers: _headers,');
buffer.writeln(' body: jsonEncode(data),');
buffer.writeln(' );');
buffer.writeln('');
buffer.writeln(
' if (response.statusCode == 200 || response.statusCode == 201) {');
buffer.writeln(' return jsonDecode(response.body);');
buffer.writeln(' } else {');
buffer.writeln(
' throw Exception(\'Failed to post data: \${response.statusCode}\');');
buffer.writeln(' }');
buffer.writeln(' }');
buffer.writeln('}');
buffer.writeln('```');
buffer.writeln('');
}
///
void _generateSchemasDocumentation(StringBuffer buffer) {
if (document.models.isEmpty) return;
buffer.writeln('## 📋 数据模型');
buffer.writeln('');
final sortedModels = document.models.values.toList()
..sort((a, b) => a.name.compareTo(b.name));
for (final model in sortedModels) {
_generateModelDocumentation(buffer, model);
}
}
///
void _generateModelDocumentation(StringBuffer buffer, ApiModel model) {
buffer.writeln('### ${model.name}');
buffer.writeln('');
if (model.description.isNotEmpty) {
buffer.writeln(model.description);
buffer.writeln('');
}
if (model.isEnum) {
buffer.writeln('**枚举值**:');
buffer.writeln('');
for (final value in model.enumValues) {
buffer.writeln('- `$value`');
}
buffer.writeln('');
} else {
buffer.writeln('**属性**:');
buffer.writeln('');
if (model.properties.isNotEmpty) {
buffer.writeln('| 属性名 | 类型 | 必填 | 描述 |');
buffer.writeln('|--------|------|------|------|');
for (final entry in model.properties.entries) {
final propName = entry.key;
final prop = entry.value;
final required = model.required.contains(propName) ? '' : '';
final type = _getPropertyTypeDescription(prop);
final description =
prop.description.isNotEmpty ? prop.description : '-';
buffer.writeln('| $propName | $type | $required | $description |');
}
}
buffer.writeln('');
}
// JSON示例
if (includeExamples) {
buffer.writeln('**JSON示例**:');
buffer.writeln('');
buffer.writeln('```json');
buffer.writeln(_generateModelExample(model));
buffer.writeln('```');
buffer.writeln('');
}
buffer.writeln('---');
buffer.writeln('');
}
///
void _generateErrorCodesDocumentation(StringBuffer buffer) {
buffer.writeln('## ❌ 错误代码');
buffer.writeln('');
// HTTP状态码
final errorCodes = {
'400': '请求参数错误',
'401': '未授权访问',
'403': '禁止访问',
'404': '资源不存在',
'405': '方法不允许',
'422': '参数验证失败',
'500': '服务器内部错误',
'502': '网关错误',
'503': '服务不可用',
};
buffer.writeln('| 状态码 | 描述 |');
buffer.writeln('|--------|------|');
for (final entry in errorCodes.entries) {
buffer.writeln('| ${entry.key} | ${entry.value} |');
}
buffer.writeln('');
//
buffer.writeln('### 错误响应格式');
buffer.writeln('');
buffer.writeln('```json');
buffer.writeln('{');
buffer.writeln(' "error": {');
buffer.writeln(' "code": "ERROR_CODE",');
buffer.writeln(' "message": "错误描述",');
buffer.writeln(' "details": "详细信息"');
buffer.writeln(' }');
buffer.writeln('}');
buffer.writeln('```');
buffer.writeln('');
}
///
void _generateExamplesDocumentation(StringBuffer buffer) {
buffer.writeln('## 💡 示例代码');
buffer.writeln('');
// Dart HTTP客户端示例
buffer.writeln('### Dart HTTP客户端');
buffer.writeln('');
buffer.writeln('```dart');
buffer.writeln('import \'dart:convert\';');
buffer.writeln('import \'package:http/http.dart\' as http;');
buffer.writeln('');
buffer.writeln('class ApiClient {');
buffer.writeln(' static const String baseUrl = \'${_getBaseUrl()}\';');
buffer.writeln(' String? _token;');
buffer.writeln('');
buffer.writeln(' void setToken(String token) {');
buffer.writeln(' _token = token;');
buffer.writeln(' }');
buffer.writeln('');
buffer.writeln(' Map<String, String> get _headers => {');
buffer.writeln(' \'Content-Type\': \'application/json\',');
buffer.writeln(
' if (_token != null) \'Authorization\': \'Bearer \$_token\',');
buffer.writeln(' };');
buffer.writeln('');
buffer
.writeln(' Future<Map<String, dynamic>> get(String endpoint) async {');
buffer.writeln(' final response = await http.get(');
buffer.writeln(' Uri.parse(\'\$baseUrl\$endpoint\'),');
buffer.writeln(' headers: _headers,');
buffer.writeln(' );');
buffer.writeln('');
buffer.writeln(' if (response.statusCode == 200) {');
buffer.writeln(' return jsonDecode(response.body);');
buffer.writeln(' } else {');
buffer.writeln(
' throw Exception(\'Failed to load data: \${response.statusCode}\');');
buffer.writeln(' }');
buffer.writeln(' }');
buffer.writeln('');
buffer.writeln(
' Future<Map<String, dynamic>> post(String endpoint, Map<String, dynamic> data) async {');
buffer.writeln(' final response = await http.post(');
buffer.writeln(' Uri.parse(\'\$baseUrl\$endpoint\'),');
buffer.writeln(' headers: _headers,');
buffer.writeln(' body: jsonEncode(data),');
buffer.writeln(' );');
buffer.writeln('');
buffer.writeln(
' if (response.statusCode == 200 || response.statusCode == 201) {');
buffer.writeln(' return jsonDecode(response.body);');
buffer.writeln(' } else {');
buffer.writeln(
' throw Exception(\'Failed to post data: \${response.statusCode}\');');
buffer.writeln(' }');
buffer.writeln(' }');
buffer.writeln('}');
buffer.writeln('```');
buffer.writeln('');
}
///
void _generateChangeLog(StringBuffer buffer) {
buffer.writeln('## 📝 更新日志');
buffer.writeln('');
buffer.writeln(
'### ${document.version} - ${DateTime.now().toIso8601String().split('T')[0]}');
buffer.writeln('');
buffer.writeln('- 🎉 初始版本发布');
buffer.writeln('- 📡 ${document.paths.length} 个API端点');
buffer.writeln('- 📋 ${document.models.length} 个数据模型');
buffer.writeln('- 🔧 完整的API文档');
buffer.writeln('');
buffer.writeln('---');
buffer.writeln('');
buffer.writeln('*文档由 Swagger CLI By Max 自动生成*');
buffer.writeln('');
}
///
Map<String, List<ApiPath>> _groupPathsByController() {
final groups = <String, List<ApiPath>>{};
for (final path in document.paths.values) {
final controllerName = StringUtils.extractControllerName(path);
groups.putIfAbsent(controllerName, () => []).add(path);
}
return groups;
}
// StringUtils.extractControllerName
/// URL ( OpenAPI 3.0 servers )
String _getBaseUrl() {
if (document.servers.isNotEmpty) {
return document.servers.first.url;
}
return '/'; //
}
///
String _getLocationName(ParameterLocation location) {
switch (location) {
case ParameterLocation.query:
return '查询';
case ParameterLocation.path:
return '路径';
case ParameterLocation.header:
return '请求头';
case ParameterLocation.body:
return '请求体';
case ParameterLocation.form:
return '表单';
case ParameterLocation.cookie:
return 'Cookie';
}
}
///
String _getPropertyTypeDescription(ApiProperty prop) {
String baseType = prop.type.value;
if (prop.format != null) {
baseType += ' (${prop.format})';
}
if (prop.nullable) {
baseType += '?';
}
return baseType;
}
///
String _generateModelExample(ApiModel model) {
if (model.isEnum) {
return '"${model.enumValues.first}"';
}
final buffer = StringBuffer();
buffer.writeln('{');
final properties = model.properties.entries.toList();
for (int i = 0; i < properties.length; i++) {
final entry = properties[i];
final propName = entry.key;
final prop = entry.value;
final exampleValue = _generatePropertyExample(prop);
buffer.write(' "$propName": $exampleValue');
if (i < properties.length - 1) {
buffer.write(',');
}
buffer.writeln();
}
buffer.write('}');
return buffer.toString();
}
///
String _generatePropertyExample(ApiProperty prop) {
switch (prop.type) {
case PropertyType.string:
return '"string"';
case PropertyType.integer:
return '0';
case PropertyType.number:
return '0.0';
case PropertyType.boolean:
return 'true';
case PropertyType.array:
return '[]';
case PropertyType.object:
return '{}';
case PropertyType.reference:
return '{}';
default:
return 'null';
}
}
///
Map<String, dynamic> _generateStats() {
final stats = <String, dynamic>{};
stats['totalEndpoints'] = document.paths.length;
stats['controllersCount'] = _groupPathsByController().length;
stats['modelsCount'] = document.models.length;
// HTTP方法统计
final methodStats = <String, int>{};
for (final path in document.paths.values) {
final method = path.method.value;
methodStats[method] = (methodStats[method] ?? 0) + 1;
}
stats['methodStats'] = methodStats;
return stats;
}
}

View File

@ -1,255 +0,0 @@
import '../core/models.dart';
import '../utils/string_utils.dart';
import 'base_generator.dart';
///
/// API端点常量代码
class EndpointCodeGenerator extends BaseGenerator {
final SwaggerDocument document;
final bool includeBaseUrl;
final String? customBaseUrl;
EndpointCodeGenerator(
this.document, {
this.includeBaseUrl = true,
this.customBaseUrl,
});
@override
String get generatorType => 'EndpointCodeGenerator';
@override
String generate() {
final buffer = StringBuffer();
//
buffer.writeln(generateFileHeader('API 端点常量定义'));
buffer.writeln('');
//
buffer.writeln('/// API路径常量定义');
buffer.writeln('/// 统一管理所有API端点路径便于维护和修改');
buffer.writeln('class ApiPaths {');
buffer.writeln(' ApiPaths._(); // 私有构造函数,防止实例化');
buffer.writeln('');
// URL常量 ( OpenAPI 3.0 servers )
if (includeBaseUrl) {
final baseUrl = customBaseUrl ??
(document.servers.isNotEmpty ? document.servers.first.url : '/');
buffer.writeln(' /// 基础URL');
buffer.writeln(' static const String baseUrl = \'$baseUrl\';');
buffer.writeln('');
}
//
final controllerGroups = _groupPathsByController();
for (final entry in controllerGroups.entries) {
final controllerName = entry.key;
final paths = entry.value;
buffer.writeln(' // ${controllerName}相关端点');
for (final path in paths) {
final constantName = _generateConstantName(path);
final cleanPath = StringUtils.cleanPath(path.path);
//
if (path.summary.isNotEmpty) {
buffer.writeln(' ${StringUtils.generateComment(path.summary)}');
}
if (path.description.isNotEmpty && path.description != path.summary) {
buffer.writeln(' ${StringUtils.generateComment(path.description)}');
}
buffer.writeln(' static const String $constantName = \'$cleanPath\';');
buffer.writeln('');
}
buffer.writeln('');
}
//
buffer.writeln(' /// 所有端点列表');
buffer.writeln(' static const List<String> allEndpoints = [');
for (final entry in controllerGroups.entries) {
final paths = entry.value;
for (final path in paths) {
final constantName = _generateConstantName(path);
buffer.writeln(' $constantName,');
}
}
buffer.writeln(' ];');
buffer.writeln('');
// HTTP方法常量
buffer.writeln(' /// HTTP方法常量');
buffer.writeln(' static const Map<String, String> httpMethods = {');
for (final entry in controllerGroups.entries) {
final paths = entry.value;
for (final path in paths) {
final constantName = _generateConstantName(path);
buffer.writeln(' \'$constantName\': \'${path.method.value}\',');
}
}
buffer.writeln(' };');
buffer.writeln('');
// URL构建方法
if (includeBaseUrl) {
buffer.writeln(' /// 构建完整URL');
buffer.writeln(
' static String buildUrl(String endpoint, {Map<String, dynamic>? params}) {');
buffer.writeln(' String url = baseUrl + endpoint;');
buffer.writeln(' ');
buffer.writeln(' if (params != null && params.isNotEmpty) {');
buffer.writeln(' final queryParams = <String>[];');
buffer.writeln(' params.forEach((key, value) {');
buffer.writeln(' if (value != null) {');
buffer.writeln(
' queryParams.add(\'\\\${Uri.encodeComponent(key)}=\\\${Uri.encodeComponent(value.toString())}\');');
buffer.writeln(' }');
buffer.writeln(' });');
buffer.writeln(' ');
buffer.writeln(' if (queryParams.isNotEmpty) {');
buffer.writeln(' url += \'?\' + queryParams.join(\'&\');');
buffer.writeln(' }');
buffer.writeln(' }');
buffer.writeln(' ');
buffer.writeln(' return url;');
buffer.writeln(' }');
buffer.writeln('');
//
buffer.writeln(' /// 替换路径参数');
buffer.writeln(
' static String replacePathParams(String endpoint, Map<String, dynamic> params) {');
buffer.writeln(' String result = endpoint;');
buffer.writeln(' params.forEach((key, value) {');
buffer.writeln(
' result = result.replaceAll(\'{\\\$key}\', value.toString());');
buffer.writeln(' });');
buffer.writeln(' return result;');
buffer.writeln(' }');
buffer.writeln('');
}
//
buffer.writeln(' /// 验证端点是否存在');
buffer.writeln(' static bool isValidEndpoint(String endpoint) {');
buffer.writeln(' return allEndpoints.contains(endpoint);');
buffer.writeln(' }');
buffer.writeln('');
// HTTP方法的方法
buffer.writeln(' /// 获取端点的HTTP方法');
buffer.writeln(' static String? getHttpMethod(String endpoint) {');
buffer.writeln(' return httpMethods[endpoint];');
buffer.writeln(' }');
buffer.writeln('');
buffer.writeln('}');
//
buffer.writeln('');
buffer.writeln('/// API端点枚举');
buffer.writeln('/// 提供类型安全的端点访问');
buffer.writeln('enum ApiEndpoint {');
for (final entry in controllerGroups.entries) {
final paths = entry.value;
for (final path in paths) {
final enumName = _generateEnumName(path);
final constantName = _generateConstantName(path);
if (path.summary.isNotEmpty) {
buffer.writeln(' ${StringUtils.generateComment(path.summary)}');
}
buffer.writeln(
' $enumName(ApiPaths.$constantName, \'${path.method.value}\'),');
}
}
buffer.writeln(';');
buffer.writeln('');
//
buffer.writeln(' const ApiEndpoint(this.path, this.method);');
buffer.writeln('');
buffer.writeln(' /// 端点路径');
buffer.writeln(' final String path;');
buffer.writeln('');
buffer.writeln(' /// HTTP方法');
buffer.writeln(' final String method;');
buffer.writeln('');
if (includeBaseUrl) {
buffer.writeln(' /// 获取完整URL');
buffer.writeln(' String get fullUrl => ApiPaths.baseUrl + path;');
buffer.writeln('');
}
buffer.writeln(' /// 根据路径查找端点');
buffer.writeln(' static ApiEndpoint? findByPath(String path) {');
buffer.writeln(' for (final endpoint in values) {');
buffer.writeln(' if (endpoint.path == path) {');
buffer.writeln(' return endpoint;');
buffer.writeln(' }');
buffer.writeln(' }');
buffer.writeln(' return null;');
buffer.writeln(' }');
buffer.writeln('');
buffer.writeln(' /// 根据HTTP方法过滤端点');
buffer
.writeln(' static List<ApiEndpoint> filterByMethod(String method) {');
buffer.writeln(
' return values.where((endpoint) => endpoint.method == method).toList();');
buffer.writeln(' }');
buffer.writeln('');
buffer.writeln('}');
return generateTypeCheckedCode(buffer.toString());
}
///
Map<String, List<ApiPath>> _groupPathsByController() {
final groups = <String, List<ApiPath>>{};
for (final path in document.paths.values) {
final controllerName = StringUtils.extractControllerName(path);
groups.putIfAbsent(controllerName, () => []).add(path);
}
return groups;
}
// StringUtils.extractControllerName
///
String _generateConstantName(ApiPath path) {
final baseName =
StringUtils.generateEndpointName(path.path, path.operationId);
final methodPrefix = path.method.value.toLowerCase();
return StringUtils.toCamelCase('${methodPrefix}_$baseName');
}
///
String _generateEnumName(ApiPath path) {
final baseName =
StringUtils.generateEndpointName(path.path, path.operationId);
final methodPrefix = path.method.value.toLowerCase();
return StringUtils.toCamelCase('${methodPrefix}_$baseName');
}
// StringUtils.cleanPath
}

View File

@ -1,618 +0,0 @@
import '../core/models.dart';
import '../core/config.dart';
import '../utils/string_utils.dart';
import 'base_generator.dart';
///
/// Dart模型类代码
class ModelCodeGenerator extends ModelGenerator {
ModelCodeGenerator(super.document, {super.useSimpleModels});
@override
String get generatorType => 'ModelCodeGenerator';
@override
String generate() {
final buffer = StringBuffer();
//
buffer.writeln(generateFileHeader('API 数据模型定义'));
buffer.writeln('');
if (!useSimpleModels) {
buffer.writeln(
'import \'package:json_annotation/json_annotation.dart\';',
);
buffer.writeln('');
}
//
final models = document.models.values.toList();
for (int i = 0; i < models.length; i++) {
final model = models[i];
buffer.writeln(generateModelCode(model));
//
if (i < models.length - 1) {
buffer.writeln('');
buffer.writeln('// ${'=' * 60}');
buffer.writeln('');
}
}
return generateTypeCheckedCode(buffer.toString());
}
@override
String generateModelCode(ApiModel model) {
if (model.isEnum) {
return generateEnumCode(model);
}
// 使 JsonSerializable
return generateAnnotatedModelCode(model);
}
///
String generateAnnotatedModelCode(ApiModel model) {
final className = StringUtils.generateClassName(model.name);
final buffer = StringBuffer();
//
final importedTypes = getImportedTypes(model);
for (final importType in importedTypes) {
final importFileName = StringUtils.generateFileName(importType);
buffer.writeln('import \'$importFileName\';');
}
if (importedTypes.isNotEmpty) {
buffer.writeln('');
}
// part
final partFileName = StringUtils.generateFileName(model.name);
final generatedPart = partFileName.replaceAll('.dart', '.g.dart');
buffer.writeln('part \'$generatedPart\';');
buffer.writeln('');
//
if (model.description.isNotEmpty) {
buffer.writeln(StringUtils.generateComment(model.description));
}
buffer.writeln('@JsonSerializable(checked: true, includeIfNull: false)');
buffer.writeln('class $className {');
//
model.properties.forEach((propName, property) {
final dartType = getDartPropertyType(property);
//
// 1. String date-time/date Swagger nullable
// 2. defaultValue json_annotation
// 3. nullable
final isNormalString = property.type == PropertyType.string &&
property.format != 'date-time' &&
property.format != 'date';
final hasDefaultValue = property.defaultValue != null || isNormalString;
final nullable = hasDefaultValue ? '' : (property.nullable ? '?' : '');
final dartPropName = StringUtils.toDartPropertyName(propName);
if (property.description.isNotEmpty) {
buffer.writeln(
' ${StringUtils.generateComment(property.description)}',
);
}
// JsonKey注解
final needsJsonKey =
_needsJsonKeyAnnotation(dartPropName, propName, property, model);
if (needsJsonKey.isNotEmpty) {
buffer.writeln(' @JsonKey($needsJsonKey)');
}
buffer.writeln(' final $dartType$nullable $dartPropName;');
buffer.writeln('');
});
//
if (model.properties.isEmpty) {
buffer.writeln(' const $className();');
} else {
buffer.writeln(' const $className({');
model.properties.forEach((propName, property) {
final dartPropName = StringUtils.toDartPropertyName(propName);
// required
// 1. String date-time/date required Swagger nullable
// 2. required
// 3. required
final isNormalString = property.type == PropertyType.string &&
property.format != 'date-time' &&
property.format != 'date';
final shouldBeRequired = isNormalString || !property.nullable;
final required = shouldBeRequired ? 'required ' : '';
buffer.writeln(' ${required}this.$dartPropName,');
});
buffer.writeln(' });');
}
buffer.writeln('');
// fromJson
buffer.writeln(
' factory $className.fromJson(Map<String, dynamic> json) =>',
);
buffer.writeln(' _\$${className}FromJson(json);');
buffer.writeln('');
// toJson
buffer.writeln(
' Map<String, dynamic> toJson() => _\$${className}ToJson(this);');
buffer.writeln('');
buffer.writeln('}');
return buffer.toString();
}
///
/// ///
String _getModelSubDirectory(ApiModel model) {
// enums
if (model.isEnum) {
return 'enums';
}
// usageType
switch (model.usageType) {
case ModelUsageType.request:
return 'request';
case ModelUsageType.response:
return 'result';
case ModelUsageType.common:
case ModelUsageType.unknown:
// common unknown result
//
return 'result';
}
}
///
Map<String, String> generateSeparateModelFiles() {
final files = <String, String>{};
//
final modelsByDirectory = <String, List<ApiModel>>{};
//
for (final model in document.models.values) {
// total items
if (_isPaginationResponseModel(model)) {
continue; // 使 BasePageResult<T>
}
final subDir = _getModelSubDirectory(model);
modelsByDirectory.putIfAbsent(subDir, () => []).add(model);
final fileName = StringUtils.generateFileName(model.name);
final filePath = '$subDir/$fileName';
final content = generateSingleModelFile(model, fileName: fileName);
files[filePath] = content;
}
// index.dart
final indexContent = _generateMainIndexFile(modelsByDirectory);
files['index.dart'] = indexContent;
// index.dart
modelsByDirectory.forEach((subDir, models) {
final subIndexContent = _generateSubDirectoryIndexFile(models);
files['$subDir/index.dart'] = subIndexContent;
});
return files;
}
///
String generateSingleModelFile(ApiModel model, {String? fileName}) {
final buffer = StringBuffer();
//
buffer.writeln(generateFileHeader(
'${model.name} 模型定义',
fileName: fileName ?? StringUtils.generateFileName(model.name),
));
buffer.writeln('');
// json_annotation 使 @JsonEnum
if (!useSimpleModels && model.isEnum) {
buffer.writeln(
'import \'package:json_annotation/json_annotation.dart\';',
);
buffer.writeln('');
}
// json_annotation
else if (!useSimpleModels && !model.isEnum) {
buffer.writeln(
'import \'package:json_annotation/json_annotation.dart\';',
);
buffer.writeln('');
}
// - 使 index.dart
// result/user_result.dart '../index.dart'
final importedTypes = getImportedTypes(model);
if (importedTypes.isNotEmpty) {
buffer.writeln('import \'../index.dart\';');
buffer.writeln('');
}
//
buffer.writeln(_generateModelCodeWithoutImports(model));
return generateTypeCheckedCode(buffer.toString());
}
///
String _generateModelCodeWithoutImports(ApiModel model) {
if (model.isEnum) {
return _generateEnumCodeWithoutImports(model);
}
// 使 JsonSerializable
return _generateAnnotatedModelCodeWithoutImports(model);
}
///
String _generateEnumCodeWithoutImports(ApiModel model) {
final className = StringUtils.generateClassName(model.name);
final enumType = model.enumType?.value ?? 'string';
final buffer = StringBuffer();
//
if (model.description.isNotEmpty) {
buffer.writeln(StringUtils.generateComment(model.description));
}
// @JsonEnum
buffer.writeln('@JsonEnum()');
buffer.writeln('enum $className {');
//
for (int i = 0; i < model.enumValues.length; i++) {
final value = model.enumValues[i];
final enumName = StringUtils.generateEnumValueName(value, i);
if (enumType == 'integer' || enumType == 'number') {
buffer.writeln(' $enumName($value),');
} else {
buffer.writeln(' $enumName(\'$value\'),');
}
}
//
final content = buffer.toString().trimRight();
buffer.clear();
buffer.writeln(content.substring(0, content.lastIndexOf(',')));
buffer.writeln(';');
buffer.writeln('');
//
buffer.writeln(' const $className(this.value);');
buffer.writeln(
' final ${enumType == 'integer' || enumType == 'number' ? 'int' : 'String'} value;',
);
buffer.writeln('');
// fromValue
buffer.writeln(' static $className fromValue(dynamic value) {');
buffer.writeln(' for (final enumValue in $className.values) {');
buffer.writeln(' if (enumValue.value == value) {');
buffer.writeln(' return enumValue;');
buffer.writeln(' }');
buffer.writeln(' }');
buffer.writeln(' throw ArgumentError(\'Unknown enum value: \$value\');');
buffer.writeln(' }');
buffer.writeln('');
// fromJson
buffer.writeln(' factory $className.fromJson(dynamic json) {');
buffer.writeln(' return fromValue(json);');
buffer.writeln(' }');
buffer.writeln('');
// toJson
buffer.writeln(' dynamic toJson() => value;');
buffer.writeln('');
buffer.writeln('}');
return buffer.toString();
}
// StringUtils.generateEnumValueName
///
String _generateAnnotatedModelCodeWithoutImports(ApiModel model) {
final className = StringUtils.generateClassName(model.name);
final buffer = StringBuffer();
// part
final partFileName = StringUtils.generateFileName(model.name);
final generatedPart = partFileName.replaceAll('.dart', '.g.dart');
buffer.writeln('part \'$generatedPart\';');
buffer.writeln('');
//
if (model.description.isNotEmpty) {
buffer.writeln(StringUtils.generateComment(model.description));
}
buffer.writeln('@JsonSerializable(checked: true, includeIfNull: false)');
buffer.writeln('class $className {');
//
model.properties.forEach((propName, property) {
final dartType = getDartPropertyType(property);
//
// 1. String date-time/date Swagger nullable
// 2. defaultValue json_annotation
// 3. nullable
final isNormalString = property.type == PropertyType.string &&
property.format != 'date-time' &&
property.format != 'date';
final hasDefaultValue = property.defaultValue != null || isNormalString;
final nullable = hasDefaultValue ? '' : (property.nullable ? '?' : '');
final dartPropName = StringUtils.toDartPropertyName(propName);
if (property.description.isNotEmpty) {
buffer.writeln(
' ${StringUtils.generateComment(property.description)}',
);
}
// JsonKey注解
final needsJsonKey =
_needsJsonKeyAnnotation(dartPropName, propName, property, model);
if (needsJsonKey.isNotEmpty) {
buffer.writeln(' @JsonKey($needsJsonKey)');
}
buffer.writeln(' final $dartType$nullable $dartPropName;');
buffer.writeln('');
});
//
if (model.properties.isEmpty) {
buffer.writeln(' const $className();');
} else {
buffer.writeln(' const $className({');
model.properties.forEach((propName, property) {
final dartPropName = StringUtils.toDartPropertyName(propName);
// required
// 1. String date-time/date required Swagger nullable
// 2. required
// 3. required
final isNormalString = property.type == PropertyType.string &&
property.format != 'date-time' &&
property.format != 'date';
final shouldBeRequired = isNormalString || !property.nullable;
final required = shouldBeRequired ? 'required ' : '';
buffer.writeln(' ${required}this.$dartPropName,');
});
buffer.writeln(' });');
}
buffer.writeln('');
// fromJson
buffer.writeln(
' factory $className.fromJson(Map<String, dynamic> json) =>',
);
buffer.writeln(' _\$${className}FromJson(json);');
buffer.writeln('');
// toJson
buffer.writeln(
' Map<String, dynamic> toJson() => _\$${className}ToJson(this);');
buffer.writeln('');
buffer.writeln('}');
return buffer.toString();
}
///
/// index.dart
String _generateMainIndexFile(Map<String, List<ApiModel>> modelsByDirectory) {
final buffer = StringBuffer();
buffer.writeln(generateFileHeader('API 模型导出文件'));
buffer.writeln('');
// library
buffer.writeln('library;');
buffer.writeln('');
// base_result base_page_result
final baseResultImport = SwaggerConfig.baseResultImport;
final basePageResultImport = SwaggerConfig.basePageResultImport;
if (baseResultImport.isNotEmpty) {
buffer.writeln('export \'$baseResultImport\';');
}
if (basePageResultImport.isNotEmpty) {
buffer.writeln('export \'$basePageResultImport\';');
}
if ((baseResultImport.isNotEmpty || basePageResultImport.isNotEmpty) &&
modelsByDirectory.isNotEmpty) {
buffer.writeln('');
}
// index.dart
final sortedDirs = modelsByDirectory.keys.toList()..sort();
for (final dir in sortedDirs) {
buffer.writeln('export \'$dir/index.dart\';');
}
return generateTypeCheckedCode(buffer.toString());
}
/// index.dart
String _generateSubDirectoryIndexFile(List<ApiModel> models) {
final buffer = StringBuffer();
buffer.writeln(generateFileHeader('模型导出文件'));
buffer.writeln('');
// library
buffer.writeln('library;');
buffer.writeln('');
//
final sortedModels = List<ApiModel>.from(models)
..sort((a, b) => a.name.compareTo(b.name));
for (final model in sortedModels) {
final fileName = StringUtils.generateFileName(model.name);
buffer.writeln('export \'$fileName\';');
}
return generateTypeCheckedCode(buffer.toString());
}
String generateIndexFile(List<String> modelFileNames) {
final buffer = StringBuffer();
buffer.writeln(generateFileHeader('API 模型导出文件'));
buffer.writeln('');
// library
buffer.writeln('library;');
buffer.writeln('');
// base_result base_page_result
final baseResultImport = SwaggerConfig.baseResultImport;
final basePageResultImport = SwaggerConfig.basePageResultImport;
if (baseResultImport.isNotEmpty) {
buffer.writeln('export \'$baseResultImport\';');
}
if (basePageResultImport.isNotEmpty) {
buffer.writeln('export \'$basePageResultImport\';');
}
if ((baseResultImport.isNotEmpty || basePageResultImport.isNotEmpty) &&
modelFileNames.isNotEmpty) {
buffer.writeln('');
}
//
final sortedFiles = List<String>.from(modelFileNames)..sort();
for (final fileName in sortedFiles) {
buffer.writeln('export \'$fileName\';');
}
return generateTypeCheckedCode(buffer.toString());
}
/// JsonKey注解以及注解的内容
String _needsJsonKeyAnnotation(
String dartPropName,
String propName,
ApiProperty property,
ApiModel model,
) {
final annotations = <String>[];
// JSON字段名不同时需要name参数
if (dartPropName != propName) {
annotations.add('name: \'$propName\'');
}
// 使 usageType
// defaultValue
final isRequestModel = model.usageType == ModelUsageType.request;
// String类型默认值处理
//
if (!isRequestModel &&
property.type == PropertyType.string &&
property.format != 'date-time' &&
property.format != 'date') {
// String类型添加默认值为空字符串
if (property.defaultValue != null) {
// OpenAPI文档中有明确的默认值使
final defaultVal = property.defaultValue.toString();
annotations.add('defaultValue: \'$defaultVal\'');
} else {
// 使
annotations.add('defaultValue: \'\'');
}
}
// List类型默认值处理
// List添加默认值
//
if (!isRequestModel &&
property.type == PropertyType.array &&
!property.nullable) {
annotations.add('defaultValue: []');
}
// DateTime类型需要特殊处理
if (property.type == PropertyType.string &&
(property.format == 'date-time' || property.format == 'date')) {
// DateTime类型json_annotation会自动处理
// annotations.add('fromJson: DateTime.parse, toJson: _dateTimeToString');
}
//
if (property.type != PropertyType.string && property.defaultValue != null) {
final defaultVal = property.defaultValue;
if (property.type == PropertyType.integer ||
property.type == PropertyType.number) {
annotations.add('defaultValue: $defaultVal');
} else if (property.type == PropertyType.boolean) {
annotations.add('defaultValue: $defaultVal');
} else {
//
annotations.add('defaultValue: \'$defaultVal\'');
}
}
//
if (property.type == PropertyType.reference) {
//
//
}
//
// if (shouldIgnore) {
// annotations.add('ignore: true');
// }
return annotations.join(', ');
}
/// total items
bool _isPaginationResponseModel(ApiModel model) {
// total items
if (!model.properties.containsKey('total') ||
!model.properties.containsKey('items')) {
return false;
}
final totalProp = model.properties['total']!;
final itemsProp = model.properties['items']!;
// total
final isTotalNumeric = totalProp.type == PropertyType.integer ||
totalProp.type == PropertyType.number;
// items
final isItemsArray = itemsProp.type == PropertyType.array;
return isTotalNumeric && isItemsArray;
}
}

View File

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

View File

@ -1,591 +0,0 @@
///
///
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

29
lib/index.dart Normal file
View File

@ -0,0 +1,29 @@
/// # Swagger Generator Flutter
///
/// A powerful Flutter OpenAPI 3.0 code generator, optimized for the Dio + Retrofit stack.
///
/// ## Core Components
///
/// - **Config**: `ConfigRepository` for loading and accessing configuration.
/// - **Parse**: `SwaggerDataParser` to fetch and parse OpenAPI documents.
/// - **Validate**: `EnhancedValidator` to validate documents against OpenAPI specs and best practices.
/// - **Generate**: `ModelCodeGenerator` and `RetrofitApiGenerator` for creating Dart code.
/// - **Render**: `TemplateRenderer` for Mustache-based template rendering.
/// - **Output**: `GenerationOutputService` to write generated files to disk.
library;
// --- Core Public API ---
export 'core/config_repository.dart';
export 'core/error_reporter.dart';
export 'core/models.dart';
// --- Pipeline API ---
export 'pipeline/generate/apis.dart';
export 'pipeline/generate/models.dart';
export 'pipeline/output/generation_output_service.dart';
export 'pipeline/parse/swagger_data_parser.dart';
export 'pipeline/render/template_renderer.dart';
export 'pipeline/validate/enhanced_validator.dart';
// --- Utilities ---
export 'utils/logger.dart';
export 'utils/path_resolver.dart';

View File

@ -0,0 +1,5 @@
/// Pipeline: generate -> apis
/// Re-export Retrofit API generator for pipeline-oriented imports.
library;
export 'impl/retrofit_api_generator.dart';

View File

@ -1,7 +1,7 @@
import '../core/config.dart';
import '../core/exceptions.dart';
import '../core/models.dart';
import '../utils/string_utils.dart';
import 'package:swagger_generator_flutter/core/config.dart';
import 'package:swagger_generator_flutter/core/exceptions.dart';
import 'package:swagger_generator_flutter/core/models.dart';
import 'package:swagger_generator_flutter/utils/string_helper.dart';
///
///
@ -16,14 +16,17 @@ abstract class BaseGenerator {
/// [description]
/// [fileName]
String generateFileHeader(String description, {String? fileName}) {
return StringUtils.generateFileHeader(
final header = StringHelper.generateFileHeader(
description,
SwaggerConfig.swaggerJsonUrls.isNotEmpty
? SwaggerConfig.swaggerJsonUrls.first
SwaggerConfig.swaggerJsonUrls.isNotEmpty
? SwaggerConfig.swaggerJsonUrls.first
: '',
fileName: fileName,
fileType: description,
);
// lint
return '$header\n// ignore_for_file: type=lint, invalid_annotation_target\n';
}
///
@ -37,6 +40,11 @@ abstract class BaseGenerator {
);
}
//
if (!code.endsWith('\n')) {
return '$code\n';
}
return code;
} catch (e) {
throw CodeGenerationException(
@ -81,11 +89,10 @@ abstract class BaseGenerator {
///
abstract class ModelGenerator extends BaseGenerator {
ModelGenerator(this.document, {this.useSimpleModels = false});
final SwaggerDocument document;
final bool useSimpleModels;
ModelGenerator(this.document, {this.useSimpleModels = false});
@override
String get generatorType => 'ModelGenerator';
@ -98,69 +105,64 @@ abstract class ModelGenerator extends BaseGenerator {
throw CodeGenerationException('模型不是枚举类型', generatorType: generatorType);
}
final className = StringUtils.generateClassName(model.name);
final className = StringHelper.generateClassName(model.name);
final enumType = model.enumType?.value ?? 'string';
final buffer = StringBuffer();
//
buffer.writeln(generateFileHeader('${model.name} 枚举定义'));
buffer.writeln('');
final valueType =
enumType == 'integer' || enumType == 'number' ? 'int' : 'String';
final buffer = StringBuffer()
//
..writeln(generateFileHeader('${model.name} 枚举定义'))
..writeln();
//
if (model.description.isNotEmpty) {
buffer.writeln(StringUtils.generateComment(model.description));
buffer.writeln(StringHelper.generateComment(model.description));
}
buffer.writeln('enum $className {');
//
for (int i = 0; i < model.enumValues.length; i++) {
for (var i = 0; i < model.enumValues.length; i++) {
final value = model.enumValues[i];
final enumName = StringUtils.generateEnumValueName(value, i);
final enumName = StringHelper.generateEnumValueName(value, i);
final enumLine = enumType == 'integer' || enumType == 'number'
? ' $enumName($value),'
: " $enumName('$value'),";
if (enumType == 'integer' || enumType == 'number') {
buffer.writeln(' $enumName($value),');
} else {
buffer.writeln(' $enumName(\'$value\'),');
}
buffer.writeln(enumLine);
}
//
final content = buffer.toString().trimRight();
buffer.clear();
buffer.writeln(content.substring(0, content.lastIndexOf(',')));
buffer.writeln(';');
buffer.writeln('');
//
buffer.writeln(' const $className(this.value);');
buffer.writeln(
' final ${enumType == 'integer' || enumType == 'number' ? 'int' : 'String'} value;',
);
buffer.writeln('');
// fromValue
buffer.writeln(' static $className fromValue(dynamic value) {');
buffer.writeln(' for (final enumValue in $className.values) {');
buffer.writeln(' if (enumValue.value == value) {');
buffer.writeln(' return enumValue;');
buffer.writeln(' }');
buffer.writeln(' }');
buffer.writeln(' throw ArgumentError(\'Unknown enum value: \$value\');');
buffer.writeln(' }');
buffer.writeln('');
// fromJson
buffer.writeln(' factory $className.fromJson(dynamic json) {');
buffer.writeln(' return fromValue(json);');
buffer.writeln(' }');
buffer.writeln('');
// toJson
buffer.writeln(' dynamic toJson() => value;');
buffer.writeln('');
buffer.writeln('}');
buffer
..clear()
..writeAll(
[
content.substring(0, content.lastIndexOf(',')),
';',
'',
' const $className(this.value);',
' final $valueType value;',
'',
' static $className fromValue(dynamic value) {',
' for (final enumValue in $className.values) {',
' if (enumValue.value == value) {',
' return enumValue;',
' }',
' }',
r" throw ArgumentError('Unknown enum value: $value');",
' }',
'',
' factory $className.fromJson(dynamic json) {',
' return fromValue(json);',
' }',
'',
' dynamic toJson() => value;',
'',
'}',
],
'\n',
);
return generateTypeCheckedCode(buffer.toString());
}
@ -211,6 +213,8 @@ abstract class ModelGenerator extends BaseGenerator {
return 'double';
case PropertyType.boolean:
return 'bool';
case PropertyType.enumType:
return 'String';
case PropertyType.array:
//
if (property.items != null) {
@ -222,9 +226,17 @@ abstract class ModelGenerator extends BaseGenerator {
return 'Map<String, dynamic>';
case PropertyType.reference:
return property.reference != null
? StringUtils.generateClassName(property.reference!)
? StringHelper.generateClassName(property.reference!)
: 'dynamic';
default:
case PropertyType.file:
return 'dynamic';
case PropertyType.bytes:
return 'List<int>';
case PropertyType.date:
return 'DateTime';
case PropertyType.dateTime:
return 'DateTime';
case PropertyType.unknown:
return 'dynamic';
}
}
@ -236,7 +248,7 @@ abstract class ModelGenerator extends BaseGenerator {
items.name != 'integer' &&
items.name != 'number' &&
items.name != 'boolean') {
return StringUtils.generateClassName(items.name);
return StringHelper.generateClassName(items.name);
}
// Dart类型
@ -257,22 +269,11 @@ abstract class ModelGenerator extends BaseGenerator {
///
class GeneratorOptions {
final bool generateEndpoints;
final bool generateModels;
final bool generateDocs;
final bool useSimpleModels;
final bool separateModelFiles;
final String modelsDirectory;
final String outputDirectory;
final String endpointsFileName;
final String docsFileName;
const GeneratorOptions({
this.generateEndpoints = true,
this.generateModels = true,
this.generateDocs = true,
this.useSimpleModels = false,
this.separateModelFiles = true,
this.modelsDirectory = 'models',
this.outputDirectory = 'generator',
this.endpointsFileName = 'api_paths.dart',
@ -281,67 +282,57 @@ class GeneratorOptions {
///
factory GeneratorOptions.fromArgs(List<String> args) {
bool generateEndpoints = false;
bool generateModels = false;
bool generateDocs = false;
bool useSimpleModels = false;
const bool separateModelFiles = true;
String modelsDirectory = 'models';
String outputDirectory = 'generator';
String endpointsFileName = 'api_paths.dart';
String docsFileName = 'api_documentation.md';
var generateEndpoints = false;
var generateModels = false;
var generateDocs = false;
var useSimpleModels = false;
var modelsDirectory = 'models';
var outputDirectory = 'generator';
var endpointsFileName = 'api_paths.dart';
var docsFileName = 'api_documentation.md';
bool hasSpecificOption = false;
var hasSpecificOption = false;
for (int i = 0; i < args.length; i++) {
for (var i = 0; i < args.length; i++) {
final arg = args[i];
switch (arg) {
case '--endpoints':
generateEndpoints = true;
hasSpecificOption = true;
break;
case '--models':
generateModels = true;
hasSpecificOption = true;
break;
case '--docs':
generateDocs = true;
hasSpecificOption = true;
break;
case '--all':
generateEndpoints = true;
generateModels = true;
generateDocs = true;
hasSpecificOption = true;
break;
case '--simple':
useSimpleModels = true;
break;
case '--models-dir':
if (i + 1 < args.length) {
modelsDirectory = args[i + 1];
i++; //
}
break;
case '--output-dir':
if (i + 1 < args.length) {
outputDirectory = args[i + 1];
i++; //
}
break;
case '--endpoints-file':
if (i + 1 < args.length) {
endpointsFileName = args[i + 1];
i++; //
}
break;
case '--docs-file':
if (i + 1 < args.length) {
docsFileName = args[i + 1];
i++; //
}
break;
}
}
@ -357,11 +348,18 @@ class GeneratorOptions {
generateModels: generateModels,
generateDocs: generateDocs,
useSimpleModels: useSimpleModels,
separateModelFiles: separateModelFiles,
modelsDirectory: modelsDirectory,
outputDirectory: outputDirectory,
endpointsFileName: endpointsFileName,
docsFileName: docsFileName,
);
}
final bool generateEndpoints;
final bool generateModels;
final bool generateDocs;
final bool useSimpleModels;
final String modelsDirectory;
final String outputDirectory;
final String endpointsFileName;
final String docsFileName;
}

View File

@ -0,0 +1,277 @@
part of '../model_code_generator.dart';
String _generateModelCodeWithoutImports(
ModelCodeGenerator generator,
ApiModel model,
) {
if (model.isEnum) {
return _generateEnumCodeWithoutImports(model);
}
return _generateAnnotatedModelCodeWithoutImports(generator, model);
}
String _generateEnumCodeWithoutImports(ApiModel model) {
final className = StringHelper.generateClassName(model.name);
final enumType = model.enumType?.value ?? 'string';
final valueType =
enumType == 'integer' || enumType == 'number' ? 'int' : 'String';
final buffer = StringBuffer();
if (model.description.isNotEmpty) {
buffer.writeln(StringHelper.generateComment(model.description));
}
buffer
..writeln('@JsonEnum()')
..writeln('enum $className {');
//
final enumMappings = SwaggerConfig.enumKeyMappings?[model.name];
for (var i = 0; i < model.enumValues.length; i++) {
final value = model.enumValues[i];
String enumName;
String? description;
// 1:
if (enumMappings != null && enumMappings.containsKey(value)) {
final mapping = enumMappings[value]!;
enumName = mapping.name;
description = mapping.description;
}
// 2: x-enum-varnames
else if (model.enumVarNames != null && i < model.enumVarNames!.length) {
enumName = model.enumVarNames![i];
// 使 x-enum-descriptions
if (model.enumDescriptions != null && i < model.enumDescriptions!.length) {
description = model.enumDescriptions![i];
}
}
// 3:
else {
enumName = StringHelper.generateEnumValueName(value, i);
}
//
if (description != null && description.isNotEmpty) {
buffer.writeln(' /// $description');
}
final enumLine = enumType == 'integer' || enumType == 'number'
? ' $enumName($value),'
: " $enumName('$value'),";
buffer.writeln(enumLine);
}
// UNKNOWN
buffer.writeln();
buffer.writeln(' /// 未知值');
final unknownLine = enumType == 'integer' || enumType == 'number'
? ' UNKNOWN(-9999),'
: " UNKNOWN('UNKNOWN'),";
buffer.writeln(unknownLine);
final content = buffer.toString().trimRight();
buffer
..clear()
..writeAll(
[
content.substring(0, content.lastIndexOf(',')),
';',
'',
' const $className(this.value);',
' final $valueType value;',
'',
' static $className fromValue(dynamic value) {',
' for (final enumValue in $className.values) {',
' if (enumValue.value == value) {',
' return enumValue;',
' }',
' }',
' return $className.UNKNOWN;',
' }',
'',
' factory $className.fromJson(dynamic json) {',
' return fromValue(json);',
' }',
'',
' dynamic toJson() => value;',
'',
'}',
],
'\n',
);
return buffer.toString();
}
String _generateAnnotatedModelCodeWithoutImports(
ModelCodeGenerator generator,
ApiModel model,
) {
final className = StringHelper.generateClassName(model.name);
final buffer = StringBuffer();
final partFileName = StringHelper.generateFileName(model.name);
final freezedPart = partFileName.replaceAll('.dart', '.freezed.dart');
final generatedPart = partFileName.replaceAll('.dart', '.g.dart');
buffer
..writeln("part '$freezedPart';")
..writeln("part '$generatedPart';")
..writeln();
if (model.description.isNotEmpty) {
buffer.writeln(StringHelper.generateComment(model.description));
}
buffer
..writeln('@freezed')
..writeln('abstract class $className with _\$$className {')
..writeln(' const factory $className({');
model.properties.forEach((propName, property) {
final dartType = generator.getDartPropertyType(property);
final isNormalString = property.type == PropertyType.string &&
property.format != 'date-time' &&
property.format != 'date';
final hasDefaultValue = property.defaultValue != null || isNormalString;
final nullable = hasDefaultValue ? '' : (property.nullable ? '?' : '');
final dartPropName = StringHelper.toDartPropertyName(propName);
if (property.description.isNotEmpty) {
buffer.writeln(
' ${StringHelper.generateComment(property.description)}',
);
}
final jsonKeyAnnotations =
_needsJsonKeyAnnotation(dartPropName, propName, property, model);
if (jsonKeyAnnotations.isNotEmpty) {
buffer.writeln(' @JsonKey($jsonKeyAnnotations)');
}
final shouldBeRequired = isNormalString || !property.nullable;
final required = shouldBeRequired ? 'required ' : '';
buffer.writeln(' $required$dartType$nullable $dartPropName,');
});
buffer
..writeln(' }) = _$className;')
..writeln()
..writeln(
' factory $className.fromJson(Map<String, dynamic> json) =>',
)
..writeln(' _\$${className}FromJson(json);')
..writeln('}');
return buffer.toString();
}
String _generateMainIndexFile(
ModelCodeGenerator generator,
Map<String, List<ApiModel>> modelsByDirectory,
) {
final buffer = StringBuffer()
..writeln(generator.generateFileHeader('API 模型导出文件'))
..writeln()
..writeln('library;')
..writeln();
final baseResultImport = SwaggerConfig.baseResultImport;
final basePageResultImport = SwaggerConfig.basePageResultImport;
if (baseResultImport.isNotEmpty) {
buffer.writeln("export '$baseResultImport';");
}
if (basePageResultImport.isNotEmpty) {
buffer.writeln("export '$basePageResultImport';");
}
if ((baseResultImport.isNotEmpty || basePageResultImport.isNotEmpty) &&
modelsByDirectory.isNotEmpty) {
buffer.writeln();
}
final sortedDirs = modelsByDirectory.keys.toList()..sort();
for (final dir in sortedDirs) {
buffer.writeln("export '$dir/index.dart';");
}
return generator.generateTypeCheckedCode(buffer.toString());
}
String _generateSubDirectoryIndexFile(
ModelCodeGenerator generator,
List<ApiModel> models,
) {
final buffer = StringBuffer()
..writeln(generator.generateFileHeader('模型导出文件'))
..writeln()
..writeln('library;')
..writeln();
final sortedModels = List<ApiModel>.from(models)
..sort((a, b) => a.name.compareTo(b.name));
for (final model in sortedModels) {
final fileName = StringHelper.generateFileName(model.name);
buffer.writeln("export '$fileName';");
}
return generator.generateTypeCheckedCode(buffer.toString());
}
String _needsJsonKeyAnnotation(
String dartPropName,
String propName,
ApiProperty property,
ApiModel model,
) {
final annotations = <String>[];
if (dartPropName != propName) {
annotations.add("name: '$propName'");
}
final isRequestModel = model.usageType == ModelUsageType.request;
if (!isRequestModel &&
property.type == PropertyType.string &&
property.format != 'date-time' &&
property.format != 'date') {
if (property.defaultValue != null) {
final defaultVal = property.defaultValue.toString();
annotations.add("defaultValue: '$defaultVal'");
} else {
annotations.add("defaultValue: ''");
}
}
if (!isRequestModel &&
property.type == PropertyType.array &&
!property.nullable) {
annotations.add('defaultValue: []');
}
if (property.type == PropertyType.string &&
(property.format == 'date-time' || property.format == 'date')) {
//
}
if (property.type != PropertyType.string && property.defaultValue != null) {
final defaultVal = property.defaultValue;
if (property.type == PropertyType.integer ||
property.type == PropertyType.number) {
annotations.add('defaultValue: $defaultVal');
} else if (property.type == PropertyType.boolean) {
annotations.add('defaultValue: $defaultVal');
} else {
annotations.add("defaultValue: '$defaultVal'");
}
}
return annotations.join(', ');
}

Some files were not shown because too many files have changed in this diff Show More