mirror of
https://gitee.com/johng/gf
synced 2026-06-15 05:13:53 +08:00
Compare commits
27 Commits
feat/gfdep
...
personal/h
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a10cdaccb | |||
| 45e8f21053 | |||
| 412f1636d4 | |||
| a36d0c7ac6 | |||
| 4578e6811c | |||
| 674443ad4e | |||
| af20fbbde7 | |||
| 3a2a73c786 | |||
| 9f0de3d535 | |||
| 0356acaa60 | |||
| 6022e917b8 | |||
| 7804d70afc | |||
| ea3318d33c | |||
| 4120223bbf | |||
| 3353927847 | |||
| 521854e371 | |||
| 457e9c158b | |||
| cca8210361 | |||
| 854254028f | |||
| 4bada34e9e | |||
| 9a8eba4eb5 | |||
| 8a0dcf060e | |||
| af15651a71 | |||
| 591f55155a | |||
| b248fbb747 | |||
| 06e23d73e9 | |||
| 72ddbe3258 |
336
.github/copilot-instructions.md
vendored
Normal file
336
.github/copilot-instructions.md
vendored
Normal 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
3
.gitignore
vendored
@ -24,5 +24,4 @@ node_modules
|
||||
.docusaurus
|
||||
output
|
||||
.example/
|
||||
.golangci.bck.yml
|
||||
*.exe
|
||||
.golangci.bck.yml
|
||||
@ -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-500ms,3 次调用总耗时可达 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
|
||||
**状态**:✅ 完成并验证
|
||||
263
MAINONLY_FIX.md
263
MAINONLY_FIX.md
@ -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
|
||||
**状态**:✅ 完成并验证
|
||||
@ -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
|
||||
|
||||
@ -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])})
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ type cGen struct {
|
||||
cGenPb
|
||||
cGenPbEntity
|
||||
cGenService
|
||||
cGenTpl
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
@ -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
|
||||
)
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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")
|
||||
})
|
||||
}
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@ -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"))
|
||||
})
|
||||
}
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
@ -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
@ -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)
|
||||
})
|
||||
}
|
||||
@ -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
@ -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)
|
||||
})
|
||||
}
|
||||
@ -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);
|
||||
@ -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();
|
||||
});
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
308
cmd/gf/internal/cmd/gen/tpl/TAG_USAGE.md
Normal file
308
cmd/gf/internal/cmd/gen/tpl/TAG_USAGE.md
Normal 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`
|
||||
106
cmd/gf/internal/cmd/gen/tpl/readme.md
Normal file
106
cmd/gf/internal/cmd/gen/tpl/readme.md
Normal 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. 是否自增
|
||||
|
||||
82
cmd/gf/internal/cmd/gen/tpl/testdata/config_example.yaml
vendored
Normal file
82
cmd/gf/internal/cmd/gen/tpl/testdata/config_example.yaml
vendored
Normal 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"
|
||||
27
cmd/gf/internal/cmd/gen/tpl/testdata/dao/dao.tpl
vendored
Normal file
27
cmd/gf/internal/cmd/gen/tpl/testdata/dao/dao.tpl
vendored
Normal 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.
|
||||
69
cmd/gf/internal/cmd/gen/tpl/testdata/dao/internal/dao_internal.tpl
vendored
Normal file
69
cmd/gf/internal/cmd/gen/tpl/testdata/dao/internal/dao_internal.tpl
vendored
Normal 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)
|
||||
}
|
||||
16
cmd/gf/internal/cmd/gen/tpl/testdata/model/do/do.tpl
vendored
Normal file
16
cmd/gf/internal/cmd/gen/tpl/testdata/model/do/do.tpl
vendored
Normal 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}}
|
||||
}
|
||||
14
cmd/gf/internal/cmd/gen/tpl/testdata/model/entity/entity.tpl
vendored
Normal file
14
cmd/gf/internal/cmd/gen/tpl/testdata/model/entity/entity.tpl
vendored
Normal 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}}
|
||||
}
|
||||
400
cmd/gf/internal/cmd/gen/tpl/tpl.go
Normal file
400
cmd/gf/internal/cmd/gen/tpl/tpl.go
Normal 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
|
||||
}
|
||||
256
cmd/gf/internal/cmd/gen/tpl/tpl_field.go
Normal file
256
cmd/gf/internal/cmd/gen/tpl/tpl_field.go
Normal 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
|
||||
}
|
||||
273
cmd/gf/internal/cmd/gen/tpl/tpl_table.go
Normal file
273
cmd/gf/internal/cmd/gen/tpl/tpl_table.go
Normal 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
|
||||
}
|
||||
25
cmd/gf/internal/cmd/gen/tpl/tpl_test.go
Normal file
25
cmd/gf/internal/cmd/gen/tpl/tpl_test.go
Normal 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)
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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;
|
||||
@ -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
|
||||
);
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
module github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/issue/4387
|
||||
|
||||
go 1.20
|
||||
@ -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().
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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")
|
||||
})
|
||||
}
|
||||
|
||||
16
encoding/gjson/testdata/yaml/i18n-issue.yaml
vendored
16
encoding/gjson/testdata/yaml/i18n-issue.yaml
vendored
@ -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"
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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")
|
||||
})
|
||||
}
|
||||
|
||||
16
i18n/gi18n/testdata/issue-yaml/zh.yaml
vendored
16
i18n/gi18n/testdata/issue-yaml/zh.yaml
vendored
@ -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"
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user