Compare commits

..

27 Commits

Author SHA1 Message Date
8a10cdaccb Merge github.com:gogf/gf into personal/hailaz 2026-05-18 20:36:20 +00:00
45e8f21053 Merge github.com:gogf/gf into personal/hailaz 2026-01-19 14:13:36 +08:00
412f1636d4 Apply gci import order changes 2025-09-30 08:23:58 +00:00
a36d0c7ac6 feat(tpl): 增加自定义标签支持,优化生成的结构体字段标签配置 2025-09-30 16:23:27 +08:00
4578e6811c feat(tpl): 重构模板生成逻辑,支持字段映射和类型映射合并 2025-09-30 15:54:25 +08:00
674443ad4e Merge branch 'master' of github.com:gogf/gf into personal/hailaz 2025-09-29 15:16:20 +08:00
af20fbbde7 更新 DAO 生成模板以使用 JSON 字段名,并重构 NewTable 函数以接受 TplObj 作为参数 2025-02-20 17:20:59 +08:00
3a2a73c786 up 2025-01-25 18:01:25 +08:00
9f0de3d535 up 2025-01-24 17:48:48 +08:00
0356acaa60 Merge branch 'personal/hailaz' of github.com:gogf/gf into personal/hailaz 2025-01-24 17:36:01 +08:00
6022e917b8 up 2025-01-24 17:35:48 +08:00
7804d70afc Apply gci import order changes 2025-01-24 09:27:07 +00:00
ea3318d33c Merge branch 'personal/hailaz' of github.com:gogf/gf into personal/hailaz 2025-01-24 17:26:30 +08:00
4120223bbf up 2025-01-24 17:23:10 +08:00
3353927847 Apply gci import order changes 2025-01-24 08:26:40 +00:00
521854e371 up 2025-01-24 16:26:04 +08:00
457e9c158b Merge branch 'master' of github.com:gogf/gf into personal/hailaz 2025-01-24 11:54:48 +08:00
cca8210361 docs: 更新目录说明 2025-01-24 11:54:20 +08:00
854254028f docs: 更新介绍文件 2025-01-23 17:40:38 +08:00
4bada34e9e Apply gci import order changes 2025-01-23 03:49:22 +00:00
9a8eba4eb5 Merge branch 'master' of github.com:gogf/gf into personal/hailaz 2025-01-23 11:48:23 +08:00
8a0dcf060e up 2023-12-13 14:52:37 +08:00
af15651a71 up 2023-12-12 10:18:22 +08:00
591f55155a fix: 调整目录细化功能 2023-12-11 16:24:17 +08:00
b248fbb747 Merge branch 'master' of github.com:gogf/gf into personal/hailaz 2023-12-07 17:21:34 +08:00
06e23d73e9 Merge branch 'master' of github.com:gogf/gf into personal/hailaz 2023-11-28 17:25:29 +08:00
72ddbe3258 test: 临时提交 2023-11-02 16:53:15 +08:00
60 changed files with 2025 additions and 8569 deletions

336
.github/copilot-instructions.md vendored Normal file
View File

@ -0,0 +1,336 @@
# GoFrame 项目指导说明
## 项目概述
GoFrame (GF) 是一个模块化、高性能、企业级的 Golang 基础开发框架。
## 目录结构说明
### 主包
```shell
gf
├── container // 容器相关包
│ ├── garray // 数组容器
│ ├── glist // 链表容器
│ ├── gmap // Map容器
│ ├── gpool // 对象池
│ ├── gqueue // 队列
│ ├── gring // 环形缓冲区
│ ├── gset // 集合
│ ├── gtree // 树结构
│ ├── gtype // 并发安全类型
│ └── gvar // 动态变量
├── crypto // 加密相关
│ ├── gaes // AES加密
│ ├── gcrc32 // CRC32校验
│ ├── gdes // DES加密
│ ├── gmd5 // MD5哈希
│ └── gsha1 // SHA1哈希
├── database // 数据库相关
│ ├── gdb // 数据库ORM
│ └── gredis // Redis客户端
├── debug // 调试工具
│ └── gdebug // 调试辅助
├── encoding // 编码相关
│ ├── gbase64 // Base64编码
│ ├── gbinary // 二进制编码
│ ├── gcharset // 字符集转换
│ ├── gcompress // 压缩解压
│ ├── ghash // 哈希算法
│ ├── ghtml // HTML处理
│ ├── gini // INI解析
│ ├── gjson // JSON处理
│ ├── gproperties // Properties解析
│ ├── gtoml // TOML解析
│ ├── gurl // URL处理
│ ├── gxml // XML处理
│ └── gyaml // YAML处理
├── errors // 错误处理
│ ├── gcode // 错误码
│ └── gerror // 错误处理
├── frame // 框架核心
│ ├── g // 全局对象
│ └── gins // 依赖注入
├── i18n // 国际化
│ └── gi18n // 国际化支持
├── net // 网络相关
│ ├── gclient // HTTP客户端
│ ├── ghttp // HTTP服务端
│ ├── gipv4 // IPv4工具
│ ├── gipv6 // IPv6工具
│ ├── goai // AI工具
│ ├── gsel // 服务发现
│ ├── gsvc // 服务治理
│ ├── gtcp // TCP工具
│ ├── gtrace // 链路追踪
│ └── gudp // UDP工具
├── os // 系统相关
│ ├── gbuild // 构建工具
│ ├── gcache // 缓存管理
│ ├── gcfg // 配置管理
│ ├── gcmd // 命令行解析
│ ├── gcron // 定时任务
│ ├── gctx // 上下文管理
│ ├── genv // 环境变量
│ ├── gfile // 文件操作
│ ├── gfpool // 文件池
│ ├── gfsnotify // 文件监控
│ ├── glog // 日志管理
│ ├── gmetric // 指标监控
│ ├── gmlock // 内存锁
│ ├── gmutex // 互斥锁
│ ├── gproc // 进程管理
│ ├── gres // 资源管理
│ ├── grpool // 协程池
│ ├── gsession // 会话管理
│ ├── gspath // 路径处理
│ ├── gstructs // 结构体工具
│ ├── gtime // 时间处理
│ ├── gtimer // 定时器
│ └── gview // 视图渲染
├── test // 测试工具
│ └── gtest // 测试框架
├── text // 文本处理
│ ├── gregex // 正则表达式
│ └── gstr // 字符串工具
└── util // 工具类
├── gconv // 类型转换
├── gmeta // 元数据处理
├── gmode // 运行模式
├── gpage // 分页工具
├── grand // 随机数
├── gtag // 标签处理
├── guid // UUID生成
├── gutil // 通用工具
└── gvalid // 数据校验
```
### cmd 命令行工具
```shell
cmd
├── gf // GF CLI主程序
│ ├── gfcmd // CLI命令入口
│ │ ├── build // 项目构建 (cmd_build.go)
│ │ ├── run // 热编译运行 (cmd_run.go)
│ │ ├── init // 项目脚手架 (cmd_init.go)
│ │ ├── gen // 代码生成入口 (cmd_gen.go)
│ │ ├── docker // 容器化操作 (cmd_docker.go)
│ │ ├── install // 依赖管理 (cmd_install.go)
│ │ ├── fix // 代码修复 (cmd_fix.go)
│ │ ├── update // 框架升级 (cmd_up.go)
│ │ ├── env // 环境变量管理 (cmd_env.go)
│ │ ├── pack // 二进制打包 (cmd_pack.go)
│ │ └── doc // 文档生成 (cmd_doc.go)
│ ├── internal/cmd/ // 命令实现核心
│ │ ├── cmd_build.go // 构建命令:交叉编译支持/构建参数配置
│ │ ├── cmd_doc.go // 文档命令Swagger/API文档自动化生成
│ │ ├── cmd_docker.go // Docker命令镜像构建/推送/多阶段编译
│ │ ├── cmd_env.go // 环境管理:变量查看/设置/环境切换
│ │ ├── cmd_fix.go // 代码修复:自动修复常见语法问题
│ │ ├── cmd_gen.go // 代码生成:统一入口路由
│ │ ├── cmd_gen_ctrl.go // MVC控制器RESTful接口生成
│ │ ├── cmd_gen_dao.go // DAO层数据库表映射生成
│ │ ├── cmd_gen_enums.go // 枚举代码:自动生成枚举类型和方法
│ │ ├── cmd_gen_pb.go // Protobuf协议文件编译生成
│ │ ├── cmd_gen_pbentity.go // Protobuf实体数据库表到proto转换
│ │ ├── cmd_gen_service.go // 微服务接口GRPC服务代码生成
│ │ ├── cmd_init.go // 项目初始化:模块化脚手架生成
│ │ ├── cmd_install.go // 依赖管理自动分析并安装go依赖
│ │ ├── cmd_pack.go // 打包发布:支持二进制/Docker/zip多种格式
│ │ ├── cmd_run.go // 运行管理:热编译/配置重载/进程监控
│ │ ├── cmd_tpl.go // 模板管理:自定义代码模板系统
│ │ ├── cmd_up.go // 框架升级:版本检测与自动更新
│ │ ├── cmd_version.go // 版本管理CLI/Golang/框架版本信息
│ │ ├── cmd_z_init_test.go // 初始化测试:脚手架生成验证
│ │ └── cmd_z_unit_*_test.go // 单元测试:各命令功能验证
│ ├── internal/cmd/gen/ // 代码生成模板
│ │ ├── tpl_field.go // 字段级模板(列映射/类型转换)
│ │ ├── tpl_table.go // 表级模板(CRUD操作/关系映射)
│ │ ├── tpl_test.go // 测试用例模板
│ │ ├── tpl_ctrl.go // 控制器模板(RESTful方法)
│ │ ├── tpl_service.go // 服务层模板(业务逻辑)
│ │ └── tpl_pbentity.go // Protobuf实体模板
│ ├── test/ // 测试相关
│ │ ├── cmd_z_unit_build_test.go // 构建命令单元测试
│ │ ├── cmd_z_unit_gen_dao_test.go // DAO生成测试
│ │ └── testdata/ // 测试用例数据
│ ├── internal // 内部实现
│ ├── test // 测试代码
│ ├── go.mod // 模块文件
│ ├── go.sum // 依赖校验
│ ├── go.work // 工作区配置
│ ├── LICENSE // 许可证
│ ├── main.go // 主入口
│ ├── Makefile // 构建配置
│ └── README.MD // 说明文档
```
### contrib 组件库
```shell
contrib
├── config // 配置中心支持
│ ├── apollo // Apollo配置中心
│ ├── consul // Consul配置中心
│ ├── kubecm // Kubernetes ConfigMap支持
│ ├── nacos // Nacos配置中心
│ └── polaris // Polaris配置中心
├── drivers // 数据库驱动
│ ├── clickhouse // ClickHouse驱动
│ ├── dm // 达梦数据库驱动
│ ├── mssql // SQL Server驱动
│ ├── mysql // MySQL驱动
│ ├── oracle // Oracle驱动
│ ├── pgsql // PostgreSQL驱动
│ ├── sqlite // SQLite驱动
│ └── sqlitecgo // SQLite CGO驱动
├── metric // 指标监控
│ └── otelmetric // OpenTelemetry指标支持
├── nosql // NoSQL支持
│ └── redis // Redis支持
├── registry // 服务注册发现
│ ├── consul // Consul支持
│ ├── etcd // Etcd支持
│ ├── file // 文件注册中心
│ ├── nacos // Nacos支持
│ ├── polaris // Polaris支持
│ └── zookeeper // Zookeeper支持
├── rpc // RPC支持
│ └── grpcx // gRPC扩展支持
├── sdk // SDK支持
│ └── httpclient // HTTP客户端SDK
└── trace // 链路追踪
├── otlpgrpc // OpenTelemetry gRPC支持
└── otlphttp // OpenTelemetry HTTP支持
```
### examples 示例库
```shell
examples
├── balancer // 负载均衡示例
│ ├── http // HTTP负载均衡
│ └── polaris // Polaris负载均衡
├── config // 配置中心示例
│ ├── apollo // Apollo配置中心
│ ├── consul // Consul配置中心
│ ├── kubecm // Kubernetes ConfigMap
│ ├── nacos // Nacos配置中心
│ └── polaris // Polaris配置中心
├── converter // 类型转换示例
│ ├── alias-type-convert // 别名类型转换
│ ├── alias-type-scan // 别名类型扫描
│ ├── struct-convert // 结构体转换
│ └── struct-scan // 结构体扫描
├── database // 数据库示例
│ └── mysql // MySQL数据库
├── httpserver // HTTP服务示例
│ ├── default-value // 默认值处理
│ ├── proxy // 代理服务
│ ├── rate // 限流控制
│ ├── response-with-json // JSON响应
│ ├── serve-file // 文件服务
│ ├── swagger // Swagger文档
│ ├── upload-file // 文件上传
│ └── swagger-set-template // Swagger模板
├── metric // 指标监控示例
│ ├── basic // 基础指标
│ ├── callback // 回调指标
│ ├── dynamic-attributes // 动态属性
│ ├── global-attributes // 全局属性
│ ├── http-client // HTTP客户端指标
│ ├── http-server // HTTP服务端指标
│ ├── meter-attributes // 计量器属性
│ └── prometheus // Prometheus集成
├── nosql // NoSQL示例
│ └── redis // Redis操作
├── os // 系统操作示例
│ ├── cron // 定时任务
│ └── log // 日志管理
├── pack // 打包示例
│ ├── hack // 打包工具
│ ├── manifest // 清单文件
│ ├── packed // 打包结果
│ └── resource // 资源文件
├── registry // 服务注册发现示例
│ ├── consul // Consul注册中心
│ ├── etcd // Etcd注册中心
│ ├── file // 文件注册中心
│ ├── nacos // Nacos注册中心
│ └── polaris // Polaris注册中心
├── rpc // RPC示例
│ └── grpcx // gRPC扩展
├── tcp // TCP示例
│ └── server // TCP服务
└── trace // 链路追踪示例
├── grpc-with-db // gRPC+数据库
├── http // HTTP链路
├── http-with-db // HTTP+数据库
├── inprocess // 进程内追踪
├── inprocess-grpc // 进程内gRPC
├── otlp // OpenTelemetry
├── processes // 进程管理
└── provider // 追踪提供者
```
## 编码规范
1. 命名规范
- 包名使用小写
- 结构体、接口名使用大驼峰
- 方法名使用大驼峰
- 变量名使用小驼峰
2. 代码格式
- 使用`gofmt`标准格式化
- 遵循 Go 官方代码规范
- 每个包都应有详细的文档注释
3. 错误处理
- 使用`gerror`包进行错误处理
- 错误信息应该清晰明确
4. 测试规范
- 所有公开接口需要单元测试
- 测试文件命名为`xxx_test.go`
- 基准测试命名为`BenchmarkXxx`
5. 依赖管理
- 使用`go mod`进行依赖管理
- golang 的版本根据 go.mod 文件中的 go 版本进行管理
## 项目特定指南
1. 模块开发
- 遵循模块化设计原则
- 使用依赖注入模式
- 保持向后兼容性
2. 文档编写
- 使用英文编写代码注释
- 中英文文档同步更新
- 示例代码需要可运行
3. 性能考虑
- 注意内存分配
- 避免不必要的类型转换
- 合理使用缓存机制
## 代码生成建议
生成代码时请遵循以下原则:
- 符合 Go 语言惯用法
- 保持代码简洁清晰
- 注重性能和可维护性
- 添加必要的注释说明

3
.gitignore vendored
View File

@ -24,5 +24,4 @@ node_modules
.docusaurus
output
.example/
.golangci.bck.yml
*.exe
.golangci.bck.yml

View File

@ -1,231 +0,0 @@
# go list 性能优化 - 完成报告
**日期**2026-01-09
**优化目标**:减少 `go list` 调用次数,提升依赖分析性能
**优化结果**:✅ 从 3 次调用减少到 2 次调用 (-33%)
---
## 问题分析
### 原始问题
`cmddep_analyzer.go``loadPackages()` 方法中存在 **3 次 `go list` 调用**
1. `go list -json %s` (行 158)
- 目标:获取指定的包信息
- 问题:只获取主包,不含依赖信息
2. `go list -json -deps %s` (行 179)
- 目标:获取指定包及其所有依赖
- 问题:和第一次调用冗余,且可能因时间差而导致数据不一致
3. `go list -json -m all` (行 198)
- 目标:获取所有模块信息(包括在 go.mod 中但代码未使用的模块)
- 问题:第三次调用导致性能开销
### 性能影响
- **频繁I/O**:每次 `go list` 都涉及 Go 工具链的启动和包元数据扫描
- **时间累积**:大型项目中每次调用可能耗时 200-500ms3 次调用总耗时可达 1-1.5s
- **不一致风险**:连续调用可能因依赖版本变更而返回不同结果
---
## 优化方案
### 关键优化点
**优化前的调用顺序**
```
调用1: go list -json %s (主包)
调用2: go list -json -deps %s (主包+依赖)
调用3: go list -json -m all (所有模块)
```
**优化后的调用顺序**
```
调用1: go list -json -m all (所有模块) - 快速
↓ 用模块信息预填充 packages 集合
调用2: go list -json -deps %s (主包+依赖) - 覆盖/补充包信息
```
### 具体改进
1. **取消第一次调用**
- 原因:`go list -json -deps` 已包含主包信息,第一次调用冗余
- 效果:减少 1 次调用 (-33%)
2. **调整模块加载时机**
- 原因:模块信息加载应先执行,为包信息加载做准备
- 优势:支持 go.mod 中声明但未在代码中直接使用的模块出现在依赖图中
3. **优化错误处理**
- 模块加载失败不中断:`if err != nil { moduleResult = "" }`
- 保证即使模块加载失败,包加载仍能继续
---
## 实现细节
### 改动代码 (cmddep_analyzer.go)
```go
// loadPackages loads package information using go list with optimized approach.
// OPTIMIZATION: Reduced from 3 separate go list calls to 2 efficient calls:
// Previously:
// 1. go list -json %s (target packages only)
// 2. go list -json -deps %s (with dependencies)
// 3. go list -json -m all (all modules)
// Now (optimized):
// 1. go list -json -m all (all modules - fast, definitive)
// 2. go list -json -deps ./... (all packages with dependencies)
func (a *analyzer) loadPackages(ctx context.Context, pkgPath string) error {
// First, load module information - fast, provides metadata
moduleCmd := "go list -json -m all"
moduleResult, err := gproc.ShellExec(ctx, moduleCmd)
if err != nil {
moduleResult = "" // Module loading is optional
}
// Parse modules and pre-populate packages
if moduleResult != "" {
// ... decode modules ...
}
// Second, load package information with dependencies
cmd := fmt.Sprintf("go list -json -deps %s", pkgPath)
result, err := gproc.ShellExec(ctx, cmd)
if err != nil {
// ... error handling ...
}
// Parse packages
// ... decode packages ...
return nil
}
```
---
## 验证结果
### 编译检查
✅ 编译成功,无编译错误
### Lint 检查
✅ 无 lint 警告或错误
### 向后兼容性
**完全兼容**
- `loadPackages()` 方法签名不变
- 返回结果(`a.packages` 数据结构)不变
- 所有调用者代码无需修改
### 功能正确性
✅ 功能保持一致
- 获取相同的包和模块信息
- 建立相同的依赖关系图
- 支持所有原有的过滤和遍历操作
---
## 性能指标
### 理论改进
| 指标 | 优化前 | 优化后 | 改进 |
|------|------|------|------|
| **go list 调用数** | 3 次 | 2 次 | -33% |
| **预期执行时间** | ~600-1500ms | ~400-1000ms | -33% |
### 说明
- 假设每次调用 200-500ms
- 模块加载(`go list -m all`)通常最快,因为不需要扫描代码
- 包依赖加载(`go list -deps`)取决于项目复杂度
---
## 设计决策
### 为什么是 2 次而不是 1 次?
虽然设计目标是"1 次调用",但实际上 2 次调用是更合理的方案:
**原因**
1. `go list -deps %s``go list -m all`**命令标志组合不兼容**
- `-deps` 要求指定包/模块作为分析起点
- `-m` 要求以模块视图操作
- 两者不能在同一命令中有效结合
2. 模块加载和包加载的 **职责不同**
- 模块加载:获取 go.mod 声明的完整依赖
- 包加载:获取代码中实际使用的包
- 两者信息互补
3. **错误隔离**的好处
- 模块加载失败不影响包加载
- 提高系统鲁棒性
---
## 后续优化机会
### 1. 缓存层 (未来版本)
```go
// 缓存 go list 结果,支持增量更新
type PackageCache struct {
modules map[string]bool // 缓存模块清单
packages map[string]*goPackage // 缓存包信息
checksum string // go.mod 校验和
}
```
### 2. 并行加载 (未来版本)
```go
// 并行执行两次 go list 调用
go func() { moduleResult = shell_exec(moduleCmd) }()
result = shell_exec(packageCmd) // 同时执行
// 等待两者完成...
```
### 3. 按需加载 (未来版本)
- 支持渐进式加载(只分析指定包及其直接依赖)
- 支持深度控制(避免加载整个传递依赖树)
---
## 代码变更统计
| 指标 | 值 |
|------|-----|
| 修改文件 | 1 (`cmddep_analyzer.go`) |
| 修改行数 | ~70 行 |
| 删除行数 | 50+ 行 |
| 净增加 | ~20 行 |
| 编译错误 | 0 |
| Lint 警告 | 0 |
| 测试通过 | ✅ |
---
## 总结
**性能优化完成** - 成功将 `go list` 调用从 3 次减少到 2 次,提升了依赖分析性能并改善了代码清晰度。
### 主要成就
- ✅ 减少 1/3 的 go list 调用
- ✅ 改进代码结构(模块优先加载)
- ✅ 增强错误隔离(模块加载失败不影响包加载)
- ✅ 保持 100% 向后兼容
- ✅ 零性能退化
### 建议
- 立即合并此优化
- 后续考虑实现缓存和并行加载进一步改进
---
**完成日期**2026-01-09
**优化者**AI Assistant
**状态**:✅ 完成并验证

View File

@ -1,263 +0,0 @@
# 修复报告 - MainModuleOnly 参数无效问题
**日期**2026-01-09
**优先级**:🔴 高 (功能缺陷)
**状态**:✅ 已修复
---
## 问题描述
`--main-only` (仅主模块) 参数在代码中定义但从未实际被使用,导致该参数完全无效。
### 表现
- 用户使用 `gf dep --main-only` 时,仍然显示所有包(包括子模块的包)
- 参数被解析,但在过滤逻辑中被忽略
### 根本原因
虽然有定义 `MainModuleOnly` 字段在 `FilterOptions` 中,但:
1. `ShouldInclude()` 方法从未检查这个参数
2. `PackageInfo` 没有记录包是否属于主模块的信息
3. 虽然有 `isMainModulePackage()` 方法可以判断,但它不被调用
---
## 修复方案
### 改动 1: 扩展 `PackageInfo` 结构
**文件**`cmddep_analyzer.go` (L51-60)
```go
type PackageInfo struct {
ImportPath string // Full import path
ModulePath string // Module path
Kind PackageKind // Package classification
Tier int // Package tier
Imports []string // Direct imports
IsStdLib bool // Standard library marker
IsModuleRoot bool // Is this the root package of its module
IsMainModule bool // ← NEW: Is this package from the main module
}
```
**原因**:需要在包信息中记录"是否属于主模块",以便在过滤时使用。
### 改动 2: 更新 `buildPackageStore()` 方法
**文件**`cmddep_analyzer.go` (L636-653)
```go
func (a *analyzer) buildPackageStore() *PackageStore {
store := newPackageStore(a.modulePrefix)
for path, goPkg := range a.packages {
pkgInfo := &PackageInfo{
ImportPath: path,
ModulePath: goPkg.Module.Path,
IsStdLib: goPkg.Standard,
Imports: goPkg.Imports,
IsMainModule: a.isMainModulePackage(path), // ← NEW
}
pkgInfo.Kind = store.identifyPackageKind(pkgInfo)
store.packages[path] = pkgInfo
}
return store
}
```
**原因**:在构建 `PackageInfo` 时调用 `isMainModulePackage()` 来填充 `IsMainModule` 字段。
### 改动 3: 修复 `ShouldInclude()` 方法
**文件**`cmddep_analyzer.go` (L325-346)
```go
func (opts *FilterOptions) ShouldInclude(pkg *PackageInfo) bool {
// ← NEW: Check main module filter first
if opts.MainModuleOnly && !pkg.IsMainModule {
return false
}
// Filter by kind
switch pkg.Kind {
case KindStdLib:
if !opts.IncludeStdLib {
return false
}
case KindInternal:
if !opts.IncludeInternal {
return false
}
case KindExternal:
if !opts.IncludeExternal {
return false
}
}
return true
}
```
**原因**:在过滤逻辑中实际检查 `MainModuleOnly` 参数。
---
## 影响范围
### 受影响的功能
- ✅ 命令行 `gf dep --main-only` 命令
- ✅ Web UI "Main module only" 复选框
- ✅ HTTP API `?main=true` 参数
### 受影响的输出格式
- ✅ Tree 格式
- ✅ List 格式
- ✅ JSON 格式
- ✅ Mermaid 格式
- ✅ Dot 格式
- ✅ Reverse 格式
- ✅ Group 格式
---
## 验证结果
### 编译检查
✅ 编译成功,无编译错误
### Lint 检查
✅ 无 lint 警告或错误
### 向后兼容性
**完全兼容**
- 所有现有代码无需修改
- API 签名不变
### 功能正确性
✅ 现在可以正确过滤子模块的包
---
## 使用示例
### 命令行
```bash
# 仅显示主模块的包
gf dep --main-only
# 结合其他参数
gf dep --external --main-only
```
### Web UI
- 勾选 "Main module only" 复选框来过滤掉子模块
### HTTP API
```
GET /api/packages?main=true
GET /api/tree?main=true
```
---
## 技术细节
### `isMainModulePackage()` 工作原理
位于 `cmddep_analyzer.go` (L381-408)
```go
func (a *analyzer) isMainModulePackage(pkg string) bool {
// 没有模块前缀,认为所有包都在主模块中
if a.modulePrefix == "" {
return true
}
// 包不在模块范围内
if !gstr.HasPrefix(pkg, a.modulePrefix) {
return false
}
// 移除模块前缀得到相对路径
relativePath := gstr.TrimLeft(pkg[len(a.modulePrefix):], "/")
if relativePath == "" {
return true // 这是模块根本身
}
// 检查是否有子 go.mod 文件(表示子模块)
parts := gstr.Split(relativePath, "/")
for i := len(parts); i > 0; i-- {
subPath := gstr.Join(parts[:i], "/")
if subPath != "" && gfile.Exists(subPath+"/go.mod") {
return false // 找到了子模块
}
}
return true // 这是主模块的一部分
}
```
**工作流程**
1. 验证包在模块范围内
2. 检查包相对路径中是否存在 `go.mod` 文件
3. 如果存在子 `go.mod`,说明这是子模块,返回 `false`
4. 否则是主模块的一部分,返回 `true`
---
## 相关代码位置
| 组件 | 文件 | 行号 | 用途 |
|------|------|------|------|
| PackageInfo | cmddep_analyzer.go | L51-60 | 数据模型 |
| FilterOptions | cmddep_analyzer.go | L62-77 | 过滤参数 |
| ShouldInclude() | cmddep_analyzer.go | L323-346 | 过滤决策 |
| buildPackageStore() | cmddep_analyzer.go | L636-653 | 数据构建 |
| isMainModulePackage() | cmddep_analyzer.go | L381-408 | 主模块检测 |
---
## 测试建议
### 手动测试
```bash
# 创建有子模块的项目或使用现有项目
cd /path/to/project/with/submodule
# 测试不带参数
gf dep --tree
# 测试只显示主模块
gf dep --tree --main-only
# 验证结果应该显著减少(子模块包被过滤)
```
### 自动测试
建议添加单元测试验证:
- `MainModuleOnly=true``ShouldInclude()` 正确返回 `false` 对于非主模块包
- `buildPackageStore()` 正确设置 `IsMainModule` 字段
- 各种输出格式都正确应用过滤
---
## 总结
**功能修复完成**
| 项 | 状态 |
|----|------|
| **编译** | ✅ 成功 |
| **Lint** | ✅ 无错误 |
| **功能** | ✅ 正常 |
| **兼容性** | ✅ 100% |
现在 `--main-only` 参数可以正确过滤掉子模块的包。
---
**完成日期**2026-01-09
**修复者**AI Assistant
**状态**:✅ 完成并验证

View File

@ -22,7 +22,6 @@ import (
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/cmd/gf/v2/internal/cmd"
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/cmddep"
"github.com/gogf/gf/cmd/gf/v2/internal/utility/allyes"
"github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
)
@ -90,7 +89,6 @@ func GetCommand(ctx context.Context) (*Command, error) {
cmd.Install,
cmd.Version,
cmd.Doc,
cmddep.Dep,
)
if err != nil {
return nil, err

View File

@ -37,13 +37,11 @@ type cEnvInput struct {
type cEnvOutput struct{}
func (c cEnv) Index(ctx context.Context, in cEnvInput) (out *cEnvOutput, err error) {
result, execErr := gproc.ShellExec(ctx, "go env")
// Note: go env may return non-zero exit code when there are warnings (e.g., invalid characters in env vars),
// but it still outputs valid environment variables. So we only fail if result is empty.
result, err := gproc.ShellExec(ctx, "go env")
if err != nil {
mlog.Fatal(err)
}
if result == "" {
if execErr != nil {
mlog.Fatal(execErr)
}
mlog.Fatal(`retrieving Golang environment variables failed, did you install Golang?`)
}
var (
@ -61,9 +59,7 @@ func (c cEnv) Index(ctx context.Context, in cEnvInput) (out *cEnvOutput, err err
}
match, _ := gregex.MatchString(`(.+?)=(.*)`, line)
if len(match) < 3 {
// Skip lines that don't match key=value format (e.g., warning messages from go env)
mlog.Debugf(`invalid Golang environment variable: "%s"`, line)
continue
mlog.Fatalf(`invalid Golang environment variable: "%s"`, line)
}
array = append(array, []string{gstr.Trim(match[1]), gstr.Trim(match[2])})
}

View File

@ -23,6 +23,7 @@ type cGen struct {
cGenPb
cGenPbEntity
cGenService
cGenTpl
}
const (

View File

@ -4,13 +4,10 @@
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package api
package cmd
// Status is a sample enum type for testing.
type Status int
import "github.com/gogf/gf/cmd/gf/v2/internal/cmd/gen/tpl"
const (
StatusPending Status = iota
StatusActive
StatusDone
type (
cGenTpl = tpl.CGenTpl
)

View File

@ -15,11 +15,9 @@ import (
)
var (
ctx = context.Background()
testDB gdb.DB
testPgDB gdb.DB
link = "mysql:root:12345678@tcp(127.0.0.1:3306)/test?loc=Local&parseTime=true"
linkPg = "pgsql:postgres:12345678@tcp(127.0.0.1:5432)/test"
ctx = context.Background()
testDB gdb.DB
link = "mysql:root:12345678@tcp(127.0.0.1:3306)/test?loc=Local&parseTime=true"
)
func init() {
@ -30,10 +28,6 @@ func init() {
if err != nil {
panic(err)
}
// PostgreSQL connection (optional, may not be available in all environments)
testPgDB, _ = gdb.New(gdb.ConfigNode{
Link: linkPg,
})
}
func dropTableWithDb(db gdb.DB, table string) {
@ -42,11 +36,3 @@ func dropTableWithDb(db gdb.DB, table string) {
gtest.Error(err)
}
}
// dropTableStd uses standard SQL syntax compatible with MySQL and PostgreSQL.
func dropTableStd(db gdb.DB, table string) {
dropTableStmt := fmt.Sprintf("DROP TABLE IF EXISTS %s", table)
if _, err := db.Exec(ctx, dropTableStmt); err != nil {
gtest.Error(err)
}
}

View File

@ -1,84 +0,0 @@
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package cmd
import (
"testing"
"github.com/gogf/gf/v2/test/gtest"
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
)
func Test_Env_Index(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Test that env command runs without error
_, err := Env.Index(ctx, cEnvInput{})
t.AssertNil(err)
})
}
func Test_Env_ParseGoEnvOutput(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Test parsing normal go env output
lines := []string{
"set GOPATH=C:\\Users\\test\\go",
"set GOROOT=C:\\Go",
"set GOOS=windows",
"GOARCH=amd64", // Unix format without "set " prefix
"CGO_ENABLED=0",
}
for _, line := range lines {
line = gstr.Trim(line)
if gstr.Pos(line, "set ") == 0 {
line = line[4:]
}
match, _ := gregex.MatchString(`(.+?)=(.*)`, line)
t.Assert(len(match) >= 3, true)
}
})
}
func Test_Env_ParseGoEnvOutput_WithWarnings(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Test parsing go env output that contains warning messages
// These lines should be skipped without causing errors
lines := []string{
"go: stripping unprintable or unescapable characters from %\"GOPROXY\"%",
"go: warning: some warning message",
"# this is a comment",
"",
"set GOPATH=C:\\Users\\test\\go",
"set GOOS=windows",
}
array := make([][]string, 0)
for _, line := range lines {
line = gstr.Trim(line)
if line == "" {
continue
}
if gstr.Pos(line, "set ") == 0 {
line = line[4:]
}
match, _ := gregex.MatchString(`(.+?)=(.*)`, line)
if len(match) < 3 {
// Skip lines that don't match key=value format (e.g., warning messages)
continue
}
array = append(array, []string{gstr.Trim(match[1]), gstr.Trim(match[2])})
}
// Should have parsed 2 valid environment variables
t.Assert(len(array), 2)
t.Assert(array[0][0], "GOPATH")
t.Assert(array[0][1], "C:\\Users\\test\\go")
t.Assert(array[1][0], "GOOS")
t.Assert(array[1][1], "windows")
})
}

View File

@ -10,7 +10,6 @@ import (
"testing"
"github.com/gogf/gf/v2/test/gtest"
"github.com/gogf/gf/v2/text/gstr"
)
func Test_Fix_doFixV25Content(t *testing.T) {
@ -23,82 +22,3 @@ func Test_Fix_doFixV25Content(t *testing.T) {
t.AssertNil(err)
})
}
func Test_Fix_doFixV25Content_WithReplacement(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
f = cFix{}
content = `s.BindHookHandlerByMap("/path", map[string]ghttp.HandlerFunc{
ghttp.HookBeforeServe: func(r *ghttp.Request) {},
})`
)
newContent, err := f.doFixV25Content(content)
t.AssertNil(err)
// Verify the replacement was made
t.Assert(gstr.Contains(newContent, "map[ghttp.HookName]ghttp.HandlerFunc"), true)
t.Assert(gstr.Contains(newContent, "map[string]ghttp.HandlerFunc"), false)
})
}
func Test_Fix_doFixV25Content_NoMatch(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
f = cFix{}
content = `package main
func main() {
fmt.Println("Hello World")
}
`
)
newContent, err := f.doFixV25Content(content)
t.AssertNil(err)
// Content should remain unchanged
t.Assert(newContent, content)
})
}
func Test_Fix_doFixV25Content_MultipleMatches(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
f = cFix{}
content = `
s.BindHookHandlerByMap("/path1", map[string]ghttp.HandlerFunc{})
s.BindHookHandlerByMap("/path2", map[string]ghttp.HandlerFunc{})
`
)
newContent, err := f.doFixV25Content(content)
t.AssertNil(err)
// Both should be replaced
count := gstr.Count(newContent, "map[ghttp.HookName]ghttp.HandlerFunc")
t.Assert(count, 2)
})
}
func Test_Fix_doFixV25Content_EmptyContent(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
f = cFix{}
content = ""
)
newContent, err := f.doFixV25Content(content)
t.AssertNil(err)
t.Assert(newContent, "")
})
}
func Test_Fix_doFixV25Content_ComplexPath(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
f = cFix{}
content = `s.BindHookHandlerByMap("/api/v1/user/{id}/profile", map[string]ghttp.HandlerFunc{
ghttp.HookBeforeServe: func(r *ghttp.Request) {
r.Response.Write("before")
},
})`
)
newContent, err := f.doFixV25Content(content)
t.AssertNil(err)
t.Assert(gstr.Contains(newContent, "map[ghttp.HookName]ghttp.HandlerFunc"), true)
})
}

View File

@ -460,398 +460,3 @@ func Test_Gen_Dao_Issue3749(t *testing.T) {
}
})
}
// https://github.com/gogf/gf/issues/4629
// Test tables pattern matching with * wildcard.
func Test_Gen_Dao_Issue4629_TablesPattern_Star(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
err error
db = testDB
table1 = "trade_order"
table2 = "trade_item"
table3 = "user_info"
table4 = "user_log"
table5 = "config"
sqlFilePath = gtest.DataPath(`gendao`, `tables_pattern.sql`)
)
dropTableStd(db, table1)
dropTableStd(db, table2)
dropTableStd(db, table3)
dropTableStd(db, table4)
dropTableStd(db, table5)
t.AssertNil(execSqlFile(db, sqlFilePath))
defer dropTableStd(db, table1)
defer dropTableStd(db, table2)
defer dropTableStd(db, table3)
defer dropTableStd(db, table4)
defer dropTableStd(db, table5)
var (
path = gfile.Temp(guid.S())
group = "test"
in = gendao.CGenDaoInput{
Path: path,
Link: link,
Group: group,
Tables: "trade_*", // Should match trade_order, trade_item
}
)
err = gutil.FillStructWithDefault(&in)
t.AssertNil(err)
err = gfile.Mkdir(path)
t.AssertNil(err)
pwd := gfile.Pwd()
err = gfile.Chdir(path)
t.AssertNil(err)
defer gfile.Chdir(pwd)
defer gfile.RemoveAll(path)
_, err = gendao.CGenDao{}.Dao(ctx, in)
t.AssertNil(err)
// Should generate 2 dao files: trade_order.go, trade_item.go
generatedFiles, err := gfile.ScanDir(gfile.Join(path, "dao"), "*.go", false)
t.AssertNil(err)
t.Assert(len(generatedFiles), 2)
// Verify the correct files are generated
t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_order.go")), true)
t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_item.go")), true)
// user_* and config should NOT be generated
t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_info.go")), false)
t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_log.go")), false)
t.Assert(gfile.Exists(gfile.Join(path, "dao", "config.go")), false)
})
}
// https://github.com/gogf/gf/issues/4629
// Test tables pattern matching with multiple patterns.
func Test_Gen_Dao_Issue4629_TablesPattern_Multiple(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
err error
db = testDB
table1 = "trade_order"
table2 = "trade_item"
table3 = "user_info"
table4 = "user_log"
table5 = "config"
sqlFilePath = gtest.DataPath(`gendao`, `tables_pattern.sql`)
)
dropTableStd(db, table1)
dropTableStd(db, table2)
dropTableStd(db, table3)
dropTableStd(db, table4)
dropTableStd(db, table5)
t.AssertNil(execSqlFile(db, sqlFilePath))
defer dropTableStd(db, table1)
defer dropTableStd(db, table2)
defer dropTableStd(db, table3)
defer dropTableStd(db, table4)
defer dropTableStd(db, table5)
var (
path = gfile.Temp(guid.S())
group = "test"
in = gendao.CGenDaoInput{
Path: path,
Link: link,
Group: group,
Tables: "trade_*,user_*", // Should match trade_order, trade_item, user_info, user_log
}
)
err = gutil.FillStructWithDefault(&in)
t.AssertNil(err)
err = gfile.Mkdir(path)
t.AssertNil(err)
pwd := gfile.Pwd()
err = gfile.Chdir(path)
t.AssertNil(err)
defer gfile.Chdir(pwd)
defer gfile.RemoveAll(path)
_, err = gendao.CGenDao{}.Dao(ctx, in)
t.AssertNil(err)
// Should generate 4 dao files
generatedFiles, err := gfile.ScanDir(gfile.Join(path, "dao"), "*.go", false)
t.AssertNil(err)
t.Assert(len(generatedFiles), 4)
// Verify the correct files are generated
t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_order.go")), true)
t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_item.go")), true)
t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_info.go")), true)
t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_log.go")), true)
// config should NOT be generated
t.Assert(gfile.Exists(gfile.Join(path, "dao", "config.go")), false)
})
}
// https://github.com/gogf/gf/issues/4629
// Test tables pattern mixed with exact table name.
func Test_Gen_Dao_Issue4629_TablesPattern_Mixed(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
err error
db = testDB
table1 = "trade_order"
table2 = "trade_item"
table3 = "user_info"
table4 = "user_log"
table5 = "config"
sqlFilePath = gtest.DataPath(`gendao`, `tables_pattern.sql`)
)
dropTableStd(db, table1)
dropTableStd(db, table2)
dropTableStd(db, table3)
dropTableStd(db, table4)
dropTableStd(db, table5)
t.AssertNil(execSqlFile(db, sqlFilePath))
defer dropTableStd(db, table1)
defer dropTableStd(db, table2)
defer dropTableStd(db, table3)
defer dropTableStd(db, table4)
defer dropTableStd(db, table5)
var (
path = gfile.Temp(guid.S())
group = "test"
in = gendao.CGenDaoInput{
Path: path,
Link: link,
Group: group,
Tables: "trade_*,config", // Pattern + exact name
}
)
err = gutil.FillStructWithDefault(&in)
t.AssertNil(err)
err = gfile.Mkdir(path)
t.AssertNil(err)
pwd := gfile.Pwd()
err = gfile.Chdir(path)
t.AssertNil(err)
defer gfile.Chdir(pwd)
defer gfile.RemoveAll(path)
_, err = gendao.CGenDao{}.Dao(ctx, in)
t.AssertNil(err)
// Should generate 3 dao files: trade_order.go, trade_item.go, config.go
generatedFiles, err := gfile.ScanDir(gfile.Join(path, "dao"), "*.go", false)
t.AssertNil(err)
t.Assert(len(generatedFiles), 3)
// Verify the correct files are generated
t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_order.go")), true)
t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_item.go")), true)
t.Assert(gfile.Exists(gfile.Join(path, "dao", "config.go")), true)
// user_* should NOT be generated
t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_info.go")), false)
t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_log.go")), false)
})
}
// https://github.com/gogf/gf/issues/4629
// Test tables pattern with ? wildcard (single character match).
func Test_Gen_Dao_Issue4629_TablesPattern_Question(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
err error
db = testDB
table1 = "trade_order"
table2 = "trade_item"
table3 = "user_info"
table4 = "user_log"
table5 = "config"
sqlFilePath = gtest.DataPath(`gendao`, `tables_pattern.sql`)
)
dropTableStd(db, table1)
dropTableStd(db, table2)
dropTableStd(db, table3)
dropTableStd(db, table4)
dropTableStd(db, table5)
t.AssertNil(execSqlFile(db, sqlFilePath))
defer dropTableStd(db, table1)
defer dropTableStd(db, table2)
defer dropTableStd(db, table3)
defer dropTableStd(db, table4)
defer dropTableStd(db, table5)
var (
path = gfile.Temp(guid.S())
group = "test"
in = gendao.CGenDaoInput{
Path: path,
Link: link,
Group: group,
Tables: "user_???", // ? matches single char: user_log (3 chars) but not user_info (4 chars)
}
)
err = gutil.FillStructWithDefault(&in)
t.AssertNil(err)
err = gfile.Mkdir(path)
t.AssertNil(err)
pwd := gfile.Pwd()
err = gfile.Chdir(path)
t.AssertNil(err)
defer gfile.Chdir(pwd)
defer gfile.RemoveAll(path)
_, err = gendao.CGenDao{}.Dao(ctx, in)
t.AssertNil(err)
// Should generate 1 dao file: user_log.go (3 chars after user_)
generatedFiles, err := gfile.ScanDir(gfile.Join(path, "dao"), "*.go", false)
t.AssertNil(err)
t.Assert(len(generatedFiles), 1)
// Verify only user_log is generated
t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_log.go")), true)
t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_info.go")), false) // 4 chars, doesn't match
})
}
// https://github.com/gogf/gf/issues/4629
// Test that exact table names still work (backward compatibility).
func Test_Gen_Dao_Issue4629_TablesPattern_ExactNames(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
err error
db = testDB
table1 = "trade_order"
table2 = "trade_item"
table3 = "user_info"
table4 = "user_log"
table5 = "config"
sqlFilePath = gtest.DataPath(`gendao`, `tables_pattern.sql`)
)
dropTableStd(db, table1)
dropTableStd(db, table2)
dropTableStd(db, table3)
dropTableStd(db, table4)
dropTableStd(db, table5)
t.AssertNil(execSqlFile(db, sqlFilePath))
defer dropTableStd(db, table1)
defer dropTableStd(db, table2)
defer dropTableStd(db, table3)
defer dropTableStd(db, table4)
defer dropTableStd(db, table5)
var (
path = gfile.Temp(guid.S())
group = "test"
in = gendao.CGenDaoInput{
Path: path,
Link: link,
Group: group,
Tables: "trade_order,config", // Exact names, no patterns
}
)
err = gutil.FillStructWithDefault(&in)
t.AssertNil(err)
err = gfile.Mkdir(path)
t.AssertNil(err)
pwd := gfile.Pwd()
err = gfile.Chdir(path)
t.AssertNil(err)
defer gfile.Chdir(pwd)
defer gfile.RemoveAll(path)
_, err = gendao.CGenDao{}.Dao(ctx, in)
t.AssertNil(err)
// Should generate 2 dao files
generatedFiles, err := gfile.ScanDir(gfile.Join(path, "dao"), "*.go", false)
t.AssertNil(err)
t.Assert(len(generatedFiles), 2)
// Verify exactly the specified tables are generated
t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_order.go")), true)
t.Assert(gfile.Exists(gfile.Join(path, "dao", "config.go")), true)
t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_item.go")), false)
})
}
// https://github.com/gogf/gf/issues/4629
// Test tables pattern matching with PostgreSQL.
func Test_Gen_Dao_Issue4629_TablesPattern_PgSql(t *testing.T) {
if testPgDB == nil {
t.Skip("PostgreSQL database not available, skipping test")
return
}
gtest.C(t, func(t *gtest.T) {
var (
err error
db = testPgDB
table1 = "trade_order"
table2 = "trade_item"
table3 = "user_info"
table4 = "user_log"
table5 = "config"
sqlFilePath = gtest.DataPath(`gendao`, `tables_pattern.sql`)
)
dropTableStd(db, table1)
dropTableStd(db, table2)
dropTableStd(db, table3)
dropTableStd(db, table4)
dropTableStd(db, table5)
t.AssertNil(execSqlFile(db, sqlFilePath))
defer dropTableStd(db, table1)
defer dropTableStd(db, table2)
defer dropTableStd(db, table3)
defer dropTableStd(db, table4)
defer dropTableStd(db, table5)
// Test tables pattern with tablesEx pattern
var (
path = gfile.Temp(guid.S())
group = "test"
in = gendao.CGenDaoInput{
Path: path,
Link: linkPg,
Group: group,
Tables: "trade_*,user_*,config", // Match only our test tables
TablesEx: "user_*", // Exclude user_* tables
}
)
err = gutil.FillStructWithDefault(&in)
t.AssertNil(err)
err = gfile.Mkdir(path)
t.AssertNil(err)
pwd := gfile.Pwd()
err = gfile.Chdir(path)
t.AssertNil(err)
defer gfile.Chdir(pwd)
defer gfile.RemoveAll(path)
_, err = gendao.CGenDao{}.Dao(ctx, in)
t.AssertNil(err)
// Should generate 3 dao files: trade_order, trade_item, config (user_* excluded by tablesEx)
generatedFiles, err := gfile.ScanDir(gfile.Join(path, "dao"), "*.go", false)
t.AssertNil(err)
t.Assert(len(generatedFiles), 3)
// Verify the correct files are generated
t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_order.go")), true)
t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_item.go")), true)
t.Assert(gfile.Exists(gfile.Join(path, "dao", "config.go")), true)
// user_* should NOT be generated (excluded by tablesEx)
t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_info.go")), false)
t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_log.go")), false)
})
}

View File

@ -18,92 +18,6 @@ import (
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/gendao"
)
// Test_Gen_Dao_Sharding_Overlapping tests the fix for issue #4603.
// When sharding patterns have overlapping prefixes (like "a_?", "a_b_?", "a_c_?"),
// longer (more specific) patterns should be matched first.
// https://github.com/gogf/gf/issues/4603
func Test_Gen_Dao_Sharding_Overlapping(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
err error
db = testDB
tableA1 = "a_1"
tableA2 = "a_2"
tableAB1 = "a_b_1"
tableAB2 = "a_b_2"
tableAC1 = "a_c_1"
tableAC2 = "a_c_2"
sqlFilePath = gtest.DataPath(`gendao`, `sharding`, `sharding_overlapping.sql`)
)
dropTableWithDb(db, tableA1)
dropTableWithDb(db, tableA2)
dropTableWithDb(db, tableAB1)
dropTableWithDb(db, tableAB2)
dropTableWithDb(db, tableAC1)
dropTableWithDb(db, tableAC2)
t.AssertNil(execSqlFile(db, sqlFilePath))
defer dropTableWithDb(db, tableA1)
defer dropTableWithDb(db, tableA2)
defer dropTableWithDb(db, tableAB1)
defer dropTableWithDb(db, tableAB2)
defer dropTableWithDb(db, tableAC1)
defer dropTableWithDb(db, tableAC2)
var (
path = gfile.Temp(guid.S())
group = "test"
in = gendao.CGenDaoInput{
Path: path,
Link: link,
Group: group,
Prefix: "",
// Patterns with overlapping prefixes - order should not matter due to sorting fix
ShardingPattern: []string{
`a_?`, // shortest, matches a_1, a_2 but also a_b_1, a_c_1 without fix
`a_b_?`, // longer, should match a_b_1, a_b_2
`a_c_?`, // longer, should match a_c_1, a_c_2
},
}
)
err = gutil.FillStructWithDefault(&in)
t.AssertNil(err)
err = gfile.Mkdir(path)
t.AssertNil(err)
pwd := gfile.Pwd()
err = gfile.Chdir(path)
t.AssertNil(err)
defer gfile.Chdir(pwd)
defer gfile.RemoveAll(path)
_, err = gendao.CGenDao{}.Dao(ctx, in)
t.AssertNil(err)
// Should generate 3 dao files: a.go, a_b.go, a_c.go (plus internal versions)
generatedFiles, err := gfile.ScanDir(path, "*.go", true)
t.AssertNil(err)
// 3 sharding groups * 4 files each (dao, internal, do, entity) = 12 files
t.Assert(len(generatedFiles), 12)
var (
daoAContent = gfile.GetContents(gfile.Join(path, "dao", "a.go"))
daoABContent = gfile.GetContents(gfile.Join(path, "dao", "a_b.go"))
daoACContent = gfile.GetContents(gfile.Join(path, "dao", "a_c.go"))
)
// Verify each sharding group has correct dao file generated
t.Assert(gstr.Contains(daoAContent, "aShardingHandler"), true)
t.Assert(gstr.Contains(daoAContent, "m.Sharding(gdb.ShardingConfig{"), true)
t.Assert(gstr.Contains(daoABContent, "aBShardingHandler"), true)
t.Assert(gstr.Contains(daoABContent, "m.Sharding(gdb.ShardingConfig{"), true)
t.Assert(gstr.Contains(daoACContent, "aCShardingHandler"), true)
t.Assert(gstr.Contains(daoACContent, "m.Sharding(gdb.ShardingConfig{"), true)
})
}
func Test_Gen_Dao_Sharding(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (

View File

@ -1,158 +0,0 @@
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package cmd
import (
"path/filepath"
"testing"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/test/gtest"
"github.com/gogf/gf/v2/util/guid"
"github.com/gogf/gf/v2/util/gutil"
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/genenums"
)
// https://github.com/gogf/gf/issues/4387
// Test that the output path is relative to the original working directory,
// not the source directory after Chdir.
func Test_Gen_Enums_Issue4387_RelativePath(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
// Create temp directory to simulate user's project
tempPath = gfile.Temp(guid.S())
// Copy testdata to temp directory
srcTestData = gtest.DataPath("issue", "4387")
)
// Setup: create temp project structure
err := gfile.CopyDir(srcTestData, tempPath)
t.AssertNil(err)
defer gfile.Remove(tempPath)
// Save original working directory
originalWd := gfile.Pwd()
// Change to temp directory (simulate user being in project root)
err = gfile.Chdir(tempPath)
t.AssertNil(err)
defer gfile.Chdir(originalWd) // Restore original working directory
// Run gen enums with relative paths
var (
srcFolder = "api"
outputPath = filepath.FromSlash("internal/packed/packed_enums.go")
in = genenums.CGenEnumsInput{
Src: srcFolder,
Path: outputPath,
}
)
err = gutil.FillStructWithDefault(&in)
t.AssertNil(err)
_, err = genenums.CGenEnums{}.Enums(ctx, in)
t.AssertNil(err)
// Expected: file should be created at tempPath/internal/packed/packed_enums.go
expectedPath := filepath.Join(tempPath, "internal", "packed", "packed_enums.go")
// Bug: file is created at tempPath/api/internal/packed/packed_enums.go
wrongPath := filepath.Join(tempPath, "api", "internal", "packed", "packed_enums.go")
// Assert the file is at the expected location
t.Assert(gfile.Exists(expectedPath), true)
// Assert the file is NOT at the wrong location
t.Assert(gfile.Exists(wrongPath), false)
})
}
// Test gen enums with absolute output path (should work correctly)
func Test_Gen_Enums_AbsolutePath(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
tempPath = gfile.Temp(guid.S())
srcTestData = gtest.DataPath("issue", "4387")
)
err := gfile.CopyDir(srcTestData, tempPath)
t.AssertNil(err)
defer gfile.Remove(tempPath)
originalWd := gfile.Pwd()
err = gfile.Chdir(tempPath)
t.AssertNil(err)
defer gfile.Chdir(originalWd)
// Use absolute path for output
var (
srcFolder = "api"
outputPath = filepath.Join(tempPath, "internal", "packed", "packed_enums.go")
in = genenums.CGenEnumsInput{
Src: srcFolder,
Path: outputPath,
}
)
err = gutil.FillStructWithDefault(&in)
t.AssertNil(err)
_, err = genenums.CGenEnums{}.Enums(ctx, in)
t.AssertNil(err)
// Assert the file exists at absolute path
t.Assert(gfile.Exists(outputPath), true)
})
}
// Test gen enums in monorepo mode (cd app/xxx/ then run command)
func Test_Gen_Enums_Issue4387_Monorepo(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
// Simulate monorepo structure
tempPath = gfile.Temp(guid.S())
srcTestData = gtest.DataPath("issue", "4387")
// app/myapp is the subdirectory in monorepo
appPath = filepath.Join(tempPath, "app", "myapp")
)
// Create monorepo structure: tempPath/app/myapp/api/...
err := gfile.Mkdir(appPath)
t.AssertNil(err)
// Copy testdata into app/myapp
err = gfile.CopyDir(srcTestData, appPath)
t.AssertNil(err)
defer gfile.Remove(tempPath)
originalWd := gfile.Pwd()
// cd app/myapp (simulate user in monorepo subdirectory)
err = gfile.Chdir(appPath)
t.AssertNil(err)
defer gfile.Chdir(originalWd)
var (
srcFolder = "api"
outputPath = filepath.FromSlash("internal/packed/packed_enums.go")
in = genenums.CGenEnumsInput{
Src: srcFolder,
Path: outputPath,
}
)
err = gutil.FillStructWithDefault(&in)
t.AssertNil(err)
_, err = genenums.CGenEnums{}.Enums(ctx, in)
t.AssertNil(err)
// Expected: file at app/myapp/internal/packed/packed_enums.go
expectedPath := filepath.Join(appPath, "internal", "packed", "packed_enums.go")
// Bug: file at app/myapp/api/internal/packed/packed_enums.go
wrongPath := filepath.Join(appPath, "api", "internal", "packed", "packed_enums.go")
t.Assert(gfile.Exists(expectedPath), true)
t.Assert(gfile.Exists(wrongPath), false)
})
}

View File

@ -88,76 +88,3 @@ func TestGenPbIssue3953(t *testing.T) {
t.Assert(gstr.Contains(genContent, notExceptText), false)
})
}
func TestGenPb_MultipleTags(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
outputPath = gfile.Temp(guid.S())
outputApiPath = filepath.Join(outputPath, "api")
outputCtrlPath = filepath.Join(outputPath, "controller")
protobufFolder = gtest.DataPath("genpb")
in = genpb.CGenPbInput{
Path: protobufFolder,
OutputApi: outputApiPath,
OutputCtrl: outputCtrlPath,
}
err error
)
err = gfile.Mkdir(outputApiPath)
t.AssertNil(err)
err = gfile.Mkdir(outputCtrlPath)
t.AssertNil(err)
defer gfile.Remove(outputPath)
_, err = genpb.CGenPb{}.Pb(ctx, in)
t.AssertNil(err)
// Test multiple_tags.proto output
genContent := gfile.GetContents(filepath.Join(outputApiPath, "multiple_tags.pb.go"))
// Id field should have combined validation tags: v:"required#Id > 0"
t.Assert(gstr.Contains(genContent, `v:"required#Id > 0"`), true)
// Name field should have dc tag from plain comment
t.Assert(gstr.Contains(genContent, `dc:"User name for login"`), true)
// Email field should have combined validation and dc tag
t.Assert(gstr.Contains(genContent, `v:"requiredemail"`), true)
t.Assert(gstr.Contains(genContent, `dc:"User email address"`), true)
})
}
func TestGenPb_NestedMessage(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
outputPath = gfile.Temp(guid.S())
outputApiPath = filepath.Join(outputPath, "api")
outputCtrlPath = filepath.Join(outputPath, "controller")
protobufFolder = gtest.DataPath("genpb")
in = genpb.CGenPbInput{
Path: protobufFolder,
OutputApi: outputApiPath,
OutputCtrl: outputCtrlPath,
}
err error
)
err = gfile.Mkdir(outputApiPath)
t.AssertNil(err)
err = gfile.Mkdir(outputCtrlPath)
t.AssertNil(err)
defer gfile.Remove(outputPath)
_, err = genpb.CGenPb{}.Pb(ctx, in)
t.AssertNil(err)
// Test nested_message.proto output
genContent := gfile.GetContents(filepath.Join(outputApiPath, "nested_message.pb.go"))
// Order.OrderId should have v:"required"
t.Assert(gstr.Contains(genContent, `v:"required"`), true)
// Order.Detail should have dc:"Order details"
t.Assert(gstr.Contains(genContent, `dc:"Order details"`), true)
// OrderDetail.Quantity should have v:"min:1"
t.Assert(gstr.Contains(genContent, `v:"min:1"`), true)
// OrderDetail.Price should have v:"min:0.01"
t.Assert(gstr.Contains(genContent, `v:"min:0.01"`), true)
})
}

View File

@ -156,85 +156,3 @@ func Test_Issue3835(t *testing.T) {
t.Assert(gfile.GetContents(genFile), gfile.GetContents(expectFile))
})
}
func Test_Gen_Service_CamelCase(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
path = gfile.Temp(guid.S())
dstFolder = path + filepath.FromSlash("/service")
srvFolder = gtest.DataPath("genservice", "logic")
in = genservice.CGenServiceInput{
SrcFolder: srvFolder,
DstFolder: dstFolder,
DstFileNameCase: "Camel",
WatchFile: "",
StPattern: "",
Packages: nil,
ImportPrefix: "",
Clear: false,
}
)
err := gutil.FillStructWithDefault(&in)
t.AssertNil(err)
err = gfile.Mkdir(path)
t.AssertNil(err)
defer gfile.Remove(path)
// Clean up generated logic.go
genSrv := srvFolder + filepath.FromSlash("/logic.go")
defer gfile.Remove(genSrv)
_, err = genservice.CGenService{}.Service(ctx, in)
t.AssertNil(err)
// Files should be in CamelCase
files, err := gfile.ScanDir(dstFolder, "*.go", true)
t.AssertNil(err)
t.Assert(files, []string{
dstFolder + filepath.FromSlash("/Article.go"),
dstFolder + filepath.FromSlash("/Base.go"),
dstFolder + filepath.FromSlash("/Delivery.go"),
dstFolder + filepath.FromSlash("/User.go"),
})
})
}
func Test_Gen_Service_PackagesFilter(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
path = gfile.Temp(guid.S())
dstFolder = path + filepath.FromSlash("/service")
srvFolder = gtest.DataPath("genservice", "logic")
in = genservice.CGenServiceInput{
SrcFolder: srvFolder,
DstFolder: dstFolder,
DstFileNameCase: "Snake",
WatchFile: "",
StPattern: "",
Packages: []string{"user"},
ImportPrefix: "",
Clear: false,
}
)
err := gutil.FillStructWithDefault(&in)
t.AssertNil(err)
err = gfile.Mkdir(path)
t.AssertNil(err)
defer gfile.Remove(path)
// Clean up generated logic.go
genSrv := srvFolder + filepath.FromSlash("/logic.go")
defer gfile.Remove(genSrv)
_, err = genservice.CGenService{}.Service(ctx, in)
t.AssertNil(err)
// Only user.go should be generated
files, err := gfile.ScanDir(dstFolder, "*.go", true)
t.AssertNil(err)
t.Assert(len(files), 1)
t.Assert(files[0], dstFolder+filepath.FromSlash("/user.go"))
})
}

View File

@ -1,346 +0,0 @@
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package cmd
import (
"context"
"path/filepath"
"testing"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/test/gtest"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/guid"
)
func Test_Pack_ToGoFile(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
srcPath = gfile.Temp(guid.S())
dstPath = gfile.Temp(guid.S())
dstFile = filepath.Join(dstPath, "packed", "data.go")
)
// Create source directory with test files
err := gfile.Mkdir(srcPath)
t.AssertNil(err)
defer gfile.Remove(srcPath)
err = gfile.Mkdir(dstPath)
t.AssertNil(err)
defer gfile.Remove(dstPath)
// Create test files
err = gfile.PutContents(filepath.Join(srcPath, "test.txt"), "hello world")
t.AssertNil(err)
err = gfile.PutContents(filepath.Join(srcPath, "test.json"), `{"key":"value"}`)
t.AssertNil(err)
// Create packed directory
err = gfile.Mkdir(filepath.Join(dstPath, "packed"))
t.AssertNil(err)
// Pack to go file
_, err = Pack.Index(context.Background(), cPackInput{
Src: srcPath,
Dst: dstFile,
Name: "packed",
})
t.AssertNil(err)
// Verify output file exists
t.Assert(gfile.Exists(dstFile), true)
// Verify it's a valid Go file
content := gfile.GetContents(dstFile)
t.Assert(gstr.Contains(content, "package packed"), true)
t.Assert(gstr.Contains(content, "func init()"), true)
})
}
func Test_Pack_ToBinaryFile(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
srcPath = gfile.Temp(guid.S())
dstPath = gfile.Temp(guid.S())
dstFile = filepath.Join(dstPath, "data.bin")
)
// Create source directory with test files
err := gfile.Mkdir(srcPath)
t.AssertNil(err)
defer gfile.Remove(srcPath)
err = gfile.Mkdir(dstPath)
t.AssertNil(err)
defer gfile.Remove(dstPath)
// Create test file
err = gfile.PutContents(filepath.Join(srcPath, "test.txt"), "binary content")
t.AssertNil(err)
// Pack to binary file (no Name specified)
_, err = Pack.Index(context.Background(), cPackInput{
Src: srcPath,
Dst: dstFile,
})
t.AssertNil(err)
// Verify output file exists
t.Assert(gfile.Exists(dstFile), true)
// Verify it's a binary file (not a Go file)
content := gfile.GetContents(dstFile)
t.Assert(gstr.Contains(content, "package"), false)
})
}
func Test_Pack_MultipleSources(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
srcPath1 = gfile.Temp(guid.S())
srcPath2 = gfile.Temp(guid.S())
dstPath = gfile.Temp(guid.S())
dstFile = filepath.Join(dstPath, "packed", "multi.go")
)
// Create source directories
err := gfile.Mkdir(srcPath1)
t.AssertNil(err)
defer gfile.Remove(srcPath1)
err = gfile.Mkdir(srcPath2)
t.AssertNil(err)
defer gfile.Remove(srcPath2)
err = gfile.Mkdir(dstPath)
t.AssertNil(err)
defer gfile.Remove(dstPath)
// Create test files in each source
err = gfile.PutContents(filepath.Join(srcPath1, "file1.txt"), "content1")
t.AssertNil(err)
err = gfile.PutContents(filepath.Join(srcPath2, "file2.txt"), "content2")
t.AssertNil(err)
// Create packed directory
err = gfile.Mkdir(filepath.Join(dstPath, "packed"))
t.AssertNil(err)
// Pack multiple sources (comma-separated)
_, err = Pack.Index(context.Background(), cPackInput{
Src: srcPath1 + "," + srcPath2,
Dst: dstFile,
Name: "packed",
})
t.AssertNil(err)
// Verify output file exists
t.Assert(gfile.Exists(dstFile), true)
})
}
func Test_Pack_WithPrefix(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
srcPath = gfile.Temp(guid.S())
dstPath = gfile.Temp(guid.S())
dstFile = filepath.Join(dstPath, "packed", "prefix.go")
)
// Create source directory
err := gfile.Mkdir(srcPath)
t.AssertNil(err)
defer gfile.Remove(srcPath)
err = gfile.Mkdir(dstPath)
t.AssertNil(err)
defer gfile.Remove(dstPath)
// Create test file
err = gfile.PutContents(filepath.Join(srcPath, "test.txt"), "with prefix")
t.AssertNil(err)
// Create packed directory
err = gfile.Mkdir(filepath.Join(dstPath, "packed"))
t.AssertNil(err)
// Pack with prefix
_, err = Pack.Index(context.Background(), cPackInput{
Src: srcPath,
Dst: dstFile,
Name: "packed",
Prefix: "/static",
})
t.AssertNil(err)
// Verify output file exists
t.Assert(gfile.Exists(dstFile), true)
})
}
func Test_Pack_WithKeepPath(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
srcPath = gfile.Temp(guid.S())
dstPath = gfile.Temp(guid.S())
dstFile = filepath.Join(dstPath, "packed", "keeppath.go")
)
// Create source directory with subdirectory
err := gfile.Mkdir(srcPath)
t.AssertNil(err)
defer gfile.Remove(srcPath)
err = gfile.Mkdir(dstPath)
t.AssertNil(err)
defer gfile.Remove(dstPath)
// Create subdirectory and file
subDir := filepath.Join(srcPath, "subdir")
err = gfile.Mkdir(subDir)
t.AssertNil(err)
err = gfile.PutContents(filepath.Join(subDir, "test.txt"), "keeppath content")
t.AssertNil(err)
// Create packed directory
err = gfile.Mkdir(filepath.Join(dstPath, "packed"))
t.AssertNil(err)
// Pack with keepPath
_, err = Pack.Index(context.Background(), cPackInput{
Src: srcPath,
Dst: dstFile,
Name: "packed",
KeepPath: true,
})
t.AssertNil(err)
// Verify output file exists
t.Assert(gfile.Exists(dstFile), true)
})
}
func Test_Pack_AutoPackageName(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
srcPath = gfile.Temp(guid.S())
dstPath = gfile.Temp(guid.S())
dstFile = filepath.Join(dstPath, "mypackage", "data.go")
)
// Create source directory
err := gfile.Mkdir(srcPath)
t.AssertNil(err)
defer gfile.Remove(srcPath)
err = gfile.Mkdir(dstPath)
t.AssertNil(err)
defer gfile.Remove(dstPath)
// Create test file
err = gfile.PutContents(filepath.Join(srcPath, "test.txt"), "auto package name")
t.AssertNil(err)
// Create mypackage directory
err = gfile.Mkdir(filepath.Join(dstPath, "mypackage"))
t.AssertNil(err)
// Pack without Name - should use directory name "mypackage"
_, err = Pack.Index(context.Background(), cPackInput{
Src: srcPath,
Dst: dstFile,
// Name not specified, should be auto-detected as "mypackage"
})
t.AssertNil(err)
// Verify output file exists and has correct package name
t.Assert(gfile.Exists(dstFile), true)
content := gfile.GetContents(dstFile)
t.Assert(gstr.Contains(content, "package mypackage"), true)
})
}
func Test_Pack_EmptySource(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
srcPath = gfile.Temp(guid.S())
dstPath = gfile.Temp(guid.S())
dstFile = filepath.Join(dstPath, "packed", "empty.go")
)
// Create empty source directory
err := gfile.Mkdir(srcPath)
t.AssertNil(err)
defer gfile.Remove(srcPath)
err = gfile.Mkdir(dstPath)
t.AssertNil(err)
defer gfile.Remove(dstPath)
// Create packed directory
err = gfile.Mkdir(filepath.Join(dstPath, "packed"))
t.AssertNil(err)
// Pack empty directory
_, err = Pack.Index(context.Background(), cPackInput{
Src: srcPath,
Dst: dstFile,
Name: "packed",
})
t.AssertNil(err)
// Verify output file exists (even if source is empty)
t.Assert(gfile.Exists(dstFile), true)
})
}
func Test_Pack_NestedDirectories(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
srcPath = gfile.Temp(guid.S())
dstPath = gfile.Temp(guid.S())
dstFile = filepath.Join(dstPath, "packed", "nested.go")
)
// Create source directory with nested structure
err := gfile.Mkdir(srcPath)
t.AssertNil(err)
defer gfile.Remove(srcPath)
err = gfile.Mkdir(dstPath)
t.AssertNil(err)
defer gfile.Remove(dstPath)
// Create nested directories and files
level1 := filepath.Join(srcPath, "level1")
level2 := filepath.Join(level1, "level2")
level3 := filepath.Join(level2, "level3")
err = gfile.Mkdir(level3)
t.AssertNil(err)
err = gfile.PutContents(filepath.Join(srcPath, "root.txt"), "root")
t.AssertNil(err)
err = gfile.PutContents(filepath.Join(level1, "l1.txt"), "level1")
t.AssertNil(err)
err = gfile.PutContents(filepath.Join(level2, "l2.txt"), "level2")
t.AssertNil(err)
err = gfile.PutContents(filepath.Join(level3, "l3.txt"), "level3")
t.AssertNil(err)
// Create packed directory
err = gfile.Mkdir(filepath.Join(dstPath, "packed"))
t.AssertNil(err)
// Pack nested directories
_, err = Pack.Index(context.Background(), cPackInput{
Src: srcPath,
Dst: dstFile,
Name: "packed",
})
t.AssertNil(err)
// Verify output file exists
t.Assert(gfile.Exists(dstFile), true)
// Verify content includes all files
content := gfile.GetContents(dstFile)
t.Assert(gstr.Contains(content, "package packed"), true)
})
}

View File

@ -1,140 +0,0 @@
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package cmddep
import (
"context"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gtag"
"github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
)
var (
Dep = cDep{}
)
type cDep struct {
g.Meta `name:"dep" brief:"{cDepBrief}" eg:"{cDepEg}"`
}
const (
cDepBrief = `analyze and display Go package dependencies`
cDepEg = `
gf dep
gf dep ./...
gf dep ./internal/...
gf dep -f list
gf dep -f mermaid
gf dep -f mermaid -g
gf dep -f dot -d 5
gf dep -f json -d 0
gf dep -g
gf dep -r
gf dep -i=false
gf dep -e
gf dep -e -i=false
gf dep -M
gf dep -M -D
gf dep -s
gf dep -s -p 8080
gf dep ./internal/... -f tree -d 2
gf dep --external --group -f mermaid
gf dep --module --direct -f json
`
)
func init() {
gtag.Sets(g.MapStrStr{
`cDepBrief`: cDepBrief,
`cDepEg`: cDepEg,
})
}
// Input defines the input parameters for dep command.
type Input struct {
g.Meta `name:"dep"`
Package string `name:"PACKAGE" arg:"true" brief:"package path to analyze, default is ./..." d:"./..."`
Format string `name:"format" short:"f" brief:"output format: tree/list/mermaid/dot/json" d:"tree"`
Depth int `name:"depth" short:"d" brief:"dependency depth limit, 0 means unlimited" d:"3"`
Group bool `name:"group" short:"g" brief:"group by top-level directory" d:"false" orphan:"true"`
Internal bool `name:"internal" short:"i" brief:"show only internal packages" d:"true" orphan:"true"`
External bool `name:"external" short:"e" brief:"show external packages" d:"false" orphan:"true"`
Module bool `name:"module" short:"M" brief:"show module-level dependencies (from go.mod)" d:"false" orphan:"true"`
Direct bool `name:"direct" short:"D" brief:"show only direct dependencies (requires --module)" d:"false" orphan:"true"`
NoStd bool `name:"nostd" short:"n" brief:"exclude standard library" d:"true" orphan:"true"`
Reverse bool `name:"reverse" short:"r" brief:"show reverse dependencies" d:"false" orphan:"true"`
Serve bool `name:"serve" short:"s" brief:"start HTTP server to view dependencies" d:"false" orphan:"true"`
Port int `name:"port" short:"p" brief:"HTTP server port" d:"8888"`
}
// Output defines the output for dep command.
type Output struct{}
// Index is the main entry point for the dep command.
func (c cDep) Index(ctx context.Context, in Input) (out *Output, err error) {
analyzer := newAnalyzer()
// Detect module prefix from go.mod
analyzer.modulePrefix = analyzer.detectModulePrefix()
// Module-level analysis mode (uses go mod graph)
if in.Module {
if in.Serve {
// Load module graph for server mode
if err := analyzer.loadModuleGraph(ctx); err != nil {
mlog.Print("Warning: Failed to load module graph: " + err.Error())
}
return nil, analyzer.startServer(in)
}
// Load module graph
if err := analyzer.loadModuleGraph(ctx); err != nil {
return nil, err
}
// Generate module-level output
output := analyzer.generateModuleOutput(in)
mlog.Print(output)
return
}
// Package-level analysis mode (original behavior)
loadErr := analyzer.loadPackages(ctx, in.Package)
// Start HTTP server if requested
// In server mode, allow starting even without local Go module
// because users may want to analyze remote modules
if in.Serve {
if loadErr != nil {
mlog.Print("Warning: No local Go module found, you can analyze remote modules in the web UI")
}
return nil, analyzer.startServer(in)
}
// For non-server mode, return error if loading failed
if loadErr != nil {
return nil, loadErr
}
if len(analyzer.packages) == 0 {
mlog.Print("No packages found")
return
}
// Generate output based on format
var output string
if in.Reverse {
output = analyzer.generateReverse(in)
} else {
output = analyzer.generate(in)
}
mlog.Print(output)
return
}

File diff suppressed because it is too large Load Diff

View File

@ -1,107 +0,0 @@
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package cmddep
import (
"testing"
"github.com/gogf/gf/v2/test/gtest"
)
func TestExternalDependencyAnalysis(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
analyzer := newAnalyzer()
analyzer.modulePrefix = "github.com/gogf/gf/cmd/gf/v2"
analyzer.packages = map[string]*goPackage{
"github.com/other/package": {
ImportPath: "github.com/other/package",
Standard: false,
},
"github.com/gogf/gf/cmd/gf/v2/internal": {
ImportPath: "github.com/gogf/gf/cmd/gf/v2/internal",
Standard: false,
},
"fmt": {
ImportPath: "fmt",
Standard: true,
},
}
// Test using new FilterOptions system
in := Input{
Internal: false,
External: true,
NoStd: true,
}
opts := analyzer.convertInputToFilterOptions(in)
opts.Normalize(analyzer.modulePrefix)
store := analyzer.buildPackageStore()
// Test external package (should be included)
externalPkg, ok := store.packages["github.com/other/package"]
t.Assert(ok, true)
t.Assert(opts.ShouldInclude(externalPkg), true)
// Test internal package (should not be included)
internalPkg, ok := store.packages["github.com/gogf/gf/cmd/gf/v2/internal"]
t.Assert(ok, true)
t.Assert(opts.ShouldInclude(internalPkg), false)
// Test standard library (should not be included due to NoStd)
stdPkg, ok := store.packages["fmt"]
t.Assert(ok, true)
t.Assert(opts.ShouldInclude(stdPkg), false)
})
}
func TestExternalGrouping(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
analyzer := newAnalyzer()
// Test external group extraction using shortName
t.Assert(analyzer.shortName("github.com/user/repo", false), "github.com/user/repo")
t.Assert(analyzer.shortName("golang.org/x/tools", false), "golang.org/x/tools")
t.Assert(analyzer.shortName("fmt", false), "fmt")
t.Assert(analyzer.shortName("simple", false), "simple")
})
}
func TestDependencyStats(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
analyzer := newAnalyzer()
analyzer.modulePrefix = "github.com/gogf/gf/cmd/gf/v2"
// Add test packages
analyzer.packages = map[string]*goPackage{
"github.com/gogf/gf/cmd/gf/v2/internal": {
ImportPath: "github.com/gogf/gf/cmd/gf/v2/internal",
Standard: false,
},
"github.com/external/package": {
ImportPath: "github.com/external/package",
Standard: false,
},
"fmt": {
ImportPath: "fmt",
Standard: true,
},
}
in := Input{
Internal: true,
External: true,
NoStd: false,
}
stats := analyzer.getDependencyStats(in)
t.Assert(stats["total"], 3)
t.Assert(stats["internal"], 1)
t.Assert(stats["external"], 1)
t.Assert(stats["stdlib"], 1)
})
}

View File

@ -1,403 +0,0 @@
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package cmddep
import (
"encoding/json"
"fmt"
"sort"
"strings"
)
// generate creates output based on format.
func (a *analyzer) generate(in Input) string {
switch in.Format {
case "tree":
return a.generateTree(in)
case "list":
return a.generateList(in)
case "mermaid":
return a.generateMermaid(in)
case "dot":
return a.generateDot(in)
case "json":
return a.generateJSON(in)
default:
// Default to tree format
return a.generateTree(in)
}
}
// generateTree generates ASCII tree output using new traversal system.
func (a *analyzer) generateTree(in Input) string {
var sb strings.Builder
// Prepare options
opts := a.convertInputToFilterOptions(in)
opts.Normalize(a.modulePrefix)
// Add statistics header if showing external dependencies
if in.External {
stats := a.getDependencyStats(in)
sb.WriteString("Dependency Statistics:\n")
fmt.Fprintf(&sb, " Total packages: %v\n", stats["total"])
fmt.Fprintf(&sb, " Internal: %v\n", stats["internal"])
fmt.Fprintf(&sb, " External: %v\n", stats["external"])
fmt.Fprintf(&sb, " Standard library: %v\n", stats["stdlib"])
if groups, ok := stats["external_groups"].(map[string]int); ok && len(groups) > 0 {
sb.WriteString(" External groups:\n")
for group, count := range groups {
fmt.Fprintf(&sb, " %s: %d\n", group, count)
}
}
sb.WriteString("\nDependency Tree:\n")
}
// Build package store
store := a.buildPackageStore()
// Find root packages (packages that are not imported by any other package)
rootPkgs := a.findRootPackages()
// Create traversal context
ctx := &TraversalContext{
visited: make(map[string]bool),
options: opts,
store: store,
maxDepth: opts.Depth,
}
for _, pkgPath := range rootPkgs {
pkgInfo, ok := store.packages[pkgPath]
if !ok || !opts.ShouldInclude(pkgInfo) {
continue
}
shortName := a.shortName(pkgPath, in.Group)
sb.WriteString(shortName + "\n")
// Use new traversal system
a.printTreeNodeNew(&sb, pkgPath, "", in, ctx, 0)
}
return sb.String()
}
// findRootPackages finds packages that are not imported by any other internal package.
func (a *analyzer) findRootPackages() []string {
// Build a set of all imported packages
imported := make(map[string]bool)
for _, pkg := range a.packages {
for _, dep := range pkg.Imports {
imported[dep] = true
}
}
// Find packages that are not imported by others
roots := make([]string, 0)
for pkgPath := range a.packages {
if !imported[pkgPath] {
roots = append(roots, pkgPath)
}
}
// If no roots found (circular dependencies), use all packages
if len(roots) == 0 {
roots = a.getSortedPackages()
}
sort.Strings(roots)
return roots
}
// printTreeNodeNew prints tree node using new traversal system.
func (a *analyzer) printTreeNodeNew(sb *strings.Builder, pkgPath string, prefix string, in Input, ctx *TraversalContext, depth int) {
if ctx.maxDepth > 0 && depth >= ctx.maxDepth {
return
}
_, ok := ctx.store.packages[pkgPath]
if !ok {
return
}
// Get filtered dependencies
deps := ctx.GetDependencies(pkgPath)
sort.Strings(deps)
for i, dep := range deps {
// Check if already visited
if ctx.Visit(dep) {
continue
}
isLast := i == len(deps)-1
connector := "├── "
if isLast {
connector = "└── "
}
shortName := a.shortName(dep, in.Group)
sb.WriteString(prefix + connector + shortName + "\n")
newPrefix := prefix
if isLast {
newPrefix += " "
} else {
newPrefix += "│ "
}
// Recursively print dependencies
ctx.depth++
a.printTreeNodeNew(sb, dep, newPrefix, in, ctx, depth+1)
ctx.depth--
}
}
// generateList generates simple list output using new traversal system.
func (a *analyzer) generateList(in Input) string {
var sb strings.Builder
// Prepare options
opts := a.convertInputToFilterOptions(in)
opts.Normalize(a.modulePrefix)
// Add statistics header if showing external dependencies
if in.External {
stats := a.getDependencyStats(in)
sb.WriteString("# Dependency Statistics\n")
fmt.Fprintf(&sb, "# Total: %v, Internal: %v, External: %v, Stdlib: %v\n",
stats["total"], stats["internal"], stats["external"], stats["stdlib"])
sb.WriteString("\n")
}
// Build package store
store := a.buildPackageStore()
allDeps := make(map[string]bool)
// Collect dependencies from packages that should be included
for pkgPath, pkgInfo := range store.packages {
if !opts.ShouldInclude(pkgInfo) {
continue
}
// Get filtered dependencies for this package
for _, dep := range store.packages[pkgPath].Imports {
depInfo, ok := store.packages[dep]
if ok && opts.ShouldInclude(depInfo) {
allDeps[dep] = true
}
}
}
deps := make([]string, 0, len(allDeps))
for dep := range allDeps {
deps = append(deps, a.shortName(dep, in.Group))
}
sort.Strings(deps)
for _, dep := range deps {
sb.WriteString(dep + "\n")
}
return sb.String()
}
// generateMermaid generates Mermaid diagram output.
func (a *analyzer) generateMermaid(in Input) string {
var sb strings.Builder
sb.WriteString("```mermaid\n")
sb.WriteString("graph TD\n")
edges := a.collectEdges(in)
sortedEdges := make([]string, 0, len(edges))
for edge := range edges {
sortedEdges = append(sortedEdges, edge)
}
sort.Strings(sortedEdges)
for _, edge := range sortedEdges {
sb.WriteString(" " + edge + "\n")
}
sb.WriteString("```\n")
return sb.String()
}
// generateMermaidRaw generates Mermaid code without markdown wrapper.
func (a *analyzer) generateMermaidRaw(in Input) string {
var sb strings.Builder
sb.WriteString("graph TD\n")
edges := a.collectEdges(in)
sortedEdges := make([]string, 0, len(edges))
for edge := range edges {
sortedEdges = append(sortedEdges, edge)
}
sort.Strings(sortedEdges)
for _, edge := range sortedEdges {
sb.WriteString(" " + edge + "\n")
}
return sb.String()
}
// generateDot generates Graphviz DOT output.
func (a *analyzer) generateDot(in Input) string {
var sb strings.Builder
sb.WriteString("digraph deps {\n")
sb.WriteString(" rankdir=TB;\n")
sb.WriteString(" node [shape=box];\n")
edges := a.collectEdges(in)
sortedEdges := make([]string, 0, len(edges))
for edge := range edges {
sortedEdges = append(sortedEdges, edge)
}
sort.Strings(sortedEdges)
for _, edge := range sortedEdges {
parts := strings.Split(edge, " --> ")
if len(parts) == 2 {
fmt.Fprintf(&sb, " \"%s\" -> \"%s\";\n", parts[0], parts[1])
}
}
sb.WriteString("}\n")
return sb.String()
}
// generateJSON generates JSON output using new traversal system.
func (a *analyzer) generateJSON(in Input) string {
opts := a.convertInputToFilterOptions(in)
opts.Normalize(a.modulePrefix)
store := a.buildPackageStore()
result := make(map[string]any)
// Add dependency nodes
nodes := make([]*depNode, 0)
for _, pkgPath := range a.getSortedPackages() {
pkgInfo, ok := store.packages[pkgPath]
if !ok || !opts.ShouldInclude(pkgInfo) {
continue
}
pkg := a.packages[pkgPath]
a.visited = make(map[string]bool)
node := a.buildDepNode(pkg, in, 0)
nodes = append(nodes, node)
}
result["dependencies"] = nodes
// Add statistics
result["statistics"] = a.getDependencyStats(in)
// Add metadata
result["metadata"] = map[string]any{
"module": a.modulePrefix,
"format": in.Format,
"depth": in.Depth,
"group": in.Group,
"internal": in.Internal,
"external": in.External,
"nostd": in.NoStd,
}
data, err := json.MarshalIndent(result, "", " ")
if err != nil {
return fmt.Sprintf("Error: %v", err)
}
return string(data)
}
func (a *analyzer) buildDepNode(pkg *goPackage, in Input, depth int) *depNode {
opts := a.convertInputToFilterOptions(in)
opts.Normalize(a.modulePrefix)
store := a.buildPackageStore()
node := &depNode{
Package: a.shortName(pkg.ImportPath, in.Group),
}
if in.Depth > 0 && depth >= in.Depth {
return node
}
pkgInfo, ok := store.packages[pkg.ImportPath]
if !ok {
return node
}
// Track visited packages to avoid cycles
if !a.visited[pkg.ImportPath] {
a.visited[pkg.ImportPath] = true
for _, dep := range pkgInfo.Imports {
depInfo, ok := store.packages[dep]
if !ok || !opts.ShouldInclude(depInfo) {
continue
}
if depPkg, ok := a.packages[dep]; ok {
childNode := a.buildDepNode(depPkg, in, depth+1)
node.Dependencies = append(node.Dependencies, childNode)
} else {
node.Dependencies = append(node.Dependencies, &depNode{
Package: a.shortName(dep, in.Group),
})
}
}
}
return node
}
// generateReverse generates reverse dependency output using new system.
func (a *analyzer) generateReverse(in Input) string {
opts := a.convertInputToFilterOptions(in)
opts.Normalize(a.modulePrefix)
store := a.buildPackageStore()
// Build reverse dependency map
reverseDeps := make(map[string][]string)
for pkgPath, pkgInfo := range store.packages {
for _, dep := range pkgInfo.Imports {
depInfo, ok := store.packages[dep]
if ok && opts.ShouldInclude(depInfo) {
reverseDeps[dep] = append(reverseDeps[dep], pkgPath)
}
}
}
var sb strings.Builder
targets := a.getSortedPackages()
for _, target := range targets {
deps := reverseDeps[target]
if len(deps) == 0 {
continue
}
sort.Strings(deps)
shortTarget := a.shortName(target, in.Group)
if shortTarget == "" {
continue
}
fmt.Fprintf(&sb, "%s (used by %d packages):\n", shortTarget, len(deps))
for i, dep := range deps {
isLast := i == len(deps)-1
connector := "├── "
if isLast {
connector = "└── "
}
sb.WriteString(connector + a.shortName(dep, in.Group) + "\n")
}
sb.WriteString("\n")
}
return sb.String()
}

File diff suppressed because it is too large Load Diff

View File

@ -1,255 +0,0 @@
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package cmddep
import (
"testing"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/test/gtest"
)
// Test data model creation and classification
func Test_PackageInfo_Creation(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
pkg := &PackageInfo{
ImportPath: "github.com/gogf/gf/v2/os/gfile",
ModulePath: "github.com/gogf/gf/v2",
Kind: KindInternal,
Tier: 2,
Imports: []string{"fmt", "os"},
IsStdLib: false,
IsModuleRoot: false,
}
t.Assert(pkg != nil, true)
t.Assert(pkg.Kind, KindInternal)
t.Assert(pkg.Tier, 2)
t.Assert(len(pkg.Imports), 2)
})
}
// Test FilterOptions normalization
func Test_FilterOptions_Normalize(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
opts := &FilterOptions{
IncludeInternal: false,
IncludeExternal: false,
IncludeStdLib: false,
}
opts.Normalize("github.com/gogf/gf/v2")
// After normalization, internal should be included by default
t.Assert(opts.IncludeInternal, true)
t.Assert(opts.IncludeExternal, false)
})
}
// Test ShouldInclude decision logic
func Test_FilterOptions_ShouldInclude(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Test internal package inclusion
opts := &FilterOptions{
IncludeInternal: true,
IncludeExternal: false,
IncludeStdLib: true,
}
internalPkg := &PackageInfo{
Kind: KindInternal,
IsStdLib: false,
}
externalPkg := &PackageInfo{
Kind: KindExternal,
IsStdLib: false,
}
stdlibPkg := &PackageInfo{
Kind: KindStdLib,
IsStdLib: true,
}
t.Assert(opts.ShouldInclude(internalPkg), true)
t.Assert(opts.ShouldInclude(externalPkg), false)
t.Assert(opts.ShouldInclude(stdlibPkg), true)
})
}
// Test TraversalContext Visit tracking
func Test_TraversalContext_Visit(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
ctx := &TraversalContext{
visited: make(map[string]bool),
}
// First visit should return false
t.Assert(ctx.Visit("pkg1"), false)
// Second visit should return true
t.Assert(ctx.Visit("pkg1"), true)
// New package should return false
t.Assert(ctx.Visit("pkg2"), false)
})
}
// Test guessModuleRoot function
func Test_GuessModuleRoot(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
tests := []struct {
input string
expect string
}{
{"github.com/gogf/gf", "github.com/gogf/gf"},
{"github.com/gogf/gf/v2", "github.com/gogf/gf/v2"},
{"github.com/gogf/gf/v2/os/gfile", "github.com/gogf/gf/v2"},
{"github.com/gogf/gf/v2/os", "github.com/gogf/gf/v2"},
}
for _, test := range tests {
result := guessModuleRoot(test.input)
t.AssertEQ(result, test.expect)
}
})
}
// Test input to filter options conversion
func Test_ConvertInputToFilterOptions(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
a := newAnalyzer()
input := Input{
Internal: true,
External: false,
NoStd: true,
Module: false,
Direct: false,
Depth: 3,
}
opts := a.convertInputToFilterOptions(input)
t.Assert(opts.IncludeInternal, true)
t.Assert(opts.IncludeExternal, false)
t.Assert(opts.IncludeStdLib, false) // NoStd=true means !IncludeStdLib
t.Assert(opts.Depth, 3)
})
}
func Test_Dep_Tree(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
ctx := gctx.New()
_, err := Dep.Index(ctx, Input{
Package: "./",
Format: "tree",
Depth: 1,
Internal: true,
NoStd: true,
})
t.AssertNil(err)
})
}
func Test_Dep_List(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
ctx := gctx.New()
_, err := Dep.Index(ctx, Input{
Package: "./",
Format: "list",
Depth: 1,
Internal: true,
NoStd: true,
})
t.AssertNil(err)
})
}
func Test_Dep_Mermaid(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
ctx := gctx.New()
_, err := Dep.Index(ctx, Input{
Package: "./",
Format: "mermaid",
Depth: 1,
Internal: true,
NoStd: true,
})
t.AssertNil(err)
})
}
func Test_Dep_Dot(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
ctx := gctx.New()
_, err := Dep.Index(ctx, Input{
Package: "./",
Format: "dot",
Depth: 1,
Internal: true,
NoStd: true,
})
t.AssertNil(err)
})
}
func Test_Dep_JSON(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
ctx := gctx.New()
_, err := Dep.Index(ctx, Input{
Package: "./",
Format: "json",
Depth: 1,
Internal: true,
NoStd: true,
})
t.AssertNil(err)
})
}
func Test_Dep_Reverse(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
ctx := gctx.New()
_, err := Dep.Index(ctx, Input{
Package: "./",
Format: "tree",
Depth: 1,
Internal: true,
NoStd: true,
Reverse: true,
})
t.AssertNil(err)
})
}
func Test_Dep_Group(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
ctx := gctx.New()
_, err := Dep.Index(ctx, Input{
Package: "./",
Format: "mermaid",
Depth: 1,
Internal: true,
NoStd: true,
Group: true,
})
t.AssertNil(err)
})
}
func Test_ModuleLevel_Direct(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
ctx := gctx.New()
// Test module level with direct only
in := Input{
Module: true,
Direct: true,
Format: "list",
}
t.Logf("Input.Module: %v, Input.Direct: %v", in.Module, in.Direct)
_, err := Dep.Index(ctx, in)
t.AssertNil(err)
})
}

View File

@ -1,946 +0,0 @@
// Main Application Module
let currentZoom = 1;
let currentView = 'graph';
let currentLayout = 'TD';
let selectedPackage = null;
let allPackages = [];
let isRemoteMode = false;
let currentRemoteModule = '';
let packageListMode = 'tree'; // 'flat' or 'tree'
let expandedNodes = new Set(); // Track expanded tree nodes
// Pan and Zoom state
let panX = 0;
let panY = 0;
let isPanning = false;
let startPanX = 0;
let startPanY = 0;
let zoomIndicatorTimeout = null;
const MIN_ZOOM = 0.1;
const MAX_ZOOM = 10;
const ZOOM_STEP = 0.1;
// Theme Management
const theme = {
current: 'light',
init() {
const savedTheme = localStorage.getItem('dep-viewer-theme');
if (savedTheme) {
this.current = savedTheme;
} else {
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
this.current = 'dark';
}
}
this.apply();
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (!localStorage.getItem('dep-viewer-theme')) {
this.current = e.matches ? 'dark' : 'light';
this.apply();
}
});
const toggleBtn = document.getElementById('themeToggle');
if (toggleBtn) {
toggleBtn.addEventListener('click', () => this.toggle());
}
},
toggle() {
this.current = this.current === 'dark' ? 'light' : 'dark';
localStorage.setItem('dep-viewer-theme', this.current);
this.apply();
},
apply() {
if (this.current === 'dark') {
document.body.setAttribute('data-theme', 'dark');
} else {
document.body.removeAttribute('data-theme');
}
mermaid.initialize({
startOnLoad: false,
theme: this.current === 'dark' ? 'dark' : 'default',
maxEdges: 2000, // Increase edge limit for large dependency graphs
flowchart: {
useMaxWidth: false,
htmlLabels: true,
curve: 'basis'
}
});
if (currentView === 'graph') {
refresh();
}
}
};
// Initialize mermaid
mermaid.initialize({
startOnLoad: false,
theme: 'default',
maxEdges: 2000, // Increase edge limit for large dependency graphs
flowchart: {
useMaxWidth: false,
htmlLabels: true,
curve: 'basis'
}
});
// Initialize application
async function init() {
theme.init();
initPanZoom();
initRemoteModuleInput();
const hasLocalModule = await loadModuleName();
if (hasLocalModule) {
await loadPackages();
await refresh();
}
}
// Initialize remote module input
function initRemoteModuleInput() {
const input = document.getElementById('remoteModuleInput');
if (input) {
// Fetch versions when input loses focus or Enter is pressed
input.addEventListener('blur', fetchVersions);
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
fetchVersions();
}
});
}
}
// Fetch versions for a module from Go proxy
async function fetchVersions() {
const input = document.getElementById('remoteModuleInput');
const versionSelect = document.getElementById('versionSelect');
const spinner = document.getElementById('loadingSpinner');
let modulePath = input.value.trim();
// Remove http:// or https:// prefix if present
modulePath = modulePath.replace(/^https?:\/\//, '');
input.value = modulePath;
if (!modulePath) {
versionSelect.disabled = true;
versionSelect.innerHTML = `<option value="">${i18n.t('selectVersion')}</option>`;
return;
}
spinner.classList.remove('hidden');
versionSelect.disabled = true;
try {
const response = await fetch('/api/versions?module=' + encodeURIComponent(modulePath));
const data = await response.json();
if (data.error) {
versionSelect.innerHTML = `<option value="">${i18n.t('errorFetchVersions')}</option>`;
} else if (data.versions && data.versions.length > 0) {
versionSelect.innerHTML = data.versions.map((v, i) => {
const label = i === 0 ? `${v} (${i18n.t('latestVersion')})` : v;
return `<option value="${v}">${label}</option>`;
}).join('');
versionSelect.disabled = false;
} else {
versionSelect.innerHTML = `<option value="">${i18n.t('noVersions')}</option>`;
}
} catch (e) {
console.error('Failed to fetch versions:', e);
versionSelect.innerHTML = `<option value="">${i18n.t('errorFetchVersions')}</option>`;
} finally {
spinner.classList.add('hidden');
}
}
// Analyze remote module
async function analyzeRemoteModule() {
const input = document.getElementById('remoteModuleInput');
const versionSelect = document.getElementById('versionSelect');
const spinner = document.getElementById('loadingSpinner');
const analyzeBtn = document.getElementById('analyzeBtn');
let modulePath = input.value.trim();
// Remove http:// or https:// prefix if present
modulePath = modulePath.replace(/^https?:\/\//, '');
input.value = modulePath;
const version = versionSelect.value;
if (!modulePath) {
alert('Please enter a module path');
return;
}
spinner.classList.remove('hidden');
analyzeBtn.disabled = true;
analyzeBtn.textContent = i18n.t('analyzing');
try {
const url = `/api/analyze?module=${encodeURIComponent(modulePath)}${version ? '&version=' + encodeURIComponent(version) : ''}`;
const response = await fetch(url);
const data = await response.json();
if (data.error) {
alert(data.error);
return;
}
// Switch to remote mode
isRemoteMode = true;
currentRemoteModule = modulePath + (version ? '@' + version : '');
document.getElementById('moduleName').textContent = currentRemoteModule;
// Clear selection and reload
selectedPackage = null;
await loadPackages();
await refresh();
} catch (e) {
console.error('Failed to analyze module:', e);
alert(i18n.t('errorAnalyze'));
} finally {
spinner.classList.add('hidden');
analyzeBtn.disabled = false;
analyzeBtn.textContent = i18n.t('analyze');
}
}
// Reset to local module
async function resetToLocal() {
const spinner = document.getElementById('loadingSpinner');
spinner.classList.remove('hidden');
try {
await fetch('/api/reset');
isRemoteMode = false;
currentRemoteModule = '';
document.getElementById('remoteModuleInput').value = '';
document.getElementById('versionSelect').innerHTML = `<option value="">${i18n.t('selectVersion')}</option>`;
document.getElementById('versionSelect').disabled = true;
selectedPackage = null;
await loadModuleName();
await loadPackages();
await refresh();
} catch (e) {
console.error('Failed to reset:', e);
} finally {
spinner.classList.add('hidden');
}
}
// Initialize pan and zoom functionality
function initPanZoom() {
const viewport = document.getElementById('graphView');
if (!viewport) return;
// Mouse wheel zoom
viewport.addEventListener('wheel', (e) => {
if (currentView !== 'graph') return;
e.preventDefault();
const rect = viewport.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
// Calculate zoom
const delta = e.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP;
const newZoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, currentZoom + delta * currentZoom));
if (newZoom !== currentZoom) {
// Zoom towards mouse position
const scale = newZoom / currentZoom;
panX = mouseX - (mouseX - panX) * scale;
panY = mouseY - (mouseY - panY) * scale;
currentZoom = newZoom;
applyTransform();
showZoomIndicator();
}
}, { passive: false });
// Pan with mouse drag
viewport.addEventListener('mousedown', (e) => {
if (currentView !== 'graph') return;
if (e.button !== 0) return; // Only left click
isPanning = true;
startPanX = e.clientX - panX;
startPanY = e.clientY - panY;
viewport.style.cursor = 'grabbing';
});
document.addEventListener('mousemove', (e) => {
if (!isPanning) return;
panX = e.clientX - startPanX;
panY = e.clientY - startPanY;
applyTransform();
});
document.addEventListener('mouseup', () => {
if (isPanning) {
isPanning = false;
const viewport = document.getElementById('graphViewport');
if (viewport) viewport.style.cursor = 'grab';
}
});
// Touch support for mobile
let lastTouchDistance = 0;
let lastTouchCenter = { x: 0, y: 0 };
viewport.addEventListener('touchstart', (e) => {
if (currentView !== 'graph') return;
if (e.touches.length === 1) {
isPanning = true;
startPanX = e.touches[0].clientX - panX;
startPanY = e.touches[0].clientY - panY;
} else if (e.touches.length === 2) {
isPanning = false;
lastTouchDistance = getTouchDistance(e.touches);
lastTouchCenter = getTouchCenter(e.touches);
}
}, { passive: true });
viewport.addEventListener('touchmove', (e) => {
if (currentView !== 'graph') return;
e.preventDefault();
if (e.touches.length === 1 && isPanning) {
panX = e.touches[0].clientX - startPanX;
panY = e.touches[0].clientY - startPanY;
applyTransform();
} else if (e.touches.length === 2) {
const distance = getTouchDistance(e.touches);
const center = getTouchCenter(e.touches);
if (lastTouchDistance > 0) {
const scale = distance / lastTouchDistance;
const newZoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, currentZoom * scale));
if (newZoom !== currentZoom) {
const rect = viewport.getBoundingClientRect();
const centerX = center.x - rect.left;
const centerY = center.y - rect.top;
const zoomScale = newZoom / currentZoom;
panX = centerX - (centerX - panX) * zoomScale;
panY = centerY - (centerY - panY) * zoomScale;
currentZoom = newZoom;
applyTransform();
showZoomIndicator();
}
}
lastTouchDistance = distance;
lastTouchCenter = center;
}
}, { passive: false });
viewport.addEventListener('touchend', () => {
isPanning = false;
lastTouchDistance = 0;
});
}
function getTouchDistance(touches) {
const dx = touches[0].clientX - touches[1].clientX;
const dy = touches[0].clientY - touches[1].clientY;
return Math.sqrt(dx * dx + dy * dy);
}
function getTouchCenter(touches) {
return {
x: (touches[0].clientX + touches[1].clientX) / 2,
y: (touches[0].clientY + touches[1].clientY) / 2
};
}
function applyTransform() {
const container = document.getElementById('mermaidContainer');
if (container) {
container.style.transform = `translate(${panX}px, ${panY}px) scale(${currentZoom})`;
}
}
function showZoomIndicator() {
const indicator = document.getElementById('zoomIndicator');
if (indicator) {
indicator.textContent = `${Math.round(currentZoom * 100)}%`;
indicator.classList.add('visible');
if (zoomIndicatorTimeout) {
clearTimeout(zoomIndicatorTimeout);
}
zoomIndicatorTimeout = setTimeout(() => {
indicator.classList.remove('visible');
}, 1500);
}
}
// Load module name from server
// Returns true if local module exists, false otherwise
async function loadModuleName() {
try {
const response = await fetch('/api/module');
const data = await response.json();
document.getElementById('moduleName').textContent = data.name || '';
// If no local module, set default value and auto-analyze
if (!data.name) {
const input = document.getElementById('remoteModuleInput');
if (input && !input.value) {
input.value = 'github.com/gogf/gf/v2';
// Fetch versions first, then auto-analyze
await fetchVersions();
analyzeRemoteModule();
}
return false;
}
return true;
} catch (e) {
console.error('Failed to load module name:', e);
return false;
}
}
// Load packages list
async function loadPackages() {
try {
const internal = document.getElementById('internal').checked;
const external = document.getElementById('external') ? document.getElementById('external').checked : false;
const moduleLevel = document.getElementById('moduleLevel') ? document.getElementById('moduleLevel').checked : false;
const directOnly = document.getElementById('directOnly') ? document.getElementById('directOnly').checked : false;
const response = await fetch(`/api/packages?internal=${internal}&external=${external}&module=${moduleLevel}&direct=${directOnly}`);
const data = await response.json();
// Handle new API response format with packages and statistics
if (data.packages && Array.isArray(data.packages)) {
allPackages = data.packages;
// Update statistics display if available
if (data.statistics) {
updateStatisticsDisplay(data.statistics);
}
} else if (Array.isArray(data)) {
// Fallback for old format
allPackages = data;
} else {
console.error('Unexpected API response format:', data);
allPackages = [];
}
document.getElementById('packageCount').textContent = allPackages.length;
renderPackageList(allPackages);
} catch (e) {
console.error('Failed to load packages:', e);
}
}
// Update statistics display
function updateStatisticsDisplay(statistics) {
if (statistics) {
document.getElementById('internalCount').textContent = statistics.internal || 0;
document.getElementById('externalCount').textContent = statistics.external || 0;
document.getElementById('stdlibCount').textContent = statistics.stdlib || 0;
// Update total count
const totalCount = statistics.total || (statistics.internal + statistics.external + statistics.stdlib);
document.getElementById('nodeCount').textContent = totalCount;
}
}
// Get package name from package object or string
function getPkgName(pkg) {
return typeof pkg === 'object' ? pkg.name : pkg;
}
// Set package list display mode
function setPackageListMode(mode) {
packageListMode = mode;
document.getElementById('modeFlat').classList.toggle('active', mode === 'flat');
document.getElementById('modeTree').classList.toggle('active', mode === 'tree');
const query = document.getElementById('searchInput').value.toLowerCase();
const filtered = query ? allPackages.filter(pkg => getPkgName(pkg).toLowerCase().includes(query)) : allPackages;
renderPackageList(filtered);
}
// Render package list in sidebar
function renderPackageList(packages) {
const list = document.getElementById('packageList');
if (packages.length === 0) {
list.innerHTML = '<div class="loading">' + i18n.t('noPackages') + '</div>';
return;
}
if (packageListMode === 'tree') {
renderPackageTree(packages, list);
} else {
renderPackageFlat(packages, list);
}
}
// Render flat package list
function renderPackageFlat(packages, container) {
container.innerHTML = packages.map(pkg => {
const name = getPkgName(pkg);
const isActive = name === selectedPackage ? ' active' : '';
const escaped = name.replace(/'/g, "\\'");
// Build stats display
let statsHtml = '';
if (typeof pkg === 'object') {
statsHtml = `<span class="pkg-stats-inline">
<span class="dep-count" title="${i18n.t('dependencies')}">→${pkg.depCount}</span>
<span class="used-count" title="${i18n.t('usedBy')}">←${pkg.usedByCount}</span>
</span>`;
}
return `<div class="package-item${isActive}" onclick="selectPackage('${escaped}')" title="${name}">
<span class="pkg-name-text">${name}</span>${statsHtml}
</div>`;
}).join('');
}
// Build tree structure from package paths
function buildPackageTree(packages) {
const root = { children: {}, packages: [] };
packages.forEach(pkg => {
const name = getPkgName(pkg);
const parts = name.split('/');
let current = root;
parts.forEach((part, index) => {
if (!current.children[part]) {
current.children[part] = { children: {}, packages: [], path: parts.slice(0, index + 1).join('/') };
}
current = current.children[part];
});
current.isPackage = true;
current.fullPath = name;
// Store stats if available
if (typeof pkg === 'object') {
current.depCount = pkg.depCount;
current.usedByCount = pkg.usedByCount;
}
});
return root;
}
// Render package tree
function renderPackageTree(packages, container) {
const tree = buildPackageTree(packages);
container.innerHTML = renderTreeNode(tree, '');
}
// Render a tree node recursively
function renderTreeNode(node, path) {
const children = Object.keys(node.children).sort();
if (children.length === 0) return '';
return children.map(name => {
const child = node.children[name];
const childPath = path ? `${path}/${name}` : name;
const hasChildren = Object.keys(child.children).length > 0;
const isExpanded = expandedNodes.has(childPath);
const isActive = child.fullPath === selectedPackage;
const isPackage = child.isPackage;
const toggleClass = hasChildren ? (isExpanded ? 'expanded' : '') : 'empty';
const activeClass = isActive ? ' active' : '';
const nameClass = isPackage ? ' package' : '';
const icon = isPackage ? '📦' : '📁';
// Build stats for packages
let statsHtml = '';
if (isPackage && child.depCount !== undefined) {
statsHtml = `<span class="pkg-stats-inline">
<span class="dep-count" title="${i18n.t('dependencies')}">→${child.depCount}</span>
<span class="used-count" title="${i18n.t('usedBy')}">←${child.usedByCount}</span>
</span>`;
}
let html = `
<div class="tree-node" data-path="${childPath}">
<div class="tree-node-header${activeClass}">
<span class="tree-node-toggle ${toggleClass}" onclick="handleToggleClick(event, '${childPath.replace(/'/g, "\\'")}', ${hasChildren})">▶</span>
<span class="tree-node-icon">${icon}</span>
<span class="tree-node-name${nameClass}" onclick="handleNameClick(event, '${childPath.replace(/'/g, "\\'")}', ${isPackage}, ${hasChildren})">${name}</span>
${statsHtml}
</div>`;
if (hasChildren) {
const childrenHtml = renderTreeNode(child, childPath);
html += `<div class="tree-node-children${isExpanded ? ' expanded' : ''}">${childrenHtml}</div>`;
}
html += '</div>';
return html;
}).join('');
}
// Handle toggle arrow click - expand/collapse
function handleToggleClick(event, path, hasChildren) {
event.stopPropagation();
if (hasChildren) {
toggleTreeNode(path);
}
}
// Handle name click - select package or toggle if folder
function handleNameClick(event, path, isPackage, hasChildren) {
event.stopPropagation();
if (isPackage) {
selectPackage(path);
} else if (hasChildren) {
toggleTreeNode(path);
}
}
// Toggle tree node expansion
function toggleTreeNode(path) {
if (expandedNodes.has(path)) {
expandedNodes.delete(path);
} else {
expandedNodes.add(path);
}
// Re-render with current filter
const query = document.getElementById('searchInput').value.toLowerCase();
const filtered = query ? allPackages.filter(pkg => getPkgName(pkg).toLowerCase().includes(query)) : allPackages;
renderPackageList(filtered);
}
// Filter packages by search query
function filterPackages() {
const query = document.getElementById('searchInput').value.toLowerCase();
const filtered = allPackages.filter(pkg => getPkgName(pkg).toLowerCase().includes(query));
renderPackageList(filtered);
}
// Select a package
async function selectPackage(pkg) {
selectedPackage = pkg;
// Update flat list items
document.querySelectorAll('.package-item').forEach(el => {
el.classList.toggle('active', el.textContent === pkg);
});
// Update tree items
document.querySelectorAll('.tree-node-header').forEach(el => {
const node = el.closest('.tree-node');
el.classList.toggle('active', node && node.dataset.path === pkg);
});
await refresh();
}
// Clear package selection
function clearSelection() {
selectedPackage = null;
document.querySelectorAll('.package-item').forEach(el => el.classList.remove('active'));
document.querySelectorAll('.tree-node-header').forEach(el => el.classList.remove('active'));
closePackageInfo();
refresh();
}
// Close package info sidebar
function closePackageInfo() {
document.getElementById('packageInfo').classList.remove('visible');
}
// Set layout direction
function setLayout(layout) {
currentLayout = layout;
document.getElementById('layoutTD').classList.toggle('active', layout === 'TD');
document.getElementById('layoutLR').classList.toggle('active', layout === 'LR');
refresh();
}
// Set view mode
function setView(view) {
currentView = view;
document.getElementById('btnGraph').classList.toggle('active', view === 'graph');
document.getElementById('btnTree').classList.toggle('active', view === 'tree');
document.getElementById('btnList').classList.toggle('active', view === 'list');
document.getElementById('zoomControls').classList.toggle('hidden', view !== 'graph');
document.getElementById('layoutGroup').classList.toggle('hidden', view !== 'graph');
refresh();
}
// Main refresh function
async function refresh() {
const depth = document.getElementById('depth').value;
const group = document.getElementById('group').checked;
const reverse = document.getElementById('reverse').checked;
const internal = document.getElementById('internal').checked;
const external = document.getElementById('external') ? document.getElementById('external').checked : false;
const moduleLevel = document.getElementById('moduleLevel') ? document.getElementById('moduleLevel').checked : false;
const directOnly = document.getElementById('directOnly') ? document.getElementById('directOnly').checked : false;
if (selectedPackage) {
await showPackageInfo(selectedPackage);
} else {
closePackageInfo();
}
if (currentView === 'graph') {
await refreshGraph(depth, group, reverse, internal, external, moduleLevel, directOnly);
} else if (currentView === 'tree') {
await refreshTree(depth, internal, external, moduleLevel, directOnly);
} else {
await refreshList(internal, external, moduleLevel, directOnly);
}
}
// Show package info panel
async function showPackageInfo(pkg) {
try {
const response = await fetch('/api/package?name=' + encodeURIComponent(pkg));
const info = await response.json();
const infoDiv = document.getElementById('packageInfo');
const contentDiv = document.getElementById('packageInfoContent');
infoDiv.classList.add('visible');
contentDiv.innerHTML = `
<span class="pkg-name" title="${info.name}">${info.name}</span>
<div class="pkg-stats">
<div class="pkg-stat">
<span class="pkg-stat-label">${i18n.t('dependencies')}:</span>
<span class="pkg-stat-value">${info.dependencies.length}</span>
</div>
<div class="pkg-stat">
<span class="pkg-stat-label">${i18n.t('usedBy')}:</span>
<span class="pkg-stat-value">${info.usedBy.length}</span>
</div>
</div>
`;
} catch (e) {
console.error('Failed to load package info:', e);
}
}
// Refresh graph view
async function refreshGraph(depth, group, reverse, internal, external, moduleLevel, directOnly) {
document.getElementById('graphView').classList.remove('hidden');
document.getElementById('textView').classList.add('hidden');
// Reset pan/zoom on refresh
currentZoom = 1;
panX = 0;
panY = 0;
applyTransform();
let url = `/api/graph?depth=${depth}&group=${group}&reverse=${reverse}&internal=${internal}`;
if (external !== undefined) {
url += `&external=${external}`;
}
if (moduleLevel !== undefined) {
url += `&module=${moduleLevel}`;
}
if (directOnly !== undefined) {
url += `&direct=${directOnly}`;
}
if (selectedPackage) {
url += '&package=' + encodeURIComponent(selectedPackage);
}
try {
const response = await fetch(url);
const data = await response.json();
document.getElementById('nodeCount').textContent = data.nodes.length;
document.getElementById('edgeCount').textContent = data.edges.length;
// Generate mermaid code
// Build node id to label map
const nodeLabels = {};
data.nodes.forEach(node => {
nodeLabels[node.id] = node.label;
});
// Use current layout direction
let mermaidCode = `graph ${currentLayout}\n`;
// First define all nodes with labels
data.nodes.forEach(node => {
mermaidCode += ` ${node.id}["${node.label}"]\n`;
});
// Then add edges
data.edges.forEach(edge => {
mermaidCode += ` ${edge.from} --> ${edge.to}\n`;
});
const container = document.getElementById('mermaidGraph');
container.innerHTML = mermaidCode;
container.removeAttribute('data-processed');
await mermaid.run({ nodes: [container] });
// Auto-fit if graph is large
setTimeout(() => {
autoFitGraph();
}, 100);
} catch (e) {
console.error('Failed to render graph:', e);
document.getElementById('mermaidGraph').innerHTML =
`<div class="loading">${i18n.t('renderError')}</div>`;
}
}
// Auto-fit graph to viewport
function autoFitGraph() {
const viewport = document.getElementById('graphView');
const svg = document.querySelector('.mermaid svg');
if (!viewport || !svg) return;
const viewportRect = viewport.getBoundingClientRect();
const svgRect = svg.getBoundingClientRect();
if (svgRect.width === 0 || svgRect.height === 0) return;
// Calculate scale to fit
const scaleX = (viewportRect.width - 40) / svgRect.width;
const scaleY = (viewportRect.height - 40) / svgRect.height;
const scale = Math.min(scaleX, scaleY, 1); // Don't zoom in beyond 100%
if (scale < 1) {
currentZoom = scale;
// Center the graph
panX = (viewportRect.width - svgRect.width * scale) / 2;
panY = 20;
applyTransform();
showZoomIndicator();
}
}
// Refresh tree view
async function refreshTree(depth, internal, external, moduleLevel, directOnly) {
document.getElementById('graphView').classList.add('hidden');
document.getElementById('textView').classList.remove('hidden');
let url = `/api/tree?depth=${depth}&internal=${internal}`;
if (external !== undefined) {
url += `&external=${external}`;
}
if (moduleLevel !== undefined) {
url += `&module=${moduleLevel}`;
}
if (directOnly !== undefined) {
url += `&direct=${directOnly}`;
}
if (selectedPackage) {
url += '&package=' + encodeURIComponent(selectedPackage);
}
try {
const response = await fetch(url);
const text = await response.text();
document.getElementById('textView').textContent = text;
const lines = text.split('\n').filter(l => l.trim());
document.getElementById('nodeCount').textContent = lines.length;
document.getElementById('edgeCount').textContent = '-';
} catch (e) {
console.error('Failed to load tree:', e);
}
}
// Refresh list view
async function refreshList(internal, external, moduleLevel, directOnly) {
document.getElementById('graphView').classList.add('hidden');
document.getElementById('textView').classList.remove('hidden');
let url = `/api/list?internal=${internal}`;
if (external !== undefined) {
url += `&external=${external}`;
}
if (moduleLevel !== undefined) {
url += `&module=${moduleLevel}`;
}
if (directOnly !== undefined) {
url += `&direct=${directOnly}`;
}
if (selectedPackage) {
url += '&package=' + encodeURIComponent(selectedPackage);
}
try {
const response = await fetch(url);
const text = await response.text();
document.getElementById('textView').textContent = text;
const lines = text.split('\n').filter(l => l.trim());
document.getElementById('nodeCount').textContent = lines.length;
document.getElementById('edgeCount').textContent = '-';
} catch (e) {
console.error('Failed to load list:', e);
}
}
// Zoom functions
function zoomIn() {
const viewport = document.getElementById('graphView');
if (!viewport) return;
const rect = viewport.getBoundingClientRect();
const centerX = rect.width / 2;
const centerY = rect.height / 2;
const newZoom = Math.min(MAX_ZOOM, currentZoom * 1.2);
const scale = newZoom / currentZoom;
panX = centerX - (centerX - panX) * scale;
panY = centerY - (centerY - panY) * scale;
currentZoom = newZoom;
applyTransform();
showZoomIndicator();
}
function zoomOut() {
const viewport = document.getElementById('graphView');
if (!viewport) return;
const rect = viewport.getBoundingClientRect();
const centerX = rect.width / 2;
const centerY = rect.height / 2;
const newZoom = Math.max(MIN_ZOOM, currentZoom / 1.2);
const scale = newZoom / currentZoom;
panX = centerX - (centerX - panX) * scale;
panY = centerY - (centerY - panY) * scale;
currentZoom = newZoom;
applyTransform();
showZoomIndicator();
}
function resetZoom() {
currentZoom = 1;
panX = 0;
panY = 0;
applyTransform();
showZoomIndicator();
}
function fitToScreen() {
autoFitGraph();
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', init);

View File

@ -1,228 +0,0 @@
// Internationalization (i18n) Module
const i18n = {
currentLang: 'en',
translations: {
en: {
title: 'Go Package Dependencies',
pageTitle: 'Package Dependency Viewer',
currentModule: 'Current:',
remoteModuleLabel: 'Analyze Module:',
remoteModulePlaceholder: 'e.g. github.com/gogf/gf/v2',
selectVersion: 'Version',
analyze: 'Analyze',
resetLocal: 'Reset',
fetchingVersions: 'Fetching...',
analyzing: 'Analyzing...',
viewLabel: 'View:',
viewGraph: 'Graph',
viewTree: 'Tree',
viewList: 'List',
depthLabel: 'Depth:',
depthUnlimited: 'Unlimited',
reverseLabel: 'Reverse (show who uses)',
groupLabel: 'Group by directory',
internalLabel: 'Internal only',
externalLabel: 'External packages',
moduleLevelLabel: 'Module level',
directOnlyLabel: 'Direct only',
statsInternal: 'Internal',
statsExternal: 'External',
statsStdlib: 'Stdlib',
layoutLabel: 'Layout:',
layoutTD: 'Top-Down',
layoutLR: 'Left-Right',
showAll: 'Show All',
packagesTitle: 'Packages',
searchPlaceholder: 'Search packages...',
flatMode: 'Flat list',
treeMode: 'Tree view',
statsPackages: 'Packages',
statsDependencies: 'Dependencies',
dependencies: 'Dependencies',
usedBy: 'Used by',
noPackages: 'No packages found',
renderError: 'Unable to render graph. Try reducing depth or selecting a specific package.',
packageNotFound: 'Package not found',
zoomIn: 'Zoom in',
zoomOut: 'Zoom out',
fitToScreen: 'Fit to screen',
resetZoom: 'Reset zoom',
dragToMove: 'Drag to move, scroll to zoom',
noVersions: 'No versions found',
latestVersion: 'Latest',
errorFetchVersions: 'Failed to fetch versions',
errorAnalyze: 'Failed to analyze module',
packageDetails: 'Package Details',
viewGraphTooltip: 'Visualize dependencies as a directed graph',
viewTreeTooltip: 'Show dependencies in a hierarchical tree structure',
viewListTooltip: 'Display dependencies as a text list',
depthTooltip: 'Limit dependency traversal depth',
reverseTooltip: 'Show reverse dependencies (who uses the selected package)',
groupTooltip: 'Group packages by directory structure',
internalTooltip: 'Show only internal packages from current module',
externalTooltip: 'Include external dependency packages',
moduleLevelTooltip: 'Show module-level dependencies (from go.mod)',
directOnlyTooltip: 'Show only direct dependencies (requires module level)',
layoutTDTooltip: 'Top-down layout for dependency graph',
layoutLRTooltip: 'Left-right layout for dependency graph',
showAllTooltip: 'Clear package selection and show all packages',
analyzeTooltip: 'Analyze remote Go module dependencies',
resetTooltip: 'Reset to local module analysis',
themeTooltip: 'Toggle light/dark theme',
langTooltip: 'Select language',
flatModeTooltip: 'Flat list view',
treeModeTooltip: 'Tree view',
zoomInTooltip: 'Zoom in',
zoomOutTooltip: 'Zoom out',
fitToScreenTooltip: 'Fit to screen',
resetZoomTooltip: 'Reset zoom',
remoteModuleTooltip: 'Enter a remote Go module path to analyze',
versionSelectTooltip: 'Select module version (optional)',
searchTooltip: 'Search packages by name'
},
zh: {
title: 'Go 包依赖分析',
pageTitle: '包依赖查看器',
currentModule: '当前模块:',
remoteModuleLabel: '分析模块:',
remoteModulePlaceholder: '例如 github.com/gogf/gf/v2',
selectVersion: '版本',
analyze: '分析',
resetLocal: '重置',
fetchingVersions: '获取中...',
analyzing: '分析中...',
viewLabel: '视图:',
viewGraph: '图表',
viewTree: '树形',
viewList: '列表',
depthLabel: '深度:',
depthUnlimited: '无限制',
reverseLabel: '反向依赖 (谁引用了它)',
groupLabel: '按目录分组',
internalLabel: '仅内部包',
externalLabel: '外部依赖包',
moduleLevelLabel: '模块级别',
directOnlyLabel: '仅直接依赖',
statsInternal: '内部',
statsExternal: '外部',
statsStdlib: '标准库',
layoutLabel: '布局:',
layoutTD: '从上到下',
layoutLR: '从左到右',
showAll: '显示全部',
packagesTitle: '包列表',
searchPlaceholder: '搜索包...',
flatMode: '平铺列表',
treeMode: '目录树',
statsPackages: '包数量',
statsDependencies: '依赖数',
dependencies: '依赖',
usedBy: '被引用',
noPackages: '未找到包',
renderError: '无法渲染图表请尝试减少深度或选择特定的包',
packageNotFound: '未找到包',
zoomIn: '放大',
zoomOut: '缩小',
fitToScreen: '适应屏幕',
resetZoom: '重置缩放',
dragToMove: '拖拽移动滚轮缩放',
noVersions: '未找到版本',
latestVersion: '最新版本',
errorFetchVersions: '获取版本失败',
errorAnalyze: '分析模块失败',
packageDetails: '包详情',
viewGraphTooltip: '以有向图形式可视化依赖关系',
viewTreeTooltip: '以层次树结构显示依赖关系',
viewListTooltip: '以文本列表形式显示依赖关系',
depthTooltip: '限制依赖遍历的深度',
reverseTooltip: '显示反向依赖谁引用了选中的包',
groupTooltip: '按目录结构分组显示包',
internalTooltip: '仅显示当前模块的内部包',
externalTooltip: '包含外部依赖包',
moduleLevelTooltip: '显示模块级别依赖来自go.mod',
directOnlyTooltip: '仅显示直接依赖需要启用模块级别',
layoutTDTooltip: '依赖图从上到下布局',
layoutLRTooltip: '依赖图从左到右布局',
showAllTooltip: '清除包选择显示所有包',
analyzeTooltip: '分析远程Go模块依赖',
resetTooltip: '重置为本地模块分析',
themeTooltip: '切换明暗主题',
langTooltip: '选择语言',
flatModeTooltip: '平铺列表视图',
treeModeTooltip: '树形视图',
zoomInTooltip: '放大',
zoomOutTooltip: '缩小',
fitToScreenTooltip: '适应屏幕',
resetZoomTooltip: '重置缩放',
remoteModuleTooltip: '输入远程Go模块路径进行分析',
versionSelectTooltip: '选择模块版本可选',
searchTooltip: '按名称搜索包'
}
},
init() {
// Load saved language preference
const savedLang = localStorage.getItem('dep-viewer-lang');
if (savedLang && this.translations[savedLang]) {
this.currentLang = savedLang;
} else {
// Detect browser language
const browserLang = navigator.language.toLowerCase();
if (browserLang.startsWith('zh')) {
this.currentLang = 'zh';
}
}
// Set select value
const langSelect = document.getElementById('langSelect');
if (langSelect) {
langSelect.value = this.currentLang;
langSelect.addEventListener('change', (e) => {
this.setLanguage(e.target.value);
});
}
this.applyTranslations();
},
setLanguage(lang) {
if (this.translations[lang]) {
this.currentLang = lang;
localStorage.setItem('dep-viewer-lang', lang);
this.applyTranslations();
}
},
t(key) {
return this.translations[this.currentLang][key] || this.translations['en'][key] || key;
},
applyTranslations() {
// Update elements with data-i18n attribute
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
el.textContent = this.t(key);
});
// Update placeholders
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
const key = el.getAttribute('data-i18n-placeholder');
el.placeholder = this.t(key);
});
// Update title attributes
document.querySelectorAll('[data-i18n-title]').forEach(el => {
const key = el.getAttribute('data-i18n-title');
el.title = this.t(key);
});
// Update page title
document.title = this.t('title');
}
};
// Initialize i18n when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
i18n.init();
});

View File

@ -1,161 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title data-i18n="title">Go Package Dependencies</title>
<link rel="stylesheet" href="/static/style.css">
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
</head>
<body>
<div class="header">
<div class="header-left">
<h1 data-i18n="pageTitle">Package Dependency Viewer</h1>
<div class="module-info">
<span data-i18n="currentModule">Current:</span>
<span class="module" id="moduleName"></span>
</div>
</div>
<div class="header-center">
<div class="remote-input-group">
<label for="remoteModuleInput" data-i18n="remoteModuleLabel">Module:</label>
<input type="text" id="remoteModuleInput" data-i18n-placeholder="remoteModulePlaceholder" placeholder="e.g. github.com/gogf/gf/v2" data-i18n-title="remoteModuleTooltip">
<select id="versionSelect" disabled data-i18n-title="versionSelectTooltip">
<option value="" data-i18n="selectVersion">Select version</option>
</select>
<button class="btn btn-sm" id="analyzeBtn" onclick="analyzeRemoteModule()" data-i18n="analyze" data-i18n-title="analyzeTooltip">Analyze</button>
<button class="btn btn-sm btn-secondary" id="resetBtn" onclick="resetToLocal()" data-i18n="resetLocal" data-i18n-title="resetTooltip">Reset</button>
<span class="loading-spinner hidden" id="loadingSpinner"></span>
</div>
</div>
<div class="header-right">
<div class="theme-switch">
<button class="icon-btn" id="themeToggle" data-i18n-title="themeTooltip">
<span class="icon-sun"></span>
<span class="icon-moon">🌙</span>
</button>
</div>
<div class="lang-switch">
<select id="langSelect" data-i18n-title="langTooltip">
<option value="en">English</option>
<option value="zh">中文</option>
</select>
</div>
</div>
</div>
<div class="controls">
<div class="control-group">
<label data-i18n="viewLabel">View:</label>
<div class="btn-group">
<button class="btn active" id="btnGraph" onclick="setView('graph')" data-i18n="viewGraph" data-i18n-title="viewGraphTooltip">Graph</button>
<button class="btn" id="btnTree" onclick="setView('tree')" data-i18n="viewTree" data-i18n-title="viewTreeTooltip">Tree</button>
<button class="btn" id="btnList" onclick="setView('list')" data-i18n="viewList" data-i18n-title="viewListTooltip">List</button>
</div>
</div>
<div class="control-group">
<label data-i18n="depthLabel">Depth:</label>
<select id="depth" onchange="refresh()" data-i18n-title="depthTooltip">
<option value="1">1</option>
<option value="2">2</option>
<option value="3" selected>3</option>
<option value="5">5</option>
<option value="0" data-i18n="depthUnlimited">Unlimited</option>
</select>
</div>
<div class="control-group">
<input type="checkbox" id="reverse" onchange="refresh()" data-i18n-title="reverseTooltip">
<label for="reverse" data-i18n="reverseLabel">Reverse (show who uses)</label>
</div>
<div class="control-group">
<input type="checkbox" id="group" onchange="refresh()" data-i18n-title="groupTooltip">
<label for="group" data-i18n="groupLabel">Group by directory</label>
</div>
<div class="control-group">
<input type="checkbox" id="internal" checked onchange="refresh()" data-i18n-title="internalTooltip">
<label for="internal" data-i18n="internalLabel">Internal only</label>
</div>
<div class="control-group">
<input type="checkbox" id="external" onchange="refresh()" data-i18n-title="externalTooltip">
<label for="external" data-i18n="externalLabel">External packages</label>
</div>
<div class="control-group">
<input type="checkbox" id="moduleLevel" onchange="refresh()" data-i18n-title="moduleLevelTooltip">
<label for="moduleLevel" data-i18n="moduleLevelLabel">Module level</label>
</div>
<div class="control-group">
<input type="checkbox" id="directOnly" onchange="refresh()" data-i18n-title="directOnlyTooltip">
<label for="directOnly" data-i18n="directOnlyLabel">Direct only</label>
</div>
<div class="control-group graph-only" id="layoutGroup">
<label data-i18n="layoutLabel">Layout:</label>
<div class="btn-group">
<button class="btn active" id="layoutTD" onclick="setLayout('TD')" data-i18n="layoutTD" data-i18n-title="layoutTDTooltip">Top-Down</button>
<button class="btn" id="layoutLR" onclick="setLayout('LR')" data-i18n="layoutLR" data-i18n-title="layoutLRTooltip">Left-Right</button>
</div>
</div>
<button class="btn btn-secondary" onclick="clearSelection()" data-i18n="showAll" data-i18n-title="showAllTooltip">Show All</button>
</div>
<div class="main-content">
<div class="sidebar">
<div class="sidebar-header">
<div class="sidebar-title">
<h3 data-i18n="packagesTitle">Packages</h3>
<span class="count" id="packageCount">0</span>
</div>
<div class="sidebar-mode">
<button class="mode-btn" id="modeFlat" onclick="setPackageListMode('flat')" data-i18n-title="flatModeTooltip"></button>
<button class="mode-btn active" id="modeTree" onclick="setPackageListMode('tree')" data-i18n-title="treeModeTooltip">🌲</button>
</div>
</div>
<div class="search-box">
<input type="text" id="searchInput" data-i18n-placeholder="searchPlaceholder" placeholder="Search packages..." oninput="filterPackages()" data-i18n-title="searchTooltip">
</div>
<div class="package-list" id="packageList"></div>
</div>
<div class="content-area">
<div class="stats">
<div class="stat-card">
<div class="value" id="nodeCount">0</div>
<div class="label" data-i18n="statsPackages">Packages</div>
</div>
<div class="stat-card">
<div class="value" id="edgeCount">0</div>
<div class="label" data-i18n="statsDependencies">Dependencies</div>
</div>
<div class="stat-card">
<div class="value" id="internalCount">0</div>
<div class="label" data-i18n="statsInternal">Internal</div>
</div>
<div class="stat-card">
<div class="value" id="externalCount">0</div>
<div class="label" data-i18n="statsExternal">External</div>
</div>
<div class="stat-card">
<div class="value" id="stdlibCount">0</div>
<div class="label" data-i18n="statsStdlib">Stdlib</div>
</div>
<div class="package-info" id="packageInfo">
<div class="package-info-content" id="packageInfoContent"></div>
</div>
</div>
<div class="graph-container" id="graphContainer">
<div id="graphView" class="graph-viewport">
<div class="mermaid-container" id="mermaidContainer">
<div class="mermaid" id="mermaidGraph"></div>
</div>
<div class="zoom-indicator" id="zoomIndicator">100%</div>
</div>
<div id="textView" class="text-output hidden"></div>
</div>
</div>
</div>
<div class="zoom-controls" id="zoomControls">
<button class="zoom-btn" onclick="zoomIn()" data-i18n-title="zoomInTooltip">+</button>
<button class="zoom-btn" onclick="zoomOut()" data-i18n-title="zoomOutTooltip"></button>
<button class="zoom-btn" onclick="fitToScreen()" data-i18n-title="fitToScreenTooltip"></button>
<button class="zoom-btn" onclick="resetZoom()" data-i18n-title="resetZoomTooltip"></button>
</div>
<script src="/static/i18n.js"></script>
<script src="/static/app.js"></script>
</body>
</html>

View File

@ -1,811 +0,0 @@
/* CSS Variables for Theming */
:root {
/* Light Theme (Default) */
--bg-primary: #f8fafc;
--bg-secondary: #ffffff;
--bg-tertiary: #f1f5f9;
--bg-hover: #e2e8f0;
--text-primary: #1e293b;
--text-secondary: #475569;
--text-muted: #94a3b8;
--border-color: #e2e8f0;
--accent-color: #3b82f6;
--accent-hover: #2563eb;
--accent-light: #dbeafe;
--success-color: #10b981;
--card-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
--input-bg: #ffffff;
--input-border: #cbd5e1;
}
[data-theme="dark"] {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--bg-hover: #475569;
--text-primary: #f1f5f9;
--text-secondary: #cbd5e1;
--text-muted: #64748b;
--border-color: #334155;
--accent-color: #60a5fa;
--accent-hover: #3b82f6;
--accent-light: #1e3a5f;
--success-color: #34d399;
--card-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
--input-bg: #1e293b;
--input-border: #475569;
}
/* Reset & Base */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
overflow: hidden;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
display: flex;
flex-direction: column;
transition: background-color 0.3s, color 0.3s;
}
/* Header */
.header {
background: var(--bg-secondary);
padding: 12px 24px;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
box-shadow: var(--card-shadow);
flex-shrink: 0;
}
.header-left {
flex-shrink: 0;
}
.header-left h1 {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
.header-left .module-info {
display: flex;
align-items: center;
gap: 6px;
color: var(--text-muted);
font-size: 12px;
margin-top: 2px;
}
.header-left .module {
color: var(--accent-color);
font-weight: 500;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Header Center - Remote Module Input */
.header-center {
flex: 1;
display: flex;
justify-content: center;
}
.remote-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.remote-input-group input[type="text"] {
width: 320px;
background: var(--input-bg);
border: 1px solid var(--input-border);
color: var(--text-primary);
padding: 6px 12px;
border-radius: 6px;
font-size: 13px;
transition: border-color 0.2s;
}
.remote-input-group input[type="text"]:focus {
outline: none;
border-color: var(--accent-color);
}
.remote-input-group select {
width: 160px;
background: var(--input-bg);
border: 1px solid var(--input-border);
color: var(--text-primary);
padding: 6px 10px;
border-radius: 6px;
font-size: 13px;
cursor: pointer;
}
.remote-input-group select:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-sm {
padding: 6px 12px;
font-size: 12px;
}
.loading-spinner {
width: 18px;
height: 18px;
border: 2px solid var(--border-color);
border-top-color: var(--accent-color);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.header-right {
display: flex;
align-items: center;
gap: 12px;
}
.icon-btn {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 8px 12px;
cursor: pointer;
font-size: 16px;
transition: all 0.2s;
}
.icon-btn:hover {
background: var(--bg-hover);
}
.icon-sun, .icon-moon {
display: none;
}
body:not([data-theme="dark"]) .icon-moon {
display: inline;
}
[data-theme="dark"] .icon-sun {
display: inline;
}
.lang-switch select {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
color: var(--text-primary);
padding: 8px 12px;
border-radius: 8px;
font-size: 13px;
cursor: pointer;
}
/* Controls */
.controls {
background: var(--bg-secondary);
padding: 12px 24px;
display: flex;
gap: 20px;
flex-wrap: wrap;
align-items: center;
border-bottom: 1px solid var(--border-color);
}
.control-group {
display: flex;
align-items: center;
gap: 8px;
}
.control-group label {
font-size: 13px;
color: var(--text-secondary);
font-weight: 500;
}
.control-group select {
background: var(--input-bg);
border: 1px solid var(--input-border);
color: var(--text-primary);
padding: 6px 12px;
border-radius: 6px;
font-size: 13px;
cursor: pointer;
}
.control-group input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: var(--accent-color);
}
/* Buttons */
.btn {
background: var(--accent-color);
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.2s;
}
.btn:hover {
background: var(--accent-hover);
}
.btn.active {
background: var(--accent-color);
box-shadow: 0 0 0 2px var(--accent-light);
}
.btn-secondary {
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.btn-secondary:hover {
background: var(--bg-hover);
}
.btn-group {
display: flex;
gap: 0;
}
.btn-group .btn {
border-radius: 0;
border-right: 1px solid rgba(255, 255, 255, 0.2);
}
.btn-group .btn:first-child {
border-radius: 6px 0 0 6px;
}
.btn-group .btn:last-child {
border-radius: 0 6px 6px 0;
border-right: none;
}
.btn-group .btn:not(.active) {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.btn-group .btn:not(.active):hover {
background: var(--bg-hover);
}
/* Main Content */
.main-content {
display: flex;
flex: 1;
overflow: hidden;
min-height: 0; /* Important for flex children to respect overflow */
}
/* Sidebar */
.sidebar {
width: 280px;
min-width: 280px;
background: var(--bg-secondary);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
overflow: hidden;
height: 100%; /* Ensure sidebar takes full height */
}
.sidebar-header {
padding: 10px 16px;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0; /* Prevent shrinking */
}
.sidebar-title {
display: flex;
align-items: center;
gap: 8px;
}
.sidebar-title h3 {
font-size: 14px;
font-weight: 600;
color: var(--text-secondary);
}
.sidebar-title .count {
background: var(--accent-color);
color: white;
padding: 2px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.sidebar-mode {
display: flex;
gap: 4px;
}
.mode-btn {
width: 28px;
height: 28px;
border: 1px solid var(--border-color);
background: var(--bg-tertiary);
border-radius: 6px;
cursor: pointer;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.mode-btn:hover {
background: var(--bg-hover);
}
.mode-btn.active {
background: var(--accent-color);
border-color: var(--accent-color);
color: white;
}
.search-box {
padding: 12px 16px;
border-bottom: 1px solid var(--border-color);
flex-shrink: 0; /* Prevent shrinking */
}
.search-box input {
width: 100%;
background: var(--input-bg);
border: 1px solid var(--input-border);
color: var(--text-primary);
padding: 10px 14px;
border-radius: 8px;
font-size: 13px;
transition: border-color 0.2s;
}
.search-box input:focus {
outline: none;
border-color: var(--accent-color);
}
.search-box input::placeholder {
color: var(--text-muted);
}
.package-list {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 8px 0;
min-height: 0; /* Important for flex overflow to work */
}
.package-item {
padding: 10px 16px;
cursor: pointer;
font-size: 13px;
color: var(--text-secondary);
border-left: 3px solid transparent;
transition: all 0.15s;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: flex;
justify-content: space-between;
align-items: center;
}
.package-item .pkg-name-text {
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
.pkg-stats-inline {
display: flex;
gap: 6px;
font-size: 11px;
margin-left: 8px;
flex-shrink: 0;
}
.pkg-stats-inline .dep-count {
color: var(--accent-color);
opacity: 0.8;
}
.pkg-stats-inline .used-count {
color: #10b981;
opacity: 0.8;
}
.package-item:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.package-item.active {
background: var(--accent-light);
border-left-color: var(--accent-color);
color: var(--accent-color);
font-weight: 500;
}
/* Tree View Styles */
.tree-node {
user-select: none;
}
.tree-node-header {
display: flex;
align-items: center;
padding: 6px 12px;
cursor: pointer;
font-size: 13px;
color: var(--text-secondary);
transition: all 0.15s;
}
.tree-node-header .pkg-stats-inline {
margin-left: auto;
padding-left: 8px;
}
.tree-node-header:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.tree-node-toggle {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 2px;
font-size: 10px;
color: var(--text-muted);
transition: transform 0.2s;
cursor: pointer;
border-radius: 4px;
}
.tree-node-toggle:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.tree-node-toggle.expanded {
transform: rotate(90deg);
}
.tree-node-toggle.empty {
visibility: hidden;
}
.tree-node-icon {
margin-right: 6px;
font-size: 12px;
}
.tree-node-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
padding: 2px 4px;
border-radius: 4px;
}
.tree-node-name:hover {
background: var(--bg-hover);
}
.tree-node-name.package {
color: var(--accent-color);
}
.tree-node-children {
padding-left: 16px;
display: none;
}
.tree-node-children.expanded {
display: block;
}
.tree-node-header.active {
background: var(--accent-light);
color: var(--accent-color);
}
.tree-node-header.active .tree-node-name {
font-weight: 500;
}
/* Content Area */
.content-area {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Stats */
.stats {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 24px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
}
.stat-card {
background: var(--bg-tertiary);
padding: 10px 20px;
border-radius: 10px;
text-align: center;
min-width: 90px;
}
.stat-card .value {
font-size: 20px;
font-weight: 700;
color: var(--accent-color);
}
.stat-card .label {
font-size: 11px;
color: var(--text-muted);
margin-top: 2px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Package Info - in stats bar */
.package-info {
display: none;
align-items: center;
gap: 16px;
background: var(--bg-tertiary);
padding: 8px 16px;
border-radius: 10px;
}
.package-info.visible {
display: flex;
}
.package-info-content {
display: flex;
align-items: center;
gap: 20px;
}
.package-info-content .pkg-name {
font-size: 14px;
font-weight: 600;
color: var(--accent-color);
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.package-info-content .pkg-stats {
display: flex;
gap: 16px;
}
.package-info-content .pkg-stat {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
}
.package-info-content .pkg-stat-label {
color: var(--text-muted);
}
.package-info-content .pkg-stat-value {
color: var(--text-primary);
font-weight: 600;
}
/* Graph Container */
.graph-container {
flex: 1;
padding: 24px;
overflow: hidden;
background: var(--bg-primary);
position: relative;
display: flex;
flex-direction: column;
min-height: 0; /* Important for flex overflow */
}
.graph-viewport {
flex: 1;
width: 100%;
overflow: hidden;
position: relative;
cursor: grab;
min-height: 0; /* Important for flex overflow */
}
.graph-viewport:active {
cursor: grabbing;
}
.mermaid-container {
display: inline-block;
min-width: 100%;
min-height: 100%;
transform-origin: 0 0;
transition: none;
}
.mermaid {
display: inline-block;
padding: 20px;
}
.mermaid svg {
display: block;
max-width: none !important;
height: auto;
}
.zoom-indicator {
position: absolute;
bottom: 16px;
left: 16px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
color: var(--text-secondary);
box-shadow: var(--card-shadow);
pointer-events: none;
opacity: 0;
transition: opacity 0.3s;
}
.zoom-indicator.visible {
opacity: 1;
}
.text-output {
flex: 1;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 20px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
font-size: 13px;
line-height: 1.7;
white-space: pre;
overflow: auto;
min-height: 0; /* Important for flex overflow */
color: var(--text-primary);
}
/* Zoom Controls */
.zoom-controls {
position: fixed;
bottom: 24px;
right: 24px;
display: flex;
gap: 8px;
}
.zoom-btn {
width: 40px;
height: 40px;
border-radius: 10px;
background: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid var(--border-color);
font-size: 18px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
box-shadow: var(--card-shadow);
}
.zoom-btn:hover {
background: var(--accent-color);
color: white;
border-color: var(--accent-color);
}
/* Utility Classes */
.hidden {
display: none !important;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
color: var(--text-muted);
font-size: 14px;
}
/* Scrollbar Styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-tertiary);
}
::-webkit-scrollbar-thumb {
background: var(--text-muted);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-secondary);
}
/* Responsive */
@media (max-width: 768px) {
.sidebar {
width: 220px;
}
.controls {
padding: 10px 16px;
gap: 12px;
}
.header {
padding: 12px 16px;
}
}

View File

@ -0,0 +1,308 @@
# 标签配置使用指南
## 功能概述
`gf gen tpl` 现在支持灵活的标签配置,可以选择性地为生成的结构体字段添加 `omitempty` 或其他自定义标签。
## 配置选项一览
| 选项 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `jsonOmitempty` | bool | false | 为所有字段添加 omitempty |
| `jsonOmitemptyAuto` | bool | false | 仅为可空字段自动添加 omitempty |
| `withOrmTag` | bool | true | 是否添加 orm 标签 |
| `descriptionTag` | bool | false | 是否添加 description 标签 |
| `noJsonTag` | bool | false | 是否禁用 JSON 标签 |
| `fieldMapping.tags` | map | - | 字段级自定义标签 |
## 配置方式
### 1. 全局开关 - `jsonOmitempty`
为所有字段的 JSON 标签添加 `omitempty`:
```yaml
gfcli:
gen:
tpl:
jsonOmitempty: true
```
**生成结果:**
```go
type User struct {
ID int `json:"id,omitempty" orm:"id" description:"用户ID"`
Name string `json:"name,omitempty" orm:"name" description:"用户名"`
Email string `json:"email,omitempty" orm:"email" description:"邮箱"`
}
```
---
### 2. 智能判断 - `jsonOmitemptyAuto` (推荐)
仅为可空字段自动添加 `omitempty`:
```yaml
gfcli:
gen:
tpl:
jsonOmitemptyAuto: true
```
**假设数据库表结构:**
```sql
CREATE TABLE user (
id INT NOT NULL,
name VARCHAR(50) NOT NULL,
email VARCHAR(100) NULL, -- 可空字段
age INT NULL -- 可空字段
);
```
**生成结果:**
```go
type User struct {
ID int `json:"id" orm:"id" description:"用户ID"`
Name string `json:"name" orm:"name" description:"用户名"`
Email string `json:"email,omitempty" orm:"email" description:"邮箱"` // 自动添加
Age int `json:"age,omitempty" orm:"age" description:"年龄"` // 自动添加
}
```
---
### 3. ORM 标签控制 - `withOrmTag`
控制是否添加 orm 标签 (默认启用):
```yaml
gfcli:
gen:
tpl:
withOrmTag: false # 不生成 orm 标签
```
**生成结果:**
```go
type User struct {
ID int `json:"id" description:"用户ID"` // 没有 orm 标签
Name string `json:"name" description:"用户名"` // 没有 orm 标签
Email string `json:"email" description:"邮箱"` // 没有 orm 标签
}
```
---
### 4. 字段级精确控制 - `fieldMapping`
针对特定字段自定义标签 (优先级最高):
```yaml
gfcli:
gen:
tpl:
fieldMapping:
user.password:
type: string
tags:
json: "-" # 不序列化
user.email:
type: string
tags:
json: "email,omitempty"
validate: "required,email"
binding: "required"
user.status:
type: int
tags:
json: "status,omitempty"
validate: "oneof=0 1 2"
example: "1"
```
**生成结果:**
```go
type User struct {
Password string `json:"-" orm:"password" description:"密码"`
Email string `binding:"required" json:"email,omitempty" validate:"required,email" description:"邮箱"`
Status int `example:"1" json:"status,omitempty" validate:"oneof=0 1 2" description:"状态"`
}
```
---
## 常见标签示例
### validate 标签 (gin validator)
```yaml
fieldMapping:
user.email:
tags:
validate: "required,email"
user.age:
tags:
validate: "gte=0,lte=150"
user.password:
tags:
validate: "required,min=8,max=32"
```
### binding 标签 (gin binding)
```yaml
fieldMapping:
user.name:
tags:
binding: "required"
user.email:
tags:
binding: "required,email"
```
### swagger 文档标签
```yaml
fieldMapping:
user.id:
tags:
example: "1"
description: "用户唯一标识"
user.status:
tags:
example: "1"
enums: "0,1,2"
```
### 多个自定义标签组合
```yaml
fieldMapping:
user.email:
type: string
tags:
json: "email,omitempty"
validate: "required,email"
binding: "required"
example: "user@example.com"
description: "用户邮箱地址"
```
---
## 配置优先级
标签配置的优先级从高到低:
1. **fieldMapping.tags** - 字段级自定义标签 (优先级最高)
2. **jsonOmitempty** - 全局 omitempty 开关
3. **jsonOmitemptyAuto** - 智能判断可空字段
4. **默认行为** - 不添加 omitempty
---
## 完整配置示例
```yaml
gfcli:
gen:
tpl:
link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test"
path: "./output"
tplPath: "./templates"
jsonCase: "CamelLower"
importPrefix: "github.com/example/project"
# 全局配置
jsonOmitemptyAuto: true # 可空字段自动添加 omitempty
withOrmTag: true # 添加 orm 标签 (默认)
descriptionTag: true # 添加 description 标签
# 类型映射
typeMapping:
decimal:
type: decimal.Decimal
import: github.com/shopspring/decimal
# 字段级配置
fieldMapping:
user.password:
type: string
tags:
json: "-"
user.email:
type: string
tags:
json: "email,omitempty"
validate: "required,email"
binding: "required"
order.total_amount:
type: decimal.Decimal
import: github.com/shopspring/decimal
tags:
json: "totalAmount,omitempty"
validate: "gt=0"
```
---
## 命令行使用
```bash
# 使用配置文件
gf gen tpl
# 命令行参数
gf gen tpl -tp ./templates -p ./output -ja -wo
# -ja: jsonOmitemptyAuto
# -wo: withOrmTag
# 组合使用
gf gen tpl -l "mysql:root:pass@tcp(127.0.0.1:3306)/db" -tp ./tpl -ja -c -wo
```
---
## 模板中使用
如果你需要在自定义模板中使用标签功能:
```go
// entity.tpl
type {{.table.NameCaseCamel}} struct { {{range $i,$v := .table.Fields}}
{{$v.NameCaseCamel}} {{$v.LocalType}} {{$v.BuildTags $.tagInput}} // {{$v.Comment}}{{end}}
}
```
或者分别使用单个标签方法:
```go
type {{.table.NameCaseCamel}} struct { {{range $i,$v := .table.Fields}}
{{$v.NameCaseCamel}} {{$v.LocalType}} `json:"{{$v.JsonTag $.tagInput.JsonOmitempty $.tagInput.JsonOmitemptyAuto}}" orm:"{{$v.OrmTag}}"` // {{$v.Comment}}{{end}}
}
```
---
## 注意事项
1. **字段名格式**: `fieldMapping` 中的 key 格式为 `表名.字段名`,使用数据库中的实际字段名 (非驼峰)
2. **标签顺序**: 自定义标签会按字母顺序排列,确保生成结果一致
3. **特殊字符**: 如果标签值包含双引号,会自动转义
4. **DO 文件**: DO 文件 (model/do) 只保留 description 标签,不包含 JSON/ORM 标签
5. **兼容性**: 与现有的 `typeMapping``fieldMapping` 完全兼容
6. **默认值**: `withOrmTag` 默认为 `true`,如果不需要 orm 标签,需要显式设置为 `false`

View File

@ -0,0 +1,106 @@
# 代码生成器设计文档
## 功能概述
基于数据库表结构通过自定义模板生成Go代码的工具。
## 功能设计
生成流程:
1. 读取数据库表结构
2. 解析出表结构信息,包括表名、表注释、字段列表
3. 根据规则裁切表数据,生成模板数据
4. 根据模板生成代码
## 命令参数设计
```shell
$ gf gen tpl -h
USAGE
gf gen tpl [OPTION]
OPTION
-p, --path directory path for generated files
-l, --link database configuration, the same as the ORM configuration of GoFrame
-t, --tables generate templates only for given tables, multiple table names separated with ','
-x, --tablesEx generate templates excluding given tables, multiple table names separated with ','
-g, --group specifying the configuration group name of database for generated ORM instance,
it's not necessary and the default value is "default"
-f, --prefix add prefix for all table of specified link/database tables
-r, --removePrefix remove specified prefix of the table, multiple prefix separated with ','
-rf, --removeFieldPrefix remove specified prefix of the field, multiple prefix separated with ','
-j, --jsonCase generated json tag case for model struct, cases are as follows:
| Case | Example |
|---------------- |--------------------|
| Camel | AnyKindOfString |
| CamelLower | anyKindOfString | default
| Snake | any_kind_of_string |
| SnakeScreaming | ANY_KIND_OF_STRING |
| SnakeFirstUpper | rgb_code_md5 |
| Kebab | any-kind-of-string |
| KebabScreaming | ANY-KIND-OF-STRING |
-i, --importPrefix custom import prefix for generated go files
-t1, --tplPath template file path for custom template
-s, --stdTime use time.Time from stdlib instead of gtime.Time for generated time/date fields of tables
-w, --withTime add created time for auto produced go files
-n, --gJsonSupport use gJsonSupport to use *gjson.Json instead of string for generated json fields of
tables
-v, --overwrite overwrite all template files
-c, --descriptionTag add comment to description tag for each field
-k, --noJsonTag no json tag will be added for each field
-m, --noModelComment no model comment will be added for each field
-a, --clear delete all generated template files that do not exist in database
-y, --typeMapping custom local type mapping for generated struct attributes relevant to fields of table
-fm, --fieldMapping custom local type mapping for generated struct attributes relevant to specific fields of
table
-h, --help more information about this command
EXAMPLE
gf gen tpl
gf gen tpl -l "mysql:root:12345678@tcp(127.0.0.1:3306)/test"
gf gen tpl -p ./template -g user-center -t user,user_detail,user_login
gf gen tpl -r user_
CONFIGURATION SUPPORT
Options are also supported by configuration file.
It's suggested using configuration file instead of command line arguments making producing.
The configuration node name is "gfcli.gen.tpl", which also supports multiple databases, for example(config.yaml):
gfcli:
gen:
tpl:
- link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test"
tables: "order,products"
jsonCase: "CamelLower"
- link: "mysql:root:12345678@tcp(127.0.0.1:3306)/primary"
path: "./my-app"
prefix: "primary_"
tables: "user, userDetail"
typeMapping:
decimal:
type: decimal.Decimal
import: github.com/shopspring/decimal
numeric:
type: string
fieldMapping:
table_name.field_name:
type: decimal.Decimal
import: github.com/shopspring/decimal
```
## 表结构信息
### 表信息
1. 表名
2. 表注释
3. 字段列表
### 字段信息
1. 字段名
2. 类型
3. 对应的 go 类型
4. 是否主键
5. 是否唯一键
6. 备注
7. 默认值
8. 是否自增

View File

@ -0,0 +1,82 @@
# gf gen tpl 标签配置示例
gfcli:
gen:
tpl:
# 数据库连接
link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test"
# 输出路径
path: "./output"
# 模板路径
tplPath: "./testdata"
# JSON 命名规则
jsonCase: "CamelLower"
# 导入路径前缀
importPrefix: "github.com/example/project"
# ===== 标签配置选项 =====
# 方式1: 全局为所有字段添加 omitempty
jsonOmitempty: false
# 方式2: 自动为可空字段添加 omitempty (推荐)
jsonOmitemptyAuto: true
# 是否添加 orm 标签 (默认: true)
withOrmTag: true
# 是否添加 description 标签
descriptionTag: true
# 是否禁用 JSON 标签
noJsonTag: false
# ===== 类型映射 =====
typeMapping:
decimal:
type: decimal.Decimal
import: github.com/shopspring/decimal
numeric:
type: string
# ===== 字段级配置 (最灵活) =====
fieldMapping:
# 表名.字段名 格式
user.password:
type: string
tags:
json: "-" # 不序列化密码字段
user.email:
type: string
tags:
json: "email,omitempty"
validate: "required,email"
binding: "required"
user.age:
type: int
tags:
json: "age"
validate: "gte=0,lte=150"
user.status:
type: int
tags:
json: "status,omitempty"
validate: "oneof=0 1 2"
example: "1"
# 自定义类型示例
order.total_amount:
type: decimal.Decimal
import: github.com/shopspring/decimal
tags:
json: "totalAmount,omitempty"
validate: "gt=0"

View File

@ -0,0 +1,27 @@
// =================================================================================
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
// =================================================================================
package dao
import (
"{{.table.PackageName}}/internal"
)
// internal{{.table.NameCaseCamel}}Dao is internal type for wrapping internal DAO implements.
type internal{{.table.NameCaseCamel}}Dao = *internal.{{.table.NameCaseCamel}}Dao
// {{.table.NameCaseCamelLower}}Dao is the data access object for table {{.table.Name}}.
// You can define custom methods on it to extend its functionality as you wish.
type {{.table.NameCaseCamelLower}}Dao struct {
internal{{.table.NameCaseCamel}}Dao
}
var (
// {{.table.NameCaseCamel}} is globally public accessible object for table {{.table.Name}} operations.
{{.table.NameCaseCamel}} = {{.table.NameCaseCamelLower}}Dao{
internal.New{{.table.NameCaseCamel}}Dao(),
}
)
// Fill with you ideas below.

View File

@ -0,0 +1,69 @@
package internal
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
// {{.table.NameCaseCamel}}Dao is the data access object for table {{.table.Name}}.
type {{.table.NameCaseCamel}}Dao struct {
table string // table is the underlying table name of the DAO.
group string // group is the database configuration group name of current DAO.
columns {{.table.NameCaseCamel}}Columns // columns contains all the column names of Table for convenient usage.
}
// {{.table.NameCaseCamel}}Columns defines and stores column names for table {{.table.Name}}.
type {{.table.NameCaseCamel}}Columns struct { {{range $i,$v := .table.Fields}}
{{$v.NameCaseCamel}} string // {{$v.Comment}}{{end}}
}
// {{.table.NameCaseCamelLower}}Columns holds the columns for table {{.table.Name}}.
var {{.table.NameCaseCamelLower}}Columns = {{.table.NameCaseCamel}}Columns{ {{range $i,$v := .table.Fields}}
{{$v.NameCaseCamel}}: "{{$v.NameJsonCase}}",{{end}}
}
// New{{.table.NameCaseCamel}}Dao creates and returns a new DAO object for table data access.
func New{{.table.NameCaseCamel}}Dao() *{{.table.NameCaseCamel}}Dao {
return &{{.table.NameCaseCamel}}Dao{
group: "test",
table: "{{.table.Name}}",
columns: {{.table.NameCaseCamelLower}}Columns,
}
}
// DB retrieves and returns the underlying raw database management object of current DAO.
func (dao *{{.table.NameCaseCamel}}Dao) DB() gdb.DB {
return g.DB(dao.group)
}
// Table returns the table name of current dao.
func (dao *{{.table.NameCaseCamel}}Dao) Table() string {
return dao.table
}
// Columns returns all column names of current dao.
func (dao *{{.table.NameCaseCamel}}Dao) Columns() {{.table.NameCaseCamel}}Columns {
return dao.columns
}
// Group returns the configuration group name of database of current dao.
func (dao *{{.table.NameCaseCamel}}Dao) Group() string {
return dao.group
}
// Ctx creates and returns the Model for current DAO, It automatically sets the context for current operation.
func (dao *{{.table.NameCaseCamel}}Dao) Ctx(ctx context.Context) *gdb.Model {
return dao.DB().Model(dao.table).Safe().Ctx(ctx)
}
// Transaction wraps the transaction logic using function f.
// It rollbacks the transaction and returns the error from function f if it returns non-nil error.
// It commits the transaction and returns nil if function f returns nil.
//
// Note that, you should not Commit or Rollback the transaction in function f
// as it is automatically handled by this function.
func (dao *{{.table.NameCaseCamel}}Dao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
return dao.Ctx(ctx).Transaction(ctx, f)
}

View File

@ -0,0 +1,16 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package do
import (
"github.com/gogf/gf/v2/frame/g"{{if .table.Imports}}{{range $k,$v := .table.Imports}}
"{{$k}}"{{end}}{{end}}
)
// {{.table.NameCaseCamel}} is the golang structure of table {{.table.Name}} for DAO operations like Where/Data.
type {{.table.NameCaseCamel}} struct {
g.Meta `orm:"table:{{.table.Name}}, do:true"`{{range $i,$v := .table.Fields}}
{{$v.NameCaseCamel}} interface{} // {{$v.Comment}}{{end}}
}

View File

@ -0,0 +1,14 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package entity
{{if .table.Imports}}
import ({{range $k,$v := .table.Imports}}
"{{$k}}"{{end}}
)
{{end}}
// {{.table.NameCaseCamel}} is the golang structure for table {{.table.Name}}.
type {{.table.NameCaseCamel}} struct { {{range $i,$v := .table.Fields}}
{{$v.NameCaseCamel}} {{$v.LocalType}} {{$v.BuildTags $.tagInput}} // {{$v.Comment}}{{end}}
}

View File

@ -0,0 +1,400 @@
package tpl
import (
"context"
"fmt"
"path/filepath"
"strings"
_ "github.com/gogf/gf/contrib/drivers/clickhouse/v2"
_ "github.com/gogf/gf/contrib/drivers/mssql/v2"
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
_ "github.com/gogf/gf/contrib/drivers/oracle/v2"
_ "github.com/gogf/gf/contrib/drivers/pgsql/v2"
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/os/gview"
"github.com/gogf/gf/v2/util/gtag"
"github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
"github.com/gogf/gf/cmd/gf/v2/internal/utility/utils"
)
const (
CGenTplConfig = `gfcli.gen.tpl`
CGenTplUsage = `gf gen tpl [OPTION]`
CGenTplBrief = `automatically generate template files`
CGenTplEg = `
gf gen tpl
gf gen tpl -l "mysql:root:12345678@tcp(127.0.0.1:3306)/test"
gf gen tpl -p ./model -g user-center -t user,user_detail,user_login
gf gen tpl -r user_
`
CGenTplAd = `
CONFIGURATION SUPPORT
Options are also supported by configuration file.
It's suggested using configuration file instead of command line arguments making producing.
The configuration node name is "gfcli.gen.dao", which also supports multiple databases, for example(config.yaml):
gfcli:
gen:
dao:
- link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test"
tables: "order,products"
jsonCase: "CamelLower"
- link: "mysql:root:12345678@tcp(127.0.0.1:3306)/primary"
path: "./my-app"
prefix: "primary_"
tables: "user, userDetail"
typeMapping:
decimal:
type: decimal.Decimal
import: github.com/shopspring/decimal
numeric:
type: string
fieldMapping:
table_name.field_name:
type: decimal.Decimal
import: github.com/shopspring/decimal
tags:
json: "field_name,omitempty"
validate: "required"
`
CGenTplBriefPath = `directory path for generated files`
CGenTplBriefLink = `database configuration, the same as the ORM configuration of GoFrame`
CGenTplBriefTables = `generate models only for given tables, multiple table names separated with ','`
CGenTplBriefTablesEx = `generate models excluding given tables, multiple table names separated with ','`
CGenTplBriefPrefix = `add prefix for all table of specified link/database tables`
CGenTplBriefRemovePrefix = `remove specified prefix of the table, multiple prefix separated with ','`
CGenTplBriefRemoveFieldPrefix = `remove specified prefix of the field, multiple prefix separated with ','`
CGenTplBriefStdTime = `use time.Time from stdlib instead of gtime.Time for generated time/date fields of tables`
CGenTplBriefWithTime = `add created time for auto produced go files`
CGenTplBriefGJsonSupport = `use gJsonSupport to use *gjson.Json instead of string for generated json fields of tables`
CGenTplBriefImportPrefix = `custom import prefix for generated go files`
CGenTplBriefDaoPath = `directory path for storing generated dao files under path`
CGenTplBriefDoPath = `directory path for storing generated do files under path`
CGenTplBriefEntityPath = `directory path for storing generated entity files under path`
CGenTplBriefOverwriteDao = `overwrite all dao files both inside/outside internal folder`
CGenTplBriefModelFile = `custom file name for storing generated model content`
CGenTplBriefModelFileForDao = `custom file name generating model for DAO operations like Where/Data. It's empty in default`
CGenTplBriefDescriptionTag = `add comment to description tag for each field`
CGenTplBriefNoJsonTag = `no json tag will be added for each field`
CGenTplBriefNoModelComment = `no model comment will be added for each field`
CGenTplBriefClear = `delete all generated go files that do not exist in database`
CGenTplBriefTypeMapping = `custom local type mapping for generated struct attributes relevant to fields of table`
CGenTplBriefFieldMapping = `custom local type mapping for generated struct attributes relevant to specific fields of table`
CGenTplBriefGroup = `
specifying the configuration group name of database for generated ORM instance,
it's not necessary and the default value is "default"
`
CGenTplBriefJsonCase = `
generated json tag case for model struct, cases are as follows:
| Case | Example |
|---------------- |--------------------|
| Camel | AnyKindOfString |
| CamelLower | anyKindOfString | default
| Snake | any_kind_of_string |
| SnakeScreaming | ANY_KIND_OF_STRING |
| SnakeFirstUpper | rgb_code_md5 |
| Kebab | any-kind-of-string |
| KebabScreaming | ANY-KIND-OF-STRING |
`
CGenTplBriefTplDaoIndexPath = `template file path for dao index file`
CGenTplBriefTplDaoInternalPath = `template file path for dao internal file`
CGenTplBriefTplDaoDoPathPath = `template file path for dao do file`
CGenTplBriefTplDaoEntityPath = `template file path for dao entity file`
CGenTplBriefJsonOmitempty = `add omitempty to all json tags`
CGenTplBriefJsonOmitemptyAuto = `automatically add omitempty to json tags for nullable fields`
CGenTplBriefWithOrmTag = `add orm tag for entity fields`
)
func init() {
gtag.Sets(g.MapStrStr{
`CGenTplConfig`: CGenTplConfig,
`CGenTplUsage`: CGenTplUsage,
`CGenTplBrief`: CGenTplBrief,
`CGenTplEg`: CGenTplEg,
`CGenTplAd`: CGenTplAd,
`CGenTplBriefPath`: CGenTplBriefPath,
`CGenTplBriefLink`: CGenTplBriefLink,
`CGenTplBriefTables`: CGenTplBriefTables,
`CGenTplBriefTablesEx`: CGenTplBriefTablesEx,
`CGenTplBriefPrefix`: CGenTplBriefPrefix,
`CGenTplBriefRemovePrefix`: CGenTplBriefRemovePrefix,
`CGenTplBriefRemoveFieldPrefix`: CGenTplBriefRemoveFieldPrefix,
`CGenTplBriefStdTime`: CGenTplBriefStdTime,
`CGenTplBriefWithTime`: CGenTplBriefWithTime,
`CGenTplBriefDaoPath`: CGenTplBriefDaoPath,
`CGenTplBriefDoPath`: CGenTplBriefDoPath,
`CGenTplBriefEntityPath`: CGenTplBriefEntityPath,
`CGenTplBriefGJsonSupport`: CGenTplBriefGJsonSupport,
`CGenTplBriefImportPrefix`: CGenTplBriefImportPrefix,
`CGenTplBriefOverwriteDao`: CGenTplBriefOverwriteDao,
`CGenTplBriefModelFile`: CGenTplBriefModelFile,
`CGenTplBriefModelFileForDao`: CGenTplBriefModelFileForDao,
`CGenTplBriefDescriptionTag`: CGenTplBriefDescriptionTag,
`CGenTplBriefNoJsonTag`: CGenTplBriefNoJsonTag,
`CGenTplBriefNoModelComment`: CGenTplBriefNoModelComment,
`CGenTplBriefClear`: CGenTplBriefClear,
`CGenTplBriefTypeMapping`: CGenTplBriefTypeMapping,
`CGenTplBriefFieldMapping`: CGenTplBriefFieldMapping,
`CGenTplBriefGroup`: CGenTplBriefGroup,
`CGenTplBriefJsonCase`: CGenTplBriefJsonCase,
`CGenTplBriefTplDaoIndexPath`: CGenTplBriefTplDaoIndexPath,
`CGenTplBriefTplDaoInternalPath`: CGenTplBriefTplDaoInternalPath,
`CGenTplBriefTplDaoDoPathPath`: CGenTplBriefTplDaoDoPathPath,
`CGenTplBriefTplDaoEntityPath`: CGenTplBriefTplDaoEntityPath,
`CGenTplBriefJsonOmitempty`: CGenTplBriefJsonOmitempty,
`CGenTplBriefJsonOmitemptyAuto`: CGenTplBriefJsonOmitemptyAuto,
`CGenTplBriefWithOrmTag`: CGenTplBriefWithOrmTag,
})
}
type (
CGenTpl struct{}
CGenTplInput struct {
g.Meta `name:"tpl" config:"{CGenTplConfig}" usage:"{CGenTplUsage}" brief:"{CGenTplBrief}" eg:"{CGenTplEg}" ad:"{CGenTplAd}"`
Path string `name:"path" short:"p" brief:"{CGenTplBriefPath}" d:"./output"`
TplPath string `name:"tplPath" short:"tp" brief:"模板目录路径"`
Link string `name:"link" short:"l" brief:"{CGenTplBriefLink}"`
Tables string `name:"tables" short:"t" brief:"{CGenTplBriefTables}"`
TablesEx string `name:"tablesEx" short:"x" brief:"{CGenTplBriefTablesEx}"`
Group string `name:"group" short:"g" brief:"{CGenTplBriefGroup}" d:"default"`
Prefix string `name:"prefix" short:"f" brief:"{CGenTplBriefPrefix}"`
RemovePrefix string `name:"removePrefix" short:"r" brief:"{CGenTplBriefRemovePrefix}"`
RemoveFieldPrefix string `name:"removeFieldPrefix" short:"rf" brief:"{CGenTplBriefRemoveFieldPrefix}"`
JsonCase string `name:"jsonCase" short:"j" brief:"{CGenTplBriefJsonCase}" d:"CamelLower"`
ImportPrefix string `name:"importPrefix" short:"i" brief:"{CGenTplBriefImportPrefix}"`
// 新增过滤参数
TableNamePattern string `name:"tableNamePattern" short:"tn" brief:"表名匹配模式,支持通配符"`
// DaoPath string `name:"daoPath" short:"d" brief:"{CGenTplBriefDaoPath}" d:"dao"`
// DoPath string `name:"doPath" short:"o" brief:"{CGenTplBriefDoPath}" d:"model/do"`
// EntityPath string `name:"entityPath" short:"e" brief:"{CGenTplBriefEntityPath}" d:"model/entity"`
// TplDaoIndexPath string `name:"tplDaoIndexPath" short:"t1" brief:"{CGenTplBriefTplDaoIndexPath}"`
// TplDaoInternalPath string `name:"tplDaoInternalPath" short:"t2" brief:"{CGenTplBriefTplDaoInternalPath}"`
// TplDaoDoPath string `name:"tplDaoDoPath" short:"t3" brief:"{CGenTplBriefTplDaoDoPathPath}"`
// TplDaoEntityPath string `name:"tplDaoEntityPath" short:"t4" brief:"{CGenTplBriefTplDaoEntityPath}"`
StdTime bool `name:"stdTime" short:"s" brief:"{CGenTplBriefStdTime}" orphan:"true"`
WithTime bool `name:"withTime" short:"w" brief:"{CGenTplBriefWithTime}" orphan:"true"`
GJsonSupport bool `name:"gJsonSupport" short:"n" brief:"{CGenTplBriefGJsonSupport}" orphan:"true"`
OverwriteDao bool `name:"overwriteDao" short:"v" brief:"{CGenTplBriefOverwriteDao}" orphan:"true"`
DescriptionTag bool `name:"descriptionTag" short:"c" brief:"{CGenTplBriefDescriptionTag}" orphan:"true"`
NoJsonTag bool `name:"noJsonTag" short:"k" brief:"{CGenTplBriefNoJsonTag}" orphan:"true"`
NoModelComment bool `name:"noModelComment" short:"m" brief:"{CGenTplBriefNoModelComment}" orphan:"true"`
Clear bool `name:"clear" short:"a" brief:"{CGenTplBriefClear}" orphan:"true"`
JsonOmitempty bool `name:"jsonOmitempty" short:"jo" brief:"{CGenTplBriefJsonOmitempty}" orphan:"true"`
JsonOmitemptyAuto bool `name:"jsonOmitemptyAuto" short:"ja" brief:"{CGenTplBriefJsonOmitemptyAuto}" orphan:"true"`
WithOrmTag bool `name:"withOrmTag" short:"wo" brief:"{CGenTplBriefWithOrmTag}" orphan:"false" d:"false"`
TypeMapping map[string]CustomAttributeType `name:"typeMapping" short:"y" brief:"{CGenTplBriefTypeMapping}" orphan:"true"`
FieldMapping map[string]CustomAttributeType `name:"fieldMapping" short:"fm" brief:"{CGenTplBriefFieldMapping}" orphan:"true"`
}
CGenTplOutput struct{}
CustomAttributeType struct {
Type string `brief:"custom attribute type name"`
Import string `brief:"custom import for this type"`
Tags map[string]string `brief:"custom tags for this field, e.g. json, validate, binding"`
}
)
var (
defaultTypeMapping = map[string]CustomAttributeType{
"decimal": {
Type: "float64",
},
"money": {
Type: "float64",
},
"numeric": {
Type: "float64",
},
"smallmoney": {
Type: "float64",
},
}
)
type (
DBFieldTypeName = string
)
// TplObj description
type TplObj struct {
ctx context.Context
in CGenTplInput
db gdb.DB
TplPathAbs string
}
// NewTpl description
//
// createTime: 2025-01-25 16:36:43
func NewTpl(ctx context.Context, in CGenTplInput) (*TplObj, error) {
db, err := in.GetDB()
if err != nil {
return nil, err
}
return &TplObj{
ctx: ctx,
in: in,
db: db,
TplPathAbs: gfile.Abs(in.TplPath),
}, nil
}
func (t *TplObj) ShowParams() {
mlog.Debug("tplPath:", t.in.TplPath)
mlog.Debug("output:", t.in.Path)
}
func (t *TplObj) Format() {
utils.GoFmt(t.in.Path)
}
// GetTplFileList description
//
// createTime: 2025-01-25 16:43:06
func (t *TplObj) GetTplFileList() ([]string, error) {
tplList, err := gfile.ScanDirFile(t.TplPathAbs, "*.tpl", true)
if err != nil {
return nil, err
}
return tplList, nil
}
func (c CGenTpl) Tpl(ctx context.Context, in CGenTplInput) (out *CGenTplOutput, err error) {
if in.TplPath == "" {
return nil, gerror.New("tplPath is required")
}
// Merge default typeMapping to input typeMapping
if in.TypeMapping == nil {
in.TypeMapping = defaultTypeMapping
} else {
for key, typeMapping := range defaultTypeMapping {
if _, ok := in.TypeMapping[key]; !ok {
in.TypeMapping[key] = typeMapping
}
}
}
// Clear old files
if in.Clear {
if err := gfile.Remove(in.Path); err != nil {
return nil, gerror.Wrapf(err, "clear output path failed")
}
}
// Create output directory
if !gfile.Exists(in.Path) {
if err := gfile.Mkdir(in.Path); err != nil {
return nil, gerror.Wrapf(err, "create output directory failed")
}
}
tplObj, err := NewTpl(ctx, in)
if err != nil {
return nil, err
}
tplList, err := tplObj.GetTplFileList()
if err != nil {
panic(err)
}
fmt.Println(tplList)
fmt.Printf("%#v\n", Table{})
fmt.Printf("%#v\n", TableField{})
tables, err := tplObj.GetTables()
if err != nil {
return nil, err
}
view := gview.New()
for _, table := range tables {
// Create tag input for this table
tagInput := TagBuildInput{
NoJsonTag: in.NoJsonTag,
JsonOmitempty: in.JsonOmitempty,
JsonOmitemptyAuto: in.JsonOmitemptyAuto,
WithOrmTag: in.WithOrmTag,
DescriptionTag: in.DescriptionTag,
}
tplData := g.Map{
"table": table,
"tables": tables,
"tagInput": tagInput,
}
fmt.Println(table.FieldsJsonStr(in.JsonCase))
for _, tpl := range tplList {
mlog.Print("generating template file:", tpl)
// 相对路径
relativePath := strings.TrimPrefix(gfile.Dir(tpl), tplObj.TplPathAbs)
mlog.Print("relativePath:", relativePath)
table.PackageName = filepath.ToSlash(filepath.Join(in.ImportPrefix, relativePath))
filePath := filepath.Join(relativePath, table.FileName())
mlog.Print("generating table filePath:", filePath)
res, err := view.Parse(ctx, tpl, tplData)
if err != nil {
mlog.Fatal(err)
}
fmt.Println(len(res), err)
err = tplObj.SaveFile(ctx, filePath, res)
if err != nil {
panic(err)
}
}
}
// Format generated files
tplObj.Format()
mlog.Print("template files generated successfully!")
return &CGenTplOutput{}, nil
}
// SaveFile description
//
// createTime: 2025-01-25 17:05:25
func (t *TplObj) SaveFile(ctx context.Context, path, content string) error {
mlog.Print("saving file:", path)
path = filepath.Join(t.in.Path, path)
mlog.Print("saving file:", path)
path = filepath.FromSlash(path)
mlog.Print("saving file:", path)
if err := gfile.PutContents(path, content); err != nil {
return err
}
return nil
}
// GetDB description
//
// createTime: 2025-01-24 16:58:46
func (in CGenTplInput) GetDB() (db gdb.DB, err error) {
// It uses user passed database configuration.
if in.Link != "" {
var tempGroup = gtime.TimestampNanoStr()
gdb.AddConfigNode(tempGroup, gdb.ConfigNode{
Link: in.Link,
})
if db, err = gdb.Instance(tempGroup); err != nil {
mlog.Fatalf(`database initialization failed: %+v`, err)
}
} else {
db = g.DB(in.Group)
}
if db == nil {
mlog.Fatal(`database initialization failed, may be invalid database configuration`)
}
return
}

View File

@ -0,0 +1,256 @@
package tpl
import (
"context"
"fmt"
"sort"
"strings"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
)
// TableField description
type TableField struct {
gdb.TableField
LocalType string
JsonCase string
CustomTags map[string]string // 自定义标签
}
type TableFields []*TableField
// Len returns the length of TableFields slice
func (s TableFields) Len() int { return len(s) }
// Swap swaps the elements with indexes i and j
func (s TableFields) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// Less reports whether the element with index i should sort before the element with index j
func (s TableFields) Less(i, j int) bool {
return strings.Compare(s[i].Name, s[j].Name) < 0
}
// Input description
type Input struct {
StdTime bool
GJsonSupport bool
TypeMapping map[string]CustomAttributeType
FieldMapping map[string]CustomAttributeType
}
// GetLocalTypeName description
//
// createTime: 2023-10-25 15:43:06
//
// author: hailaz
func (field *TableField) GetLocalTypeName(ctx context.Context, db gdb.DB, in Input) (appendImport string) {
var (
err error
localTypeName gdb.LocalType
localTypeNameStr string
)
if in.TypeMapping != nil && len(in.TypeMapping) > 0 {
var (
tryTypeName string
)
tryTypeMatch, _ := gregex.MatchString(`(.+?)\((.+)\)`, field.Type)
if len(tryTypeMatch) == 3 {
tryTypeName = gstr.Trim(tryTypeMatch[1])
} else {
tryTypeName = gstr.Split(field.Type, " ")[0]
}
if tryTypeName != "" {
if typeMapping, ok := in.TypeMapping[strings.ToLower(tryTypeName)]; ok {
localTypeNameStr = typeMapping.Type
appendImport = typeMapping.Import
}
}
}
if localTypeNameStr == "" {
localTypeName, err = db.CheckLocalTypeForField(ctx, field.Type, nil)
if err != nil {
panic(err)
}
localTypeNameStr = string(localTypeName)
switch localTypeName {
case gdb.LocalTypeDate, gdb.LocalTypeDatetime:
if in.StdTime {
localTypeNameStr = "time.Time"
} else {
localTypeNameStr = "*gtime.Time"
appendImport = "github.com/gogf/gf/v2/os/gtime"
}
case gdb.LocalTypeInt64Bytes:
localTypeNameStr = "int64"
case gdb.LocalTypeUint64Bytes:
localTypeNameStr = "uint64"
// Special type handle.
case gdb.LocalTypeJson, gdb.LocalTypeJsonb:
if in.GJsonSupport {
localTypeNameStr = "*gjson.Json"
appendImport = "github.com/gogf/gf/v2/encoding/gjson"
} else {
localTypeNameStr = "string"
}
}
}
// Check field-specific mapping (overrides type mapping)
if len(in.FieldMapping) > 0 {
fieldKey := field.Name
if typeMapping, ok := in.FieldMapping[fieldKey]; ok {
localTypeNameStr = typeMapping.Type
if typeMapping.Import != "" {
appendImport = typeMapping.Import
}
}
}
field.LocalType = localTypeNameStr
return
}
// NameJsonCase description
//
// createTime: 2025-01-25 15:27:01
func (f *TableField) NameJsonCase() string {
return gstr.CaseConvert(f.Name, gstr.CaseTypeMatch(f.JsonCase))
}
// NameCaseConvert 字段名转换
func (f *TableField) NameCaseConvert(caseName string) string {
return gstr.CaseConvert(f.Name, gstr.CaseTypeMatch(caseName))
}
// NameCaseCamel returns the field name in camel case format
func (f *TableField) NameCaseCamel() string {
return gstr.CaseCamel(f.Name)
}
// NameCaseCamelLower returns the field name in lower camel case format
func (f *TableField) NameCaseCamelLower() string {
return gstr.CaseCamelLower(f.Name)
}
// NameCaseSnake returns the field name in snake case format
func (f *TableField) NameCaseSnake() string {
return gstr.CaseSnake(f.Name)
}
// NameCaseKebabScreaming returns the field name in screaming kebab case format
func (f *TableField) NameCaseKebabScreaming() string {
return gstr.CaseKebabScreaming(f.Name)
}
// IsNullable returns whether the field is nullable
func (f *TableField) IsNullable() bool {
return f.Null
}
// JsonTag generates json tag for the field
func (f *TableField) JsonTag(omitempty bool, omitemptyAuto bool) string {
if f.CustomTags != nil {
if jsonTag, ok := f.CustomTags["json"]; ok {
return jsonTag
}
}
name := f.NameJsonCase()
if omitempty || (omitemptyAuto && f.IsNullable()) {
return name + ",omitempty"
}
return name
}
// OrmTag generates orm tag for the field
func (f *TableField) OrmTag() string {
if f.CustomTags != nil {
if ormTag, ok := f.CustomTags["orm"]; ok {
return ormTag
}
}
return f.Name
}
// DescriptionTag generates description tag for the field
func (f *TableField) DescriptionTag() string {
if f.CustomTags != nil {
if descTag, ok := f.CustomTags["description"]; ok {
return descTag
}
}
// 转义双引号
comment := strings.ReplaceAll(f.Comment, `"`, `\"`)
return comment
}
// CustomTag returns custom tag value by name
func (f *TableField) CustomTag(name string) string {
if f.CustomTags == nil {
return ""
}
return f.CustomTags[name]
}
// BuildTags builds all tags for the field
func (f *TableField) BuildTags(in TagBuildInput) string {
var tags []string
// JSON tag
if !in.NoJsonTag {
jsonValue := f.JsonTag(in.JsonOmitempty, in.JsonOmitemptyAuto)
tags = append(tags, fmt.Sprintf(`json:"%s"`, jsonValue))
}
// ORM tag
if in.WithOrmTag {
ormValue := f.OrmTag()
tags = append(tags, fmt.Sprintf(`orm:"%s"`, ormValue))
}
// Description tag
if in.DescriptionTag {
descValue := f.DescriptionTag()
tags = append(tags, fmt.Sprintf(`description:"%s"`, descValue))
}
// Custom tags from CustomTags map
if f.CustomTags != nil {
// 按字母顺序遍历,确保输出稳定
var keys []string
for k := range f.CustomTags {
// 跳过已处理的标准标签
if k == "json" || k == "orm" || k == "description" {
continue
}
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
v := f.CustomTags[k]
tags = append(tags, fmt.Sprintf(`%s:"%s"`, k, v))
}
}
if len(tags) == 0 {
return ""
}
return "`" + strings.Join(tags, " ") + "`"
}
// TagBuildInput for building tags
type TagBuildInput struct {
NoJsonTag bool
JsonOmitempty bool
JsonOmitemptyAuto bool
WithOrmTag bool
DescriptionTag bool
}

View File

@ -0,0 +1,273 @@
package tpl
import (
"context"
"encoding/json"
"fmt"
"regexp"
"sort"
"strings"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/text/gstr"
)
// Table description
type Table struct {
Name string // 表名
OutputName string // 输出表名,用于生成文件名
OutputNameCase string // 输出表名的命名规则
PackageName string
db gdb.DB
Fields TableFields
FieldsSource map[string]*gdb.TableField
Imports map[string]struct{}
}
type Tables []*Table
// NewTable description
//
// createTime: 2023-12-11 16:17:33
//
// author: hailaz
func NewTable(t *TplObj, tableName string) (*Table, error) {
fields, err := t.db.TableFields(t.ctx, tableName)
if err != nil {
return nil, err
}
table := Table{
Name: tableName,
OutputName: t.TableOutputName(tableName),
FieldsSource: fields,
db: t.db,
Imports: make(map[string]struct{}),
}
table.toTableFields(t.in)
return &table, nil
}
// Name description
//
// createTime: 2023-10-23 16:17:30
//
// author: hailaz
func (t *Table) Show() string {
return fmt.Sprintf("table name is %s", t.Name)
}
// NameCase description
func (t *Table) NameCase() string {
return gstr.CaseConvert(t.Name, gstr.CaseTypeMatch(t.OutputNameCase))
}
// NameCaseCamel description
func (t *Table) NameCaseCamel() string {
return gstr.CaseCamel(t.Name)
}
// NameCaseCamelLower description
func (t *Table) NameCaseCamelLower() string {
return gstr.CaseCamelLower(t.Name)
}
// NameCaseSnake description
func (t *Table) NameCaseSnake() string {
return gstr.CaseSnake(t.Name)
}
// NameCaseKebabScreaming description
func (t *Table) NameCaseKebabScreaming() string {
return gstr.CaseKebabScreaming(t.Name)
}
// FileName description
func (t *Table) FileName() string {
return gstr.CaseConvert(t.OutputName, gstr.CaseTypeMatch(t.OutputNameCase)) + ".go"
}
// toTableFields description
//
// createTime: 2023-10-23 17:22:40
//
// author: hailaz
func (t *Table) toTableFields(in CGenTplInput) {
if len(t.Fields) > 0 {
return
}
t.Fields = make(TableFields, len(t.FieldsSource))
for _, v := range t.FieldsSource {
field := &TableField{
TableField: *v,
JsonCase: in.JsonCase,
CustomTags: make(map[string]string),
}
// 设置字段类型
appendImport := field.GetLocalTypeName(context.Background(), t.db, Input{
TypeMapping: in.TypeMapping,
FieldMapping: in.FieldMapping,
StdTime: in.StdTime,
GJsonSupport: in.GJsonSupport,
})
if appendImport != "" {
t.Imports[appendImport] = struct{}{}
}
// 从 FieldMapping 中提取自定义标签
if in.FieldMapping != nil {
if fieldMapping, ok := in.FieldMapping[v.Name]; ok {
if fieldMapping.Tags != nil {
for tagName, tagValue := range fieldMapping.Tags {
field.CustomTags[tagName] = tagValue
}
}
}
}
t.Fields[v.Index] = field
}
}
// SortFields 字段排序
//
// createTime: 2023-10-23 17:18:22
//
// author: hailaz
func (t *Table) SortFields(isReverse bool) {
if isReverse {
sort.Sort(sort.Reverse(t.Fields))
} else {
sort.Sort(t.Fields)
}
}
// FieldsJsonStr 表字段json字符串
//
// createTime: 2023-10-23 17:29:39
//
// author: hailaz
func (t *Table) FieldsJsonStr(caseName string) string {
mapStr := make(map[string]interface{}, len(t.Fields))
for _, v := range t.Fields {
mapStr[v.NameCaseConvert(caseName)] = v.Default
}
b, err := json.MarshalIndent(mapStr, "", " ")
if err != nil {
return ""
}
return string(b)
}
// TagInput holds input for tag generation
type TagInput struct {
in CGenTplInput
}
// GetTagInput returns TagInput for template usage
func (t *Table) GetTagInput(in CGenTplInput) TagInput {
return TagInput{in: in}
}
// GetTables 获取数据库表结构信息
func (t *TplObj) GetTables() (Tables, error) {
nameList, err := t.db.Tables(t.ctx)
if err != nil {
return nil, err
}
// 过滤表名
nameList = filterTablesByName(nameList, t.in.TableNamePattern)
// 根据Tables参数过滤
nameList = filterTablesByInclude(nameList, t.in.Tables)
// 根据TablesEx参数过滤
nameList = filterTablesByExclude(nameList, t.in.TablesEx)
tables := make(Tables, 0, len(nameList))
for _, v := range nameList {
t, err := NewTable(t, v)
if err != nil {
continue
}
t.SortFields(true)
tables = append(tables, t)
}
return tables, nil
}
// TableOutputName description
//
// createTime: 2025-01-25 17:20:46
func (t *TplObj) TableOutputName(name string) string {
if t.in.Prefix != "" {
name = t.in.Prefix + name
}
if t.in.RemovePrefix != "" {
name = strings.TrimPrefix(name, t.in.RemovePrefix)
}
return name
}
// 新增过滤函数
func filterTablesByName(tables []string, pattern string) []string {
if pattern == "" {
return tables
}
var result []string
re, err := regexp.Compile(pattern)
if err != nil {
return tables
}
for _, table := range tables {
if re.MatchString(table) {
result = append(result, table)
}
}
return result
}
// 根据包含表名过滤
func filterTablesByInclude(tables []string, include string) []string {
if include == "" {
return tables
}
includeTables := strings.Split(include, ",")
result := make([]string, 0, len(includeTables))
for _, table := range tables {
for _, includeTable := range includeTables {
if table == includeTable {
result = append(result, table)
break
}
}
}
return result
}
// 根据排除表名过滤
func filterTablesByExclude(tables []string, exclude string) []string {
if exclude == "" {
return tables
}
excludeTables := strings.Split(exclude, ",")
result := make([]string, 0, len(tables))
for _, table := range tables {
exclude := false
for _, excludeTable := range excludeTables {
if table == excludeTable {
exclude = true
break
}
}
if !exclude {
result = append(result, table)
}
}
return result
}

View File

@ -0,0 +1,25 @@
package tpl_test
import (
"context"
"fmt"
"testing"
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/gen/tpl"
)
func TestTpl(t *testing.T) {
c := tpl.CGenTpl{}
t.Log(c)
out, err := c.Tpl(context.Background(), tpl.CGenTplInput{
Path: "./output",
TplPath: "./testdata",
Link: fmt.Sprintf("mysql:root:%s@tcp(127.0.0.1:3306)/focus?loc=Local&parseTime=true", "root123"),
Tables: "gf_user",
ImportPrefix: "github.com/gogf/gf/cmd/gf/v2/internal/cmd/gen/tpl/output",
})
if err != nil {
t.Error(err)
}
t.Log(out)
}

View File

@ -9,7 +9,6 @@ package gendao
import (
"context"
"fmt"
"sort"
"strings"
"github.com/olekukonko/tablewriter"
@ -188,27 +187,7 @@ func doGenDaoForArray(ctx context.Context, index int, in CGenDaoInput) {
var tableNames []string
if in.Tables != "" {
inputTables := gstr.SplitAndTrim(in.Tables, ",")
// Check if any table pattern contains wildcard characters.
// https://github.com/gogf/gf/issues/4629
var hasPattern bool
for _, t := range inputTables {
if containsWildcard(t) {
hasPattern = true
break
}
}
if hasPattern {
// Fetch all tables first, then filter by patterns.
allTables, err := db.Tables(context.TODO())
if err != nil {
mlog.Fatalf("fetching tables failed: %+v", err)
}
tableNames = filterTablesByPatterns(allTables, inputTables)
} else {
// Use exact table names as before.
tableNames = inputTables
}
tableNames = gstr.SplitAndTrim(in.Tables, ",")
} else {
tableNames, err = db.Tables(context.TODO())
if err != nil {
@ -219,11 +198,22 @@ func doGenDaoForArray(ctx context.Context, index int, in CGenDaoInput) {
if in.TablesEx != "" {
array := garray.NewStrArrayFrom(tableNames)
for _, p := range gstr.SplitAndTrim(in.TablesEx, ",") {
if containsWildcard(p) {
// Use exact match with ^ and $ anchors for consistency with tables pattern.
regPattern := "^" + patternToRegex(p) + "$"
if gstr.Contains(p, "*") || gstr.Contains(p, "?") {
p = gstr.ReplaceByMap(p, map[string]string{
"\r": "",
"\n": "",
})
p = gstr.ReplaceByMap(p, map[string]string{
"*": "\r",
"?": "\n",
})
p = gregex.Quote(p)
p = gstr.ReplaceByMap(p, map[string]string{
"\r": ".*",
"\n": ".",
})
for _, v := range array.Clone().Slice() {
if gregex.IsMatchString(regPattern, v) {
if gregex.IsMatchString(p, v) {
array.RemoveValue(v)
}
}
@ -250,22 +240,13 @@ func doGenDaoForArray(ctx context.Context, index int, in CGenDaoInput) {
newTableNames = make([]string, len(tableNames))
shardingNewTableSet = gset.NewStrSet()
)
// Sort sharding patterns by length descending, so that longer (more specific) patterns
// are matched first. This prevents shorter patterns like "a_?" from incorrectly matching
// tables that should match longer patterns like "a_b_?" or "a_c_?".
// https://github.com/gogf/gf/issues/4603
sortedShardingPatterns := make([]string, len(in.ShardingPattern))
copy(sortedShardingPatterns, in.ShardingPattern)
sort.Slice(sortedShardingPatterns, func(i, j int) bool {
return len(sortedShardingPatterns[i]) > len(sortedShardingPatterns[j])
})
for i, tableName := range tableNames {
newTableName := tableName
for _, v := range removePrefixArray {
newTableName = gstr.TrimLeftStr(newTableName, v, 1)
}
if len(sortedShardingPatterns) > 0 {
for _, pattern := range sortedShardingPatterns {
if len(in.ShardingPattern) > 0 {
for _, pattern := range in.ShardingPattern {
var (
match []string
regPattern = gstr.Replace(pattern, "?", `(.+)`)
@ -281,11 +262,10 @@ func doGenDaoForArray(ctx context.Context, index int, in CGenDaoInput) {
newTableName = gstr.Trim(newTableName, `_.-`)
if shardingNewTableSet.Contains(newTableName) {
tableNames[i] = ""
break
continue
}
// Add prefix to sharding table name, if not, the isSharding check would not match.
shardingNewTableSet.Add(in.Prefix + newTableName)
break
}
}
newTableName = in.Prefix + newTableName
@ -431,61 +411,3 @@ func getTemplateFromPathOrDefault(filePath string, def string) string {
}
return def
}
// containsWildcard checks if the pattern contains wildcard characters (* or ?).
func containsWildcard(pattern string) bool {
return gstr.Contains(pattern, "*") || gstr.Contains(pattern, "?")
}
// patternToRegex converts a wildcard pattern to a regex pattern.
// Wildcard characters: * matches any characters, ? matches single character.
func patternToRegex(pattern string) string {
pattern = gstr.ReplaceByMap(pattern, map[string]string{
"\r": "",
"\n": "",
})
pattern = gstr.ReplaceByMap(pattern, map[string]string{
"*": "\r",
"?": "\n",
})
pattern = gregex.Quote(pattern)
pattern = gstr.ReplaceByMap(pattern, map[string]string{
"\r": ".*",
"\n": ".",
})
return pattern
}
// filterTablesByPatterns filters tables by given patterns.
// Patterns support wildcard characters: * matches any characters, ? matches single character.
// https://github.com/gogf/gf/issues/4629
func filterTablesByPatterns(allTables []string, patterns []string) []string {
var result []string
matched := make(map[string]bool)
allTablesSet := make(map[string]bool)
for _, t := range allTables {
allTablesSet[t] = true
}
for _, p := range patterns {
if containsWildcard(p) {
regPattern := "^" + patternToRegex(p) + "$"
for _, table := range allTables {
if !matched[table] && gregex.IsMatchString(regPattern, table) {
result = append(result, table)
matched[table] = true
}
}
} else {
// Exact table name, use direct string comparison.
if !allTablesSet[p] {
mlog.Printf(`table "%s" does not exist, skipped`, p)
continue
}
if !matched[p] {
result = append(result, p)
matched[p] = true
}
}
}
return result
}

View File

@ -1,182 +0,0 @@
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gendao
import (
"testing"
"github.com/gogf/gf/v2/test/gtest"
)
// Test containsWildcard function.
func Test_containsWildcard(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
t.Assert(containsWildcard("trade_*"), true)
t.Assert(containsWildcard("user_?"), true)
t.Assert(containsWildcard("*"), true)
t.Assert(containsWildcard("?"), true)
t.Assert(containsWildcard("trade_order"), false)
t.Assert(containsWildcard(""), false)
})
}
// Test patternToRegex function.
func Test_patternToRegex(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// * should become .*
t.Assert(patternToRegex("trade_*"), "trade_.*")
// ? should become .
t.Assert(patternToRegex("user_???"), "user_...")
// Mixed
t.Assert(patternToRegex("*_order_?"), ".*_order_.")
// No wildcards - should escape special regex chars
t.Assert(patternToRegex("trade_order"), "trade_order")
// Just *
t.Assert(patternToRegex("*"), ".*")
})
}
// Test filterTablesByPatterns with * wildcard.
func Test_filterTablesByPatterns_Star(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
allTables := []string{"trade_order", "trade_item", "user_info", "user_log", "config"}
// Single pattern with *
result := filterTablesByPatterns(allTables, []string{"trade_*"})
t.Assert(len(result), 2)
t.AssertIN("trade_order", result)
t.AssertIN("trade_item", result)
// Multiple patterns with *
result = filterTablesByPatterns(allTables, []string{"trade_*", "user_*"})
t.Assert(len(result), 4)
t.AssertIN("trade_order", result)
t.AssertIN("trade_item", result)
t.AssertIN("user_info", result)
t.AssertIN("user_log", result)
})
}
// Test filterTablesByPatterns with ? wildcard.
func Test_filterTablesByPatterns_Question(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
allTables := []string{"trade_order", "trade_item", "user_info", "user_log", "config"}
// ? matches single character: user_log (3 chars) but not user_info (4 chars)
result := filterTablesByPatterns(allTables, []string{"user_???"})
t.Assert(len(result), 1)
t.AssertIN("user_log", result)
t.AssertNI("user_info", result)
// user_???? should match user_info (4 chars)
result = filterTablesByPatterns(allTables, []string{"user_????"})
t.Assert(len(result), 1)
t.AssertIN("user_info", result)
t.AssertNI("user_log", result)
})
}
// Test filterTablesByPatterns with mixed patterns and exact names.
func Test_filterTablesByPatterns_Mixed(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
allTables := []string{"trade_order", "trade_item", "user_info", "user_log", "config"}
// Pattern + exact name
result := filterTablesByPatterns(allTables, []string{"trade_*", "config"})
t.Assert(len(result), 3)
t.AssertIN("trade_order", result)
t.AssertIN("trade_item", result)
t.AssertIN("config", result)
t.AssertNI("user_info", result)
t.AssertNI("user_log", result)
})
}
// Test filterTablesByPatterns with exact names only (backward compatibility).
func Test_filterTablesByPatterns_ExactNames(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
allTables := []string{"trade_order", "trade_item", "user_info", "user_log", "config"}
// Exact names only
result := filterTablesByPatterns(allTables, []string{"trade_order", "config"})
t.Assert(len(result), 2)
t.AssertIN("trade_order", result)
t.AssertIN("config", result)
t.AssertNI("trade_item", result)
})
}
// Test filterTablesByPatterns - no duplicates when table matches multiple patterns.
func Test_filterTablesByPatterns_NoDuplicates(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
allTables := []string{"trade_order", "trade_item", "user_info"}
// trade_order matches both patterns, should only appear once
result := filterTablesByPatterns(allTables, []string{"trade_*", "trade_order"})
t.Assert(len(result), 2) // trade_order, trade_item
// Count occurrences of trade_order
count := 0
for _, v := range result {
if v == "trade_order" {
count++
}
}
t.Assert(count, 1) // No duplicates
})
}
// Test filterTablesByPatterns - pattern matches nothing.
func Test_filterTablesByPatterns_NoMatch(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
allTables := []string{"trade_order", "trade_item", "user_info"}
// Pattern that matches nothing
result := filterTablesByPatterns(allTables, []string{"nonexistent_*"})
t.Assert(len(result), 0)
})
}
// Test filterTablesByPatterns - empty input.
func Test_filterTablesByPatterns_Empty(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
allTables := []string{"trade_order", "trade_item"}
// Empty patterns
result := filterTablesByPatterns(allTables, []string{})
t.Assert(len(result), 0)
// Empty tables
result = filterTablesByPatterns([]string{}, []string{"trade_*"})
t.Assert(len(result), 0)
})
}
// Test filterTablesByPatterns - "*" matches all tables.
func Test_filterTablesByPatterns_MatchAll(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
allTables := []string{"trade_order", "trade_item", "user_info", "user_log", "config"}
// "*" should match all tables
result := filterTablesByPatterns(allTables, []string{"*"})
t.Assert(len(result), 5)
})
}
// Test filterTablesByPatterns - non-existent exact table name should be skipped.
func Test_filterTablesByPatterns_NonExistent(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
allTables := []string{"trade_order", "trade_item", "user_info"}
// Mix of existing and non-existing tables
result := filterTablesByPatterns(allTables, []string{"trade_order", "nonexistent", "user_info"})
t.Assert(len(result), 2)
t.AssertIN("trade_order", result)
t.AssertIN("user_info", result)
t.AssertNI("nonexistent", result)
})
}

View File

@ -55,13 +55,6 @@ func (c CGenEnums) Enums(ctx context.Context, in CGenEnumsInput) (out *CGenEnums
if realPath == "" {
mlog.Fatalf(`source folder path "%s" does not exist`, in.Src)
}
// Convert output path to absolute before Chdir, so it remains correct after directory change.
// See: https://github.com/gogf/gf/issues/4387
outputPath := gfile.Abs(in.Path)
originPwd := gfile.Pwd()
defer gfile.Chdir(originPwd)
err = gfile.Chdir(realPath)
if err != nil {
mlog.Fatal(err)
@ -79,14 +72,14 @@ func (c CGenEnums) Enums(ctx context.Context, in CGenEnumsInput) (out *CGenEnums
p := NewEnumsParser(in.Prefixes)
p.ParsePackages(pkgs)
var enumsContent = gstr.ReplaceByMap(consts.TemplateGenEnums, g.MapStrStr{
"{PackageName}": gfile.Basename(gfile.Dir(outputPath)),
"{PackageName}": gfile.Basename(gfile.Dir(in.Path)),
"{EnumsJson}": "`" + p.Export() + "`",
})
enumsContent = gstr.Trim(enumsContent)
if err = gfile.PutContents(outputPath, enumsContent); err != nil {
if err = gfile.PutContents(in.Path, enumsContent); err != nil {
return
}
mlog.Printf(`generated enums go file: %s`, outputPath)
mlog.Printf(`generated enums go file: %s`, in.Path)
mlog.Print("done!")
return
}

View File

@ -1,368 +0,0 @@
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package genenums
import (
"go/constant"
"path/filepath"
"testing"
"golang.org/x/tools/go/packages"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/test/gtest"
"github.com/gogf/gf/v2/util/guid"
)
func Test_NewEnumsParser(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Test creating parser without prefixes
p := NewEnumsParser(nil)
t.AssertNE(p, nil)
t.Assert(len(p.enums), 0)
t.Assert(len(p.prefixes), 0)
t.AssertNE(p.parsedPkg, nil)
t.AssertNE(p.standardPackages, nil)
})
}
func Test_NewEnumsParser_WithPrefixes(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Test creating parser with prefixes
prefixes := []string{"github.com/gogf", "github.com/test"}
p := NewEnumsParser(prefixes)
t.AssertNE(p, nil)
t.Assert(len(p.prefixes), 2)
t.Assert(p.prefixes[0], "github.com/gogf")
t.Assert(p.prefixes[1], "github.com/test")
})
}
func Test_EnumsParser_Export_Empty(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Test exporting empty enums
p := NewEnumsParser(nil)
result := p.Export()
t.Assert(result, "{}")
})
}
func Test_EnumsParser_Export_WithEnums(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Test exporting with manually added enums
p := NewEnumsParser(nil)
// Add some test enums
p.enums = []EnumItem{
{
Name: "StatusActive",
Value: "1",
Type: "pkg.Status",
Kind: constant.Int,
},
{
Name: "StatusInactive",
Value: "0",
Type: "pkg.Status",
Kind: constant.Int,
},
{
Name: "TypeA",
Value: "type_a",
Type: "pkg.Type",
Kind: constant.String,
},
}
result := p.Export()
t.AssertNE(result, "")
// Parse the result to verify - use raw map to avoid gjson path issues with "."
var resultMap map[string][]interface{}
err := gjson.DecodeTo(result, &resultMap)
t.AssertNil(err)
// Verify Status type has 2 values
statusValues := resultMap["pkg.Status"]
t.Assert(len(statusValues), 2)
// Verify Type type has 1 value
typeValues := resultMap["pkg.Type"]
t.Assert(len(typeValues), 1)
t.Assert(typeValues[0], "type_a")
})
}
func Test_EnumsParser_Export_IntValues(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
p := NewEnumsParser(nil)
p.enums = []EnumItem{
{Name: "One", Value: "1", Type: "pkg.Int", Kind: constant.Int},
{Name: "Two", Value: "2", Type: "pkg.Int", Kind: constant.Int},
{Name: "Negative", Value: "-5", Type: "pkg.Int", Kind: constant.Int},
}
result := p.Export()
var resultMap map[string][]interface{}
err := gjson.DecodeTo(result, &resultMap)
t.AssertNil(err)
values := resultMap["pkg.Int"]
t.Assert(len(values), 3)
// Int values should be exported as integers (stored as float64 in JSON)
t.Assert(values[0], float64(1))
t.Assert(values[1], float64(2))
t.Assert(values[2], float64(-5))
})
}
func Test_EnumsParser_Export_FloatValues(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
p := NewEnumsParser(nil)
p.enums = []EnumItem{
{Name: "Pi", Value: "3.14159", Type: "pkg.Float", Kind: constant.Float},
{Name: "E", Value: "2.71828", Type: "pkg.Float", Kind: constant.Float},
}
result := p.Export()
var resultMap map[string][]interface{}
err := gjson.DecodeTo(result, &resultMap)
t.AssertNil(err)
values := resultMap["pkg.Float"]
t.Assert(len(values), 2)
})
}
func Test_EnumsParser_Export_BoolValues(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
p := NewEnumsParser(nil)
p.enums = []EnumItem{
{Name: "True", Value: "true", Type: "pkg.Bool", Kind: constant.Bool},
{Name: "False", Value: "false", Type: "pkg.Bool", Kind: constant.Bool},
}
result := p.Export()
var resultMap map[string][]interface{}
err := gjson.DecodeTo(result, &resultMap)
t.AssertNil(err)
values := resultMap["pkg.Bool"]
t.Assert(len(values), 2)
t.Assert(values[0], true)
t.Assert(values[1], false)
})
}
func Test_EnumsParser_Export_StringValues(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
p := NewEnumsParser(nil)
p.enums = []EnumItem{
{Name: "Hello", Value: "hello", Type: "pkg.Str", Kind: constant.String},
{Name: "World", Value: "world", Type: "pkg.Str", Kind: constant.String},
}
result := p.Export()
var resultMap map[string][]interface{}
err := gjson.DecodeTo(result, &resultMap)
t.AssertNil(err)
values := resultMap["pkg.Str"]
t.Assert(len(values), 2)
t.Assert(values[0], "hello")
t.Assert(values[1], "world")
})
}
func Test_EnumsParser_Export_MixedTypes(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
p := NewEnumsParser(nil)
p.enums = []EnumItem{
{Name: "IntVal", Value: "42", Type: "pkg.IntType", Kind: constant.Int},
{Name: "StrVal", Value: "test", Type: "pkg.StrType", Kind: constant.String},
{Name: "BoolVal", Value: "true", Type: "pkg.BoolType", Kind: constant.Bool},
}
result := p.Export()
var resultMap map[string][]interface{}
err := gjson.DecodeTo(result, &resultMap)
t.AssertNil(err)
// Each type should have its own array
t.Assert(len(resultMap["pkg.IntType"]), 1)
t.Assert(len(resultMap["pkg.StrType"]), 1)
t.Assert(len(resultMap["pkg.BoolType"]), 1)
})
}
func Test_EnumItem_Structure(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Test EnumItem structure
item := EnumItem{
Name: "TestEnum",
Value: "test_value",
Type: "github.com/test/pkg.EnumType",
Kind: constant.String,
}
t.Assert(item.Name, "TestEnum")
t.Assert(item.Value, "test_value")
t.Assert(item.Type, "github.com/test/pkg.EnumType")
t.Assert(item.Kind, constant.String)
})
}
func Test_EnumsParser_ParsePackages_Integration(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Create a temporary directory with a Go package containing enums
// Note: The module path must contain "/" for enums to be parsed
// (the parser skips std types without "/" in the type name)
tempDir := gfile.Temp(guid.S())
err := gfile.Mkdir(tempDir)
t.AssertNil(err)
defer gfile.Remove(tempDir)
// Create go.mod with a path containing "/"
goModContent := `module github.com/test/enumtest
go 1.21
`
err = gfile.PutContents(filepath.Join(tempDir, "go.mod"), goModContent)
t.AssertNil(err)
// Create a Go file with enum definitions
enumsContent := `package enumtest
type Status int
const (
StatusActive Status = 1
StatusInactive Status = 0
)
type Color string
const (
ColorRed Color = "red"
ColorGreen Color = "green"
ColorBlue Color = "blue"
)
`
err = gfile.PutContents(filepath.Join(tempDir, "enums.go"), enumsContent)
t.AssertNil(err)
// Load the package
cfg := &packages.Config{
Dir: tempDir,
Mode: pkgLoadMode,
Tests: false,
}
pkgs, err := packages.Load(cfg)
t.AssertNil(err)
t.Assert(len(pkgs) > 0, true)
// Parse the packages
p := NewEnumsParser(nil)
p.ParsePackages(pkgs)
// Export and verify - result should contain parsed enums
result := p.Export()
// Verify the export contains some data
t.Assert(len(result) > 2, true) // More than just "{}"
// Parse result as raw map to handle keys with "/"
var resultMap map[string][]interface{}
err = gjson.DecodeTo(result, &resultMap)
t.AssertNil(err)
// Verify Status enum was parsed (type will be "github.com/test/enumtest.Status")
statusKey := "github.com/test/enumtest.Status"
statusValues, hasStatus := resultMap[statusKey]
t.Assert(hasStatus, true)
t.Assert(len(statusValues), 2)
// Verify Color enum was parsed
colorKey := "github.com/test/enumtest.Color"
colorValues, hasColor := resultMap[colorKey]
t.Assert(hasColor, true)
t.Assert(len(colorValues), 3)
})
}
func Test_EnumsParser_ParsePackages_WithPrefixes(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Create a temporary directory with a Go package
tempDir := gfile.Temp(guid.S())
err := gfile.Mkdir(tempDir)
t.AssertNil(err)
defer gfile.Remove(tempDir)
// Create go.mod with a specific module name
goModContent := `module github.com/allowed/pkg
go 1.21
`
err = gfile.PutContents(filepath.Join(tempDir, "go.mod"), goModContent)
t.AssertNil(err)
// Create a Go file with enum definitions
enumsContent := `package pkg
type Status int
const (
StatusOK Status = 1
)
`
err = gfile.PutContents(filepath.Join(tempDir, "enums.go"), enumsContent)
t.AssertNil(err)
// Load the package
cfg := &packages.Config{
Dir: tempDir,
Mode: pkgLoadMode,
Tests: false,
}
pkgs, err := packages.Load(cfg)
t.AssertNil(err)
// Parse with prefix filter that matches
p := NewEnumsParser([]string{"github.com/allowed"})
p.ParsePackages(pkgs)
result := p.Export()
// Should have enums because prefix matches
t.AssertNE(result, "{}")
// Parse with prefix filter that doesn't match
p2 := NewEnumsParser([]string{"github.com/other"})
p2.ParsePackages(pkgs)
result2 := p2.Export()
// Should be empty because prefix doesn't match
t.Assert(result2, "{}")
})
}
func Test_getStandardPackages(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
stdPkgs := getStandardPackages()
t.AssertNE(stdPkgs, nil)
t.Assert(len(stdPkgs) > 0, true)
// Verify some common standard packages are included
_, hasFmt := stdPkgs["fmt"]
t.Assert(hasFmt, true)
_, hasOs := stdPkgs["os"]
t.Assert(hasOs, true)
_, hasContext := stdPkgs["context"]
t.Assert(hasContext, true)
})
}

View File

@ -1,359 +0,0 @@
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package geninit
import (
"context"
"path/filepath"
"testing"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/test/gtest"
"github.com/gogf/gf/v2/util/guid"
)
func Test_ParseGitURL_Basic(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Test basic github URL
info, err := ParseGitURL("github.com/gogf/gf")
t.AssertNil(err)
t.Assert(info.Host, "github.com")
t.Assert(info.Owner, "gogf")
t.Assert(info.Repo, "gf")
t.Assert(info.SubPath, "")
t.Assert(info.Branch, "main")
t.Assert(info.CloneURL, "https://github.com/gogf/gf.git")
})
}
func Test_ParseGitURL_WithHTTPS(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Test URL with https prefix
info, err := ParseGitURL("https://github.com/gogf/gf")
t.AssertNil(err)
t.Assert(info.Host, "github.com")
t.Assert(info.Owner, "gogf")
t.Assert(info.Repo, "gf")
})
}
func Test_ParseGitURL_WithGitSuffix(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Test URL with .git suffix
info, err := ParseGitURL("github.com/gogf/gf.git")
t.AssertNil(err)
t.Assert(info.Host, "github.com")
t.Assert(info.Owner, "gogf")
t.Assert(info.Repo, "gf")
})
}
func Test_ParseGitURL_WithSubPath(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Test URL with subdirectory
info, err := ParseGitURL("github.com/gogf/examples/httpserver/jwt")
t.AssertNil(err)
t.Assert(info.Host, "github.com")
t.Assert(info.Owner, "gogf")
t.Assert(info.Repo, "examples")
t.Assert(info.SubPath, "httpserver/jwt")
t.Assert(info.CloneURL, "https://github.com/gogf/examples.git")
})
}
func Test_ParseGitURL_WithTreeBranch(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Test GitHub web URL with /tree/branch/
info, err := ParseGitURL("github.com/gogf/examples/tree/develop/httpserver/jwt")
t.AssertNil(err)
t.Assert(info.Host, "github.com")
t.Assert(info.Owner, "gogf")
t.Assert(info.Repo, "examples")
t.Assert(info.Branch, "develop")
t.Assert(info.SubPath, "httpserver/jwt")
})
}
func Test_ParseGitURL_WithVersion(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Test URL with version suffix
info, err := ParseGitURL("github.com/gogf/gf/cmd/gf/v2@v2.9.7")
t.AssertNil(err)
t.Assert(info.Host, "github.com")
t.Assert(info.Owner, "gogf")
t.Assert(info.Repo, "gf")
t.Assert(info.SubPath, "cmd/gf/v2")
})
}
func Test_ParseGitURL_Invalid(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Test invalid URL (too short)
_, err := ParseGitURL("github.com/gogf")
t.AssertNE(err, nil)
})
}
func Test_IsSubdirRepo_NotSubdir(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Standard Go module paths should not be detected as subdirectory
t.Assert(IsSubdirRepo("github.com/gogf/gf"), false)
t.Assert(IsSubdirRepo("github.com/gogf/gf/v2"), false)
})
}
func Test_IsSubdirRepo_GoModuleWithCmd(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Go module paths with common patterns should not be detected as subdirectory
t.Assert(IsSubdirRepo("github.com/gogf/gf/cmd/gf/v2"), false)
t.Assert(IsSubdirRepo("github.com/gogf/gf/contrib/drivers/mysql/v2"), false)
})
}
func Test_IsSubdirRepo_ActualSubdir(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Actual subdirectories should be detected
t.Assert(IsSubdirRepo("github.com/gogf/examples/httpserver/jwt"), true)
t.Assert(IsSubdirRepo("github.com/gogf/examples/grpc/basic"), true)
})
}
func Test_GetModuleNameFromGoMod_Valid(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Create temp directory with go.mod
tempDir := gfile.Temp(guid.S())
err := gfile.Mkdir(tempDir)
t.AssertNil(err)
defer gfile.Remove(tempDir)
// Write go.mod file
goModContent := `module github.com/test/myproject
go 1.21
require (
github.com/gogf/gf/v2 v2.9.0
)
`
err = gfile.PutContents(filepath.Join(tempDir, "go.mod"), goModContent)
t.AssertNil(err)
// Test extraction
moduleName := GetModuleNameFromGoMod(tempDir)
t.Assert(moduleName, "github.com/test/myproject")
})
}
func Test_GetModuleNameFromGoMod_NoFile(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Create temp directory without go.mod
tempDir := gfile.Temp(guid.S())
err := gfile.Mkdir(tempDir)
t.AssertNil(err)
defer gfile.Remove(tempDir)
// Test extraction - should return empty
moduleName := GetModuleNameFromGoMod(tempDir)
t.Assert(moduleName, "")
})
}
func Test_GetModuleNameFromGoMod_SimpleModule(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Create temp directory with simple go.mod
tempDir := gfile.Temp(guid.S())
err := gfile.Mkdir(tempDir)
t.AssertNil(err)
defer gfile.Remove(tempDir)
// Write simple go.mod file
goModContent := `module main
go 1.21
`
err = gfile.PutContents(filepath.Join(tempDir, "go.mod"), goModContent)
t.AssertNil(err)
// Test extraction
moduleName := GetModuleNameFromGoMod(tempDir)
t.Assert(moduleName, "main")
})
}
func Test_ASTReplacer_ReplaceInFile(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Create temp directory
tempDir := gfile.Temp(guid.S())
err := gfile.Mkdir(tempDir)
t.AssertNil(err)
defer gfile.Remove(tempDir)
// Create a Go file with imports
goFileContent := `package main
import (
"fmt"
"github.com/old/module/internal/service"
"github.com/old/module/pkg/utils"
"github.com/other/package"
)
func main() {
fmt.Println("Hello")
}
`
goFilePath := filepath.Join(tempDir, "main.go")
err = gfile.PutContents(goFilePath, goFileContent)
t.AssertNil(err)
// Replace imports
replacer := NewASTReplacer("github.com/old/module", "github.com/new/project")
err = replacer.ReplaceInFile(context.Background(), goFilePath)
t.AssertNil(err)
// Verify replacement
content := gfile.GetContents(goFilePath)
t.Assert(gfile.Exists(goFilePath), true)
// Check that old imports are replaced
t.AssertNE(content, "")
t.Assert(contains(content, `"github.com/new/project/internal/service"`), true)
t.Assert(contains(content, `"github.com/new/project/pkg/utils"`), true)
// Check that other imports are not affected
t.Assert(contains(content, `"github.com/other/package"`), true)
t.Assert(contains(content, `"fmt"`), true)
})
}
func Test_ASTReplacer_ReplaceInDir(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Create temp directory structure
tempDir := gfile.Temp(guid.S())
err := gfile.Mkdir(tempDir)
t.AssertNil(err)
defer gfile.Remove(tempDir)
// Create subdirectory
subDir := filepath.Join(tempDir, "sub")
err = gfile.Mkdir(subDir)
t.AssertNil(err)
// Create main.go
mainContent := `package main
import "github.com/old/module/sub"
func main() {
sub.Hello()
}
`
err = gfile.PutContents(filepath.Join(tempDir, "main.go"), mainContent)
t.AssertNil(err)
// Create sub/sub.go
subContent := `package sub
import "github.com/old/module/pkg"
func Hello() {
pkg.Do()
}
`
err = gfile.PutContents(filepath.Join(subDir, "sub.go"), subContent)
t.AssertNil(err)
// Replace imports in directory
replacer := NewASTReplacer("github.com/old/module", "github.com/new/project")
err = replacer.ReplaceInDir(context.Background(), tempDir)
t.AssertNil(err)
// Verify main.go replacement
mainResult := gfile.GetContents(filepath.Join(tempDir, "main.go"))
t.Assert(contains(mainResult, `"github.com/new/project/sub"`), true)
// Verify sub/sub.go replacement
subResult := gfile.GetContents(filepath.Join(subDir, "sub.go"))
t.Assert(contains(subResult, `"github.com/new/project/pkg"`), true)
})
}
func Test_findGoFiles(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Create temp directory structure
tempDir := gfile.Temp(guid.S())
err := gfile.Mkdir(tempDir)
t.AssertNil(err)
defer gfile.Remove(tempDir)
// Create subdirectories
subDir := filepath.Join(tempDir, "sub")
err = gfile.Mkdir(subDir)
t.AssertNil(err)
// Create various files
err = gfile.PutContents(filepath.Join(tempDir, "main.go"), "package main")
t.AssertNil(err)
err = gfile.PutContents(filepath.Join(tempDir, "readme.md"), "# README")
t.AssertNil(err)
err = gfile.PutContents(filepath.Join(subDir, "sub.go"), "package sub")
t.AssertNil(err)
err = gfile.PutContents(filepath.Join(subDir, "data.json"), "{}")
t.AssertNil(err)
// Find Go files
files, err := findGoFiles(tempDir)
t.AssertNil(err)
// Should find exactly 2 Go files
t.Assert(len(files), 2)
// Verify file names
hasMain := false
hasSub := false
for _, f := range files {
if filepath.Base(f) == "main.go" {
hasMain = true
}
if filepath.Base(f) == "sub.go" {
hasSub = true
}
}
t.Assert(hasMain, true)
t.Assert(hasSub, true)
})
}
func Test_findGoFiles_EmptyDir(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Create empty temp directory
tempDir := gfile.Temp(guid.S())
err := gfile.Mkdir(tempDir)
t.AssertNil(err)
defer gfile.Remove(tempDir)
// Find Go files
files, err := findGoFiles(tempDir)
t.AssertNil(err)
t.Assert(len(files), 0)
})
}
// Helper function to check if string contains substring
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsAt(s, substr))
}
func containsAt(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}

View File

@ -1,47 +0,0 @@
-- Test case for issue #4603: overlapping sharding patterns
-- https://github.com/gogf/gf/issues/4603
--
-- Patterns: "a_?", "a_b_?", "a_c_?"
-- Expected: a_1/a_2 -> "a", a_b_1/a_b_2 -> "a_b", a_c_1/a_c_2 -> "a_c"
CREATE TABLE `a_1`
(
`id` int unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(45) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `a_2`
(
`id` int unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(45) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `a_b_1`
(
`id` int unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(45) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `a_b_2`
(
`id` int unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(45) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `a_c_1`
(
`id` int unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(45) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `a_c_2`
(
`id` int unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(45) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

View File

@ -1,30 +0,0 @@
-- Test case for issue #4629: tables pattern matching
-- https://github.com/gogf/gf/issues/4629
-- Standard SQL syntax compatible with MySQL and PostgreSQL
--
-- Tables: trade_order, trade_item, user_info, user_log, config
CREATE TABLE trade_order (
id INTEGER PRIMARY KEY,
name VARCHAR(45) NOT NULL
);
CREATE TABLE trade_item (
id INTEGER PRIMARY KEY,
name VARCHAR(45) NOT NULL
);
CREATE TABLE user_info (
id INTEGER PRIMARY KEY,
name VARCHAR(45) NOT NULL
);
CREATE TABLE user_log (
id INTEGER PRIMARY KEY,
name VARCHAR(45) NOT NULL
);
CREATE TABLE config (
id INTEGER PRIMARY KEY,
name VARCHAR(45) NOT NULL
);

View File

@ -1,22 +0,0 @@
syntax = "proto3";
package genpb;
option go_package = "genpb/v1";
message UserReq {
// v:required
// v:#Id > 0
int64 Id = 1;
// User name for login
string Name = 2;
// v:required
// v:email
string Email = 3; // User email address
}
message UserResp {
int64 Id = 1;
string Name = 2;
string Email = 3;
}

View File

@ -1,21 +0,0 @@
syntax = "proto3";
package genpb;
option go_package = "genpb/v1";
message Order {
// v:required
int64 OrderId = 1;
// Order details
OrderDetail Detail = 2;
}
message OrderDetail {
// v:required
string ProductName = 1;
// v:min:1
int32 Quantity = 2;
// v:min:0.01
double Price = 3;
}

View File

@ -1,3 +0,0 @@
module github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/issue/4387
go 1.20

View File

@ -21,7 +21,7 @@ import (
"github.com/gogf/gf/v2/util/gconv"
)
type ContentType = string
type ContentType string
const (
ContentTypeJSON ContentType = `json`
@ -35,40 +35,23 @@ const (
)
const (
// Separator char for hierarchical data access.
defaultSplitChar = '.'
defaultSplitChar = '.' // Separator char for hierarchical data access.
)
// Json is the customized JSON struct.
type Json struct {
mu rwmutex.RWMutex
// Pointer for hierarchical data access, it's the root of data in default.
p *any
// Char separator('.' in default).
c byte
// Violence Check(false in default),
// which is used to access data when the hierarchical data key contains separator char.
vc bool
p *any // Pointer for hierarchical data access, it's the root of data in default.
c byte // Char separator('.' in default).
vc bool // Violence Check(false in default), which is used to access data when the hierarchical data key contains separator char.
}
// Options for Json object creating/loading.
type Options struct {
// Mark this object is for in concurrent-safe usage. This is especially for Json object creating.
Safe bool
// Custom priority tags for decoding, eg: "json,yaml,MyTag".
// This is specially for struct parsing into Json object.
Tags string
// Type specifies the data content type, eg: json, xml, yaml, toml, ini.
Type ContentType
// StrNumber causes the Decoder to unmarshal a number into an any as a string instead of as a float64.
// This is specially for json content parsing into Json object.
StrNumber bool
Safe bool // Mark this object is for in concurrent-safe usage. This is especially for Json object creating.
Tags string // Custom priority tags for decoding, eg: "json,yaml,MyTag". This is especially for struct parsing into Json object.
Type ContentType // Type specifies the data content type, eg: json, xml, yaml, toml, ini.
StrNumber bool // StrNumber causes the Decoder to unmarshal a number into an any as a string instead of as a float64.
}
// iInterfaces is used for type assert api for Interfaces().

View File

@ -161,86 +161,56 @@ func loadContentWithOptions(data []byte, options Options) (*Json, error) {
if len(data) == 0 {
return NewWithOptions(nil, options), nil
}
var (
checkType ContentType
decodedData any
)
if options.Type != "" {
checkType = gstr.TrimLeft(options.Type, ".")
} else {
checkType, err = checkDataType(data)
if options.Type == "" {
options.Type, err = checkDataType(data)
if err != nil {
return nil, err
}
}
switch checkType {
options.Type = ContentType(gstr.TrimLeft(
string(options.Type), "."),
)
switch options.Type {
case ContentTypeJSON, ContentTypeJs:
decoder := json.NewDecoder(bytes.NewReader(data))
if options.StrNumber {
decoder.UseNumber()
}
if err = decoder.Decode(&result); err != nil {
return nil, err
}
switch result.(type) {
case string, []byte:
return nil, gerror.Newf(`json decoding failed for content: %s`, data)
}
return NewWithOptions(result, options), nil
case ContentTypeXML:
decodedData, err = gxml.Decode(data)
if err != nil {
return nil, err
}
return NewWithOptions(decodedData, options), nil
data, err = gxml.ToJson(data)
case ContentTypeYaml, ContentTypeYml:
decodedData, err = gyaml.Decode(data)
if err != nil {
return nil, err
}
return NewWithOptions(decodedData, options), nil
data, err = gyaml.ToJson(data)
case ContentTypeToml:
decodedData, err = gtoml.Decode(data)
if err != nil {
return nil, err
}
return NewWithOptions(decodedData, options), nil
data, err = gtoml.ToJson(data)
case ContentTypeIni:
decodedData, err = gini.Decode(data)
if err != nil {
return nil, err
}
return NewWithOptions(decodedData, options), nil
data, err = gini.ToJson(data)
case ContentTypeProperties:
decodedData, err = gproperties.Decode(data)
if err != nil {
return nil, err
}
return NewWithOptions(decodedData, options), nil
data, err = gproperties.ToJson(data)
default:
err = gerror.NewCodef(
gcode.CodeInvalidParameter,
`unsupported type "%s" for loading`,
options.Type,
)
}
if err != nil {
return nil, err
}
decoder := json.NewDecoder(bytes.NewReader(data))
if options.StrNumber {
decoder.UseNumber()
}
// ignore some duplicated types, like js and yml,
// which are not necessary shown in error message.
allSupportedTypes := []string{
ContentTypeJSON,
ContentTypeXML,
ContentTypeYaml,
ContentTypeToml,
ContentTypeIni,
ContentTypeProperties,
if err = decoder.Decode(&result); err != nil {
return nil, err
}
return nil, gerror.NewCodef(
gcode.CodeInvalidParameter,
`unsupported type "%s" for loading, all supported types: %s`,
options.Type, gstr.Join(allSupportedTypes, ", "),
)
switch result.(type) {
case string, []byte:
return nil, gerror.Newf(`json decoding failed for content: %s`, data)
}
return NewWithOptions(result, options), nil
}
// checkDataType automatically checks and returns the data type for `content`.
@ -277,104 +247,33 @@ func checkDataType(data []byte) (ContentType, error) {
}
}
// isXMLContent checks whether given content is XML format.
// XML format is easy to be identified using regular expression.
func isXMLContent(data []byte) bool {
return gregex.IsMatch(`^\s*<.+>[\S\s]+<.+>\s*$`, data)
}
// isYamlContent checks whether given content is YAML format.
func isYamlContent(data []byte) bool {
// x = y
// "x.x" = "y"
tomlFormat1 := gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*"""[\s\S]+"""`, data)
if tomlFormat1 {
return false
}
// "x.x" = '''
// y
// '''
tomlFormat2 := gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*'''[\s\S]+'''`, data)
if tomlFormat2 {
return false
}
// content starts with:
// x : "y"
yamlFormat1 := gregex.IsMatch(`^[\n\r]*[\w\-\s\t]+\s*:\s+".+"`, data)
// content starts with:
// x : y
yamlFormat2 := gregex.IsMatch(`^[\n\r]*[\w\-\s\t]+\s*:\s+\w+`, data)
// line starts with:
// x : "y"
yamlFormat3 := gregex.IsMatch(`[\n\r]+[\w\-\s\t]+\s*:\s+".+"`, data)
// line starts with:
// x : y
yamlFormat4 := gregex.IsMatch(`[\n\r]+[\w\-\s\t]+\s*:\s+\w+`, data)
// content starts with:
// "x" : "y"
yamlFormat5 := gregex.IsMatch(`^[\n\r]*".+":\s+".+"`, data)
// line starts with:
// "x" : y
yamlFormat6 := gregex.IsMatch(`[\n\r]+".+":\s+\w+`, data)
return yamlFormat1 || yamlFormat2 || yamlFormat3 || yamlFormat4 || yamlFormat5 || yamlFormat6
return !gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*"""[\s\S]+"""`, data) &&
!gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*'''[\s\S]+'''`, data) &&
((gregex.IsMatch(`^[\n\r]*[\w\-\s\t]+\s*:\s*".+"`, data) ||
gregex.IsMatch(`^[\n\r]*[\w\-\s\t]+\s*:\s*\w+`, data)) ||
(gregex.IsMatch(`[\n\r]+[\w\-\s\t]+\s*:\s*".+"`, data) ||
gregex.IsMatch(`[\n\r]+[\w\-\s\t]+\s*:\s*\w+`, data)))
}
// isTomlContent checks whether given content is TOML format.
func isTomlContent(data []byte) bool {
// content starts with:
// ; comment line
contentStartsWithSemicolonComment := gregex.IsMatch(`^[\s\t\n\r]*;.+`, data)
if contentStartsWithSemicolonComment {
return false
}
// line starts with:
// ; comment line
lineStartsWithSemicolonComment := gregex.IsMatch(`[\s\t\n\r]+;.+`, data)
if lineStartsWithSemicolonComment {
return false
}
// line starts with, this should not be toml format:
// key.with.dot = value
keyWithDot := gregex.IsMatch(`[\n\r]+[\s\t\w\-]+\.[\s\t\w\-]+\s*=\s*.+`, data)
if keyWithDot {
return false
}
// line starts with:
// key = value
// key = "value"
// "key" = "value"
// "key" = value
tomlFormat1 := gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*".+"`, data)
tomlFormat2 := gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*\w+`, data)
return tomlFormat1 || tomlFormat2
return !gregex.IsMatch(`^[\s\t\n\r]*;.+`, data) &&
!gregex.IsMatch(`[\s\t\n\r]+;.+`, data) &&
!gregex.IsMatch(`[\n\r]+[\s\t\w\-]+\.[\s\t\w\-]+\s*=\s*.+`, data) &&
(gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*".+"`, data) ||
gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*\w+`, data))
}
// isIniContent checks whether given content is INI format.
func isIniContent(data []byte) bool {
// no section like: [section], but ini format must have sections.
hasBrackets := gregex.IsMatch(`\[[\w\.]+\]`, data)
if !hasBrackets {
return false
}
iniFormat1 := gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*".+"`, data)
iniFormat2 := gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*\w+`, data)
return iniFormat1 || iniFormat2
return gregex.IsMatch(`\[[\w\.]+\]`, data) &&
(gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*".+"`, data) ||
gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*\w+`, data))
}
// isPropertyContent checks whether given content is Properties format.
func isPropertyContent(data []byte) bool {
// line starts with:
// key = value
// "key" = value
propertyFormat := gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*\w+`, data)
return propertyFormat
return gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*\w+`, data)
}

View File

@ -29,7 +29,7 @@ func LoadPath(path string, options Options) (*Json, error) {
path = p
}
if options.Type == "" {
options.Type = gfile.Ext(path)
options.Type = ContentType(gfile.Ext(path))
}
return loadContentWithOptions(gfile.GetBytes(path), options)
return loadContentWithOptions(gfile.GetBytesWithCache(path), options)
}

View File

@ -418,13 +418,3 @@ DBINFO.password=password
t.AssertNE(err, nil)
})
}
func Test_Load_YAML_For_I18n(t *testing.T) {
var data = []byte(gtest.DataContent("yaml", "i18n-issue.yaml"))
gtest.C(t, func(t *gtest.T) {
j, err := gjson.LoadContent(data)
t.AssertNil(err)
j.SetViolenceCheck(true)
t.Assert(j.Get("resourceUsage.workflow").String(), "workflow")
})
}

View File

@ -1,16 +0,0 @@
"environment status is Creating/Updating, please wait for sync to complete": "环境当前状为创建中/更新中,请等待同步完成"
"There are still queues in the current environment, please ensure there are no queues before deletion": "当前环境还存在队列,确保环境没有队列再删除"
"the current repository has associated environments in use, please ensure no environment associations before deleting the repository": "当前仓库有关联环境正在使用,请确保没有环境关联再删除该仓库"
"There are environments using this cluster, please ensure all environments have been deleted before deleting the cluster": "当前集群存在环境正在使用,请确保所有环境已经删除再删除该集群"
"shareStrategy.Init": "未拆卡"
"shareStrategy.Pending": "切分中"
"shareStrategy.Success": "拆卡成功"
"shareStrategy.Canceling": "拆卡取消中"
"shareStrategy.unknown": "未知状态"
"resourceUsage.none": "无"
"resourceUsage.inference": "推理"
"resourceUsage.training": "训练"
"resourceUsage.workflow": "workflow"
"resourceUsage.hybrid": "混合"
"resourceUsage.unknown": "unknown"

View File

@ -298,7 +298,7 @@ func (m *Manager) init(ctx context.Context) {
if m.data[lang] == nil {
m.data[lang] = make(map[string]string)
}
if j, err := gjson.LoadPath(file, gjson.Options{}); err == nil {
if j, err := gjson.LoadContent(gfile.GetBytes(file)); err == nil {
for k, v := range j.Var().Map() {
m.data[lang][k] = gconv.String(v)
}

View File

@ -259,25 +259,3 @@ func Test_PathInNormal(t *testing.T) {
t.Assert(i18n.T(context.Background(), "{#lang}"), "en-US")
})
}
func Test_Issue_Yaml(t *testing.T) {
// Copy i18n files to current directory.
err := gfile.CopyDir(
gtest.DataPath("issue-yaml"),
gfile.Join(gdebug.CallerDirectory(), "manifest/i18n"),
)
// Remove copied files after testing.
defer gfile.RemoveAll(gfile.Join(gdebug.CallerDirectory(), "manifest"))
gtest.AssertNil(err)
var (
i18n = gi18n.New()
ctx = context.Background()
)
gtest.C(t, func(t *gtest.T) {
i18n.SetLanguage("zh")
t.Assert(i18n.T(ctx, "{#resourceUsage.workflow}"), "workflow")
})
}

View File

@ -1,16 +0,0 @@
"environment status is Creating/Updating, please wait for sync to complete": "环境当前状为创建中/更新中,请等待同步完成"
"There are still queues in the current environment, please ensure there are no queues before deletion": "当前环境还存在队列,确保环境没有队列再删除"
"the current repository has associated environments in use, please ensure no environment associations before deleting the repository": "当前仓库有关联环境正在使用,请确保没有环境关联再删除该仓库"
"There are environments using this cluster, please ensure all environments have been deleted before deleting the cluster": "当前集群存在环境正在使用,请确保所有环境已经删除再删除该集群"
"shareStrategy.Init": "未拆卡"
"shareStrategy.Pending": "切分中"
"shareStrategy.Success": "拆卡成功"
"shareStrategy.Canceling": "拆卡取消中"
"shareStrategy.unknown": "未知状态"
"resourceUsage.none": "无"
"resourceUsage.inference": "推理"
"resourceUsage.training": "训练"
"resourceUsage.workflow": "workflow"
"resourceUsage.hybrid": "混合"
"resourceUsage.unknown": "unknown"

View File

@ -11,8 +11,6 @@ import (
"context"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/internal/command"
"github.com/gogf/gf/v2/internal/intlog"
"github.com/gogf/gf/v2/internal/utils"
@ -119,20 +117,10 @@ func (c *Config) Get(ctx context.Context, pattern string, def ...any) (*gvar.Var
//
// Fetching Rules: Environment arguments are in uppercase format, eg: GF_PACKAGE_VARIABLE.
func (c *Config) GetWithEnv(ctx context.Context, pattern string, def ...any) (*gvar.Var, error) {
value, err := c.Get(ctx, pattern)
if err != nil && gerror.Code(err) != gcode.CodeNotFound {
return nil, err
if v := genv.Get(utils.FormatEnvKey(pattern)); v != nil {
return v, nil
}
if value == nil {
if v := genv.Get(utils.FormatEnvKey(pattern)); v != nil {
return v, nil
}
if len(def) > 0 {
return gvar.New(def[0]), nil
}
return nil, nil
}
return value, nil
return c.Get(ctx, pattern, def...)
}
// GetWithCmd returns the configuration value specified by pattern `pattern`.
@ -141,20 +129,10 @@ func (c *Config) GetWithEnv(ctx context.Context, pattern string, def ...any) (*g
//
// Fetching Rules: Command line arguments are in lowercase format, eg: gf.package.variable.
func (c *Config) GetWithCmd(ctx context.Context, pattern string, def ...any) (*gvar.Var, error) {
value, err := c.Get(ctx, pattern)
if err != nil && gerror.Code(err) != gcode.CodeNotFound {
return nil, err
if v := command.GetOpt(utils.FormatCmdKey(pattern)); v != "" {
return gvar.New(v), nil
}
if value == nil {
if v := command.GetOpt(utils.FormatCmdKey(pattern)); v != "" {
return gvar.New(v), nil
}
if len(def) > 0 {
return gvar.New(def[0]), nil
}
return nil, nil
}
return value, nil
return c.Get(ctx, pattern, def...)
}
// Data retrieves and returns all configuration data as map type.

View File

@ -10,6 +10,7 @@ import (
"fmt"
"os"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcfg"
"github.com/gogf/gf/v2/os/gcmd"
@ -23,10 +24,9 @@ func ExampleConfig_GetWithEnv() {
ctx = gctx.New()
)
v, err := g.Cfg().GetWithEnv(ctx, key)
if err != nil {
panic(err)
if err == nil {
panic(gerror.New("environment variable is not defined"))
}
fmt.Printf("env:%s\n", v)
if err = genv.Set(key, "gf"); err != nil {
panic(err)
}
@ -37,7 +37,6 @@ func ExampleConfig_GetWithEnv() {
fmt.Printf("env:%s", v)
// Output:
// env:
// env:gf
}
@ -47,10 +46,9 @@ func ExampleConfig_GetWithCmd() {
ctx = gctx.New()
)
v, err := g.Cfg().GetWithCmd(ctx, key)
if err != nil {
panic(err)
if err == nil {
panic(gerror.New("command option is not defined"))
}
fmt.Printf("cmd:%s\n", v)
// Re-Initialize custom command arguments.
os.Args = append(os.Args, fmt.Sprintf(`--%s=yes`, key))
gcmd.Init(os.Args...)
@ -62,7 +60,6 @@ func ExampleConfig_GetWithCmd() {
fmt.Printf("cmd:%s", v)
// Output:
// cmd:
// cmd:yes
}

View File

@ -806,41 +806,6 @@ func Test_Issue3903(t *testing.T) {
})
}
// https://github.com/gogf/gf/issues/4218
func Test_Issue4218(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
type SysMenuVo struct {
MenuId int64 `json:"menuId" orm:"menu_id"`
MenuName string `json:"menuName" orm:"menu_name"`
Children []*SysMenuVo `json:"children" orm:"children"`
ParentId int64 `json:"parentId" orm:"parent_id"`
}
menus := []*SysMenuVo{
{
MenuId: 1,
MenuName: "系统管理",
ParentId: 0,
},
{
MenuId: 2,
MenuName: "字典查询",
ParentId: 1,
},
}
var parent *SysMenuVo
err := gconv.Scan(menus[0], &parent)
t.AssertNil(err)
t.Assert(parent.MenuId, 1)
t.Assert(parent.ParentId, 0)
parent.Children = append(parent.Children, menus[1])
t.Assert(len(menus[0].Children), 1)
t.Assert(menus[0].Children[0].MenuId, 2)
t.Assert(menus[0].Children[0].ParentId, 1)
})
}
// https://github.com/gogf/gf/issues/4542
func Test_Issue4542(t *testing.T) {
// Test case 1: Nested map conversion - map[string]any to map[string]map[string]float64

View File

@ -96,14 +96,11 @@ func (c *Converter) Scan(srcValue any, dstPointer any, option ...ScanOption) (er
}
// Get the element type and kind of dstPointer
var dstPointerReflectValueElem = dstPointerReflectValue.Elem()
// Check if srcValue and dstPointer are the same type, in which case direct assignment can be performed
if ok := c.doConvertWithTypeCheck(srcValueReflectValue, dstPointerReflectValueElem); ok {
return nil
}
var (
dstPointerReflectValueElem = dstPointerReflectValue.Elem()
dstPointerReflectValueElemKind = dstPointerReflectValueElem.Kind()
)
// Handle multiple level pointers
var dstPointerReflectValueElemKind = dstPointerReflectValueElem.Kind()
if dstPointerReflectValueElemKind == reflect.Pointer {
if dstPointerReflectValueElem.IsNil() {
// Create a new value for the pointer dereference
@ -117,6 +114,11 @@ func (c *Converter) Scan(srcValue any, dstPointer any, option ...ScanOption) (er
return c.Scan(srcValueReflectValue, dstPointerReflectValueElem, option...)
}
// Check if srcValue and dstPointer are the same type, in which case direct assignment can be performed
if ok := c.doConvertWithTypeCheck(srcValueReflectValue, dstPointerReflectValueElem); ok {
return nil
}
scanOption := c.getScanOption(option...)
// Handle different destination types
switch dstPointerReflectValueElemKind {