Compare commits

..

13 Commits

Author SHA1 Message Date
4f6a216a56 fix(container): remove unnecessary nil check design 2026-05-18 20:36:33 +00:00
9b5ebbc3d7 fix(container): remove unnecessary nil check design 2026-01-22 20:39:19 +08:00
096c0b2b05 Merge branch 'fix/instances2' of https://github.com/LanceAdd/gf into fix/container-nil-check 2026-01-22 19:35:56 +08:00
0a82036da5 feat(contrib/registry): update nacos sdk to 2.3.5 (#4628)
- Update nacos go sdk to 2.3.5;
- ctx params not use, skip it;
- adjust docs style

---------

Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
Co-authored-by: hailaz <739476267@qq.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-22 19:09:06 +08:00
b4053ed32e feat(os/gcfg): add Loader with automatic struct binding and config watching (like Spring Boot @ConfigurationProperties) (#4575)
# Loader 配置加载器

Loader 是一个通用的配置管理器,提供了类似于 Spring Boot
的`@ConfigurationProperties`的配置加载、监控、更新和管理功能。

## 功能特性

- **泛型支持**:使用 Go 泛型,类型安全的配置绑定
- **配置加载**:从配置源加载数据并绑定到结构体
- **配置监控**:自动监控配置变化并更新
- **自定义转换器**:支持自定义数据转换函数
- **回调处理**:配置变更时的回调函数
- **错误处理**:灵活的错误处理机制

## 安装

```bash
go get github.com/gogf/gf/v2
```

## 使用示例

### 1. 基本用法

#### 用法一

```go
package main

import (
	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/os/gcfg"
	"github.com/gogf/gf/v2/os/gctx"
)

type AppConfig struct {
	Name     string       `json:"name"`
	Age      int          `json:"age"`
	Enabled  bool         `json:"enabled"`
	Features []string     `json:"features"`
	Server   ServerConfig `json:"server"`
}

type ServerConfig struct {
	Host string `json:"host"`
	Port int    `json:"port"`
}

func main() {
	ctx := gctx.New()
	// 创建配置器实例
	loader := gcfg.NewLoader[AppConfig](g.Cfg("test"), "")

	// 加载和监听配置
	loader.MustLoadAndWatch(ctx, "test-watcher")

	// 获取配置
	config := loader.Get()
	fmt.Println(config.Name)
}
```

#### 用法二

```go
package main

import (
	"fmt"
	"github.com/gogf/gf/v2/os/gcfg"
	"github.com/gogf/gf/v2/os/gctx"
)

type AppConfig struct {
	Name     string       `json:"name"`
	Age      int          `json:"age"`
	Enabled  bool         `json:"enabled"`
	Features []string     `json:"features"`
	Server   ServerConfig `json:"server"`
}

type ServerConfig struct {
	Host string `json:"host"`
	Port int    `json:"port"`
}

func main() {
	ctx := gctx.New()

	// 使用单独的适配器创建
	// 创建配置管理器
	cfg, _ := gcfg.NewAdapterFile("test.yaml")
	// 创建配置器实例
	loader := gcfg.NewLoaderWithAdapter[AppConfig](cfg, "")

	// 加载和监听配置
	loader.MustLoadAndWatch(ctx, "test-watcher")

	// 获取配置
	config := loader.Get()
	fmt.Println(config.Name)
}
```

### 2. 配置监控

```go


// 仅加载App配置
loader := gcfg.NewLoaderWithAdapter[AppConfig](cfg, "app")

// 设置配置变更回调
loader.OnChange(func (updated AppConfig) error {
// 配置变更时的处理逻辑
println("配置已更新:", updated.Name)
return nil
})

// 加载数据
err := loader.Load(ctx)
if err != nil {
panic(err)
}

// 开始监控配置变化
err := loader.Watch(context.Background(), "my-watcher")
if err != nil {
panic(err)
}

```

### 3. 自定义转换器

```go
// 设置自定义转换器
loader.SetConverter(func (data any, target *AppConfig) error {
// 自定义数据转换逻辑
return nil
})
```

### 4. 便捷方法

```go
// 一步完成加载和监控
loader.MustLoadAndWatch(context.Background(), "my-app")
```

## API 参考

### `NewLoader`

创建一个新的 Loader 实例。

```go
func NewLoader[T any](config *Config, propertyKey string, targetStruct ...*T) *Loader[T]
```

参数:

- `config`: 配置实例,用于监控变化
- `propertyKey`: 监控的属性键模式(使用 "" 或 "." 监控所有配置)
- `targetStruct`: 接收配置值的结构体指针(可选)

### `NewLoaderWithAdapter`

使用适配器创建一个新的 Loader 实例。

```go
func NewLoaderWithAdapter[T any](adapter Adapter, propertyKey string, targetStruct ...*T) *Loader[T]
```

### `Load`

从配置实例加载数据并绑定到目标结构体。

```go
func (l *Loader[T]) Load(ctx context.Context) error
```

### `MustLoad`

与 Load 类似,但出错时会 panic。

```go
func (l *Loader[T]) MustLoad(ctx context.Context)
```

### `Watch`

开始监控配置变化并自动更新目标结构体。

```go
func (l *Loader[T]) Watch(ctx context.Context, name string) error
```

### `MustWatch`

与 Watch 类似,但出错时会 panic。

```go
func (l *Loader[T]) MustWatch(ctx context.Context, name string)
```

### `MustLoadAndWatch`

便捷方法,调用 MustLoad 和 MustWatch。

```go
func (l *Loader[T]) MustLoadAndWatch(ctx context.Context, name string)
```

### `Get`

返回当前配置结构体。

```go
func (l *Loader[T]) Get() T
```

### `GetPointer() *T`

返回指向当前配置结构体的指针。

```go
func (l *Loader[T]) GetPointer() *T
```

### `OnChange`

设置配置变化时调用的回调函数。

```go
func (l *Loader[T]) OnChange(fn func (updated T) error)
```

### `SetConverter`

设置在 Load 操作期间使用的自定义转换函数。

```go
func (l *Loader[T]) SetConverter(converter func (data any, target *T) error)
```

### `SetWatchErrorHandler`

设置在 Watch 过程中 Load 操作失败时调用的错误处理函数。

```go
func (l *Loader[T]) SetWatchErrorHandler(errorFunc func(ctx context.Context, err error))
```

### `SetReuseTargetStruct`

设置是否在更新时重用相同的目标结构体或创建新结构体。

```go
func (l *Loader[T]) SetReuseTargetStruct(reuse bool)
```

### `StopWatch`

停止监控配置变化并移除关联的监控器。

```go
func (l *Loader[T]) StopWatch(ctx context.Context) (bool, error)
```

### `IsWatching`

返回 Loader 是否正在监控配置变化。

```go
func (l *Loader[T]) IsWatching() bool
```

## 高级用法

### 监控特定配置键

```go
// 只监控特定配置键
loader := gcfg.NewLoaderWithAdapter[ServerConfig](cfg, "server")
```

### 使用默认值

```go
// 创建带默认值的目标结构体
var targetConfig AppConfig
targetConfig.Name = "default-app" // 设置默认值

loader := gcfg.NewLoaderWithAdapter(cfg, "", &targetConfig)
```

## 错误处理

Loader 提供了灵活的错误处理机制:

```go
loader.SetWatchErrorHandler(func(ctx context.Context, err error) {
    // 处理加载错误
    log.Printf("配置加载失败: %v", err)
})
```

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-01-22 19:04:52 +08:00
be3606d6b5 Merge branch 'master' into fix/instances2 2026-01-22 14:55:49 +08:00
fb0e66a85c refactor(database): 解决循环变量地址重用问题 2026-01-22 14:51:30 +08:00
d734f6ddd9 Update os/gcfg/gcfg_adapter_file.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-22 14:45:48 +08:00
cb0bf9e569 Merge branch 'master' into fix/instances2 2026-01-20 21:14:47 +08:00
941bc1b15c Merge branch 'master' into fix/instances2 2026-01-20 13:12:04 +08:00
3b9f2b893e refactor(drivers): 更新数据插入操作中的集合类型定义
- 将 dm 驱动中的 conflictKeySet 从 gset.New 修改为 gset.NewStrSet
- 将 gaussdb 驱动中的 conflictKeySet 从 gset.New 修改为 gset.NewStrSet
- 将 mssql 驱动中的 conflictKeySet 从 gset.New 修改为 gset.NewStrSet
- 将 oracle 驱动中的 conflictKeySet 从 gset.New 修改为 gset.NewStrSet
- 统一使用字符串集合类型以提高类型安全性
2026-01-19 17:57:19 +08:00
95a20ea1e4 refactor(config): 更新配置文件适配器的数据结构和类型安全
- 将 jsonMap 从 StrAnyMap 类型更改为泛型 KVMap[string, *gjson.Json] 类型
- 添加 jsonMapChecker 函数用于 JSON 对象验证
- 使用 NewKVMapWithChecker 替代 NewStrAnyMap 提高类型安全性
- 修改 GetOrSetFuncLock 回调函数返回类型为 *gjson.Json
- 简化数据库链接关闭日志中的键值转换逻辑
2026-01-19 17:44:07 +08:00
465324551a refactor(database): 优化数据库连接管理实现
- 修改 RegisterNilChecker 方法返回实例以支持链式调用
- 更新 Core 结构体中 links 字段类型为类型安全的 KVMap
- 添加专门的链接检查器函数用于连接池管理
- 使用泛型 KVMap 替代原始 map 类型提升类型安全性
- 简化连接关闭逻辑并移除不必要的类型断言
- 优化统计功能中的迭代器实现提高性能
2026-01-19 17:38:49 +08:00
67 changed files with 785 additions and 6598 deletions

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,14 +17,10 @@ import (
"github.com/gogf/gf/v2/util/gconv"
)
// NilChecker is a function that checks whether the given value is nil.
type NilChecker[V any] func(V) bool
// KVMap wraps map type `map[K]V` and provides more map features.
type KVMap[K comparable, V any] struct {
mu rwmutex.RWMutex
data map[K]V
nilChecker NilChecker[V]
mu rwmutex.RWMutex
data map[K]V
}
// NewKVMap creates and returns an empty hash map.
@ -33,13 +29,6 @@ func NewKVMap[K comparable, V any](safe ...bool) *KVMap[K, V] {
return NewKVMapFrom(make(map[K]V), safe...)
}
// NewKVMapWithChecker creates and returns an empty hash map with a custom nil checker.
// The parameter `checker` is a function used to determine if a value is nil.
// The parameter `safe` is used to specify whether to use the map in concurrent-safety mode, which is false by default.
func NewKVMapWithChecker[K comparable, V any](checker NilChecker[V], safe ...bool) *KVMap[K, V] {
return NewKVMapWithCheckerFrom(make(map[K]V), checker, safe...)
}
// NewKVMapFrom creates and returns a hash map from given map `data`.
// Note that, the param `data` map will be set as the underlying data map (no deep copy),
// there might be some concurrent-safe issues when changing the map outside.
@ -51,37 +40,6 @@ func NewKVMapFrom[K comparable, V any](data map[K]V, safe ...bool) *KVMap[K, V]
return m
}
// NewKVMapWithCheckerFrom creates and returns a hash map from given map `data` with a custom nil checker.
// Note that, the param `data` map will be set as the underlying data map (no deep copy),
// and there might be some concurrent-safe issues when changing the map outside.
// The parameter `checker` is a function used to determine if a value is nil.
// The parameter `safe` is used to specify whether to use the map in concurrent-safety mode, which is false by default.
func NewKVMapWithCheckerFrom[K comparable, V any](data map[K]V, checker NilChecker[V], safe ...bool) *KVMap[K, V] {
m := NewKVMapFrom[K, V](data, safe...)
m.RegisterNilChecker(checker)
return m
}
// RegisterNilChecker registers a custom nil checker function for the map values.
// This function is used to determine if a value should be considered as nil.
// The nil checker function takes a value of type V and returns a boolean indicating
// whether the value should be treated as nil.
func (m *KVMap[K, V]) RegisterNilChecker(nilChecker NilChecker[V]) {
m.mu.Lock()
defer m.mu.Unlock()
m.nilChecker = nilChecker
}
// isNil checks whether the given value is nil.
// It first checks if a custom nil checker function is registered and uses it if available,
// otherwise it performs a standard nil check using any(v) == nil.
func (m *KVMap[K, V]) isNil(v V) bool {
if m.nilChecker != nil {
return m.nilChecker(v)
}
return any(v) == nil
}
// Iterator iterates the hash map readonly with custom callback function `f`.
// If `f` returns true, then it continues iterating; or false to stop.
func (m *KVMap[K, V]) Iterator(f func(k K, v V) bool) {
@ -258,9 +216,7 @@ func (m *KVMap[K, V]) doSetWithLockCheck(key K, value V) (val V, ok bool) {
if v, ok := m.data[key]; ok {
return v, true
}
if !m.isNil(value) {
m.data[key] = value
}
m.data[key] = value
return value, false
}
@ -295,9 +251,7 @@ func (m *KVMap[K, V]) GetOrSetFuncLock(key K, f func() V) V {
return v
}
value := f()
if !m.isNil(value) {
m.data[key] = value
}
m.data[key] = value
return value
}

View File

@ -27,10 +27,9 @@ import (
//
// Reference: http://en.wikipedia.org/wiki/Associative_array
type ListKVMap[K comparable, V any] struct {
mu rwmutex.RWMutex
data map[K]*glist.TElement[*gListKVMapNode[K, V]]
list *glist.TList[*gListKVMapNode[K, V]]
nilChecker NilChecker[V]
mu rwmutex.RWMutex
data map[K]*glist.TElement[*gListKVMapNode[K, V]]
list *glist.TList[*gListKVMapNode[K, V]]
}
type gListKVMapNode[K comparable, V any] struct {
@ -50,16 +49,6 @@ func NewListKVMap[K comparable, V any](safe ...bool) *ListKVMap[K, V] {
}
}
// NewListKVMapWithChecker creates and returns a new ListKVMap instance with a custom nil checker.
// The parameter `checker` is a function used to determine if a value is nil.
// The parameter `safe` is used to specify whether using map in concurrent-safety,
// which is false by default.
func NewListKVMapWithChecker[K comparable, V any](checker NilChecker[V], safe ...bool) *ListKVMap[K, V] {
m := NewListKVMap[K, V](safe...)
m.RegisterNilChecker(checker)
return m
}
// NewListKVMapFrom returns a link map from given map `data`.
// Note that, the param `data` map will be copied to the underlying data structure,
// so changes to the original map will not affect the link map.
@ -69,38 +58,6 @@ func NewListKVMapFrom[K comparable, V any](data map[K]V, safe ...bool) *ListKVMa
return m
}
// NewListKVMapWithCheckerFrom returns a link map from given map `data` with a custom nil checker.
// Note that, the param `data` map will be copied to the underlying data structure,
// so changes to the original map will not affect the link map.
// The parameter `checker` is a function used to determine if a value is nil.
// The parameter `safe` is used to specify whether using map in concurrent-safety,
// which is false by default.
func NewListKVMapWithCheckerFrom[K comparable, V any](data map[K]V, nilChecker NilChecker[V], safe ...bool) *ListKVMap[K, V] {
m := NewListKVMapWithChecker[K, V](nilChecker, safe...)
m.Sets(data)
return m
}
// RegisterNilChecker registers a custom nil checker function for the map values.
// This function is used to determine if a value should be considered as nil.
// The nil checker function takes a value of type V and returns a boolean indicating
// whether the value should be treated as nil.
func (m *ListKVMap[K, V]) RegisterNilChecker(nilChecker NilChecker[V]) {
m.mu.Lock()
defer m.mu.Unlock()
m.nilChecker = nilChecker
}
// isNil checks whether the given value is nil.
// It first checks if a custom nil checker function is registered and uses it if available,
// otherwise it performs a standard nil check using any(v) == nil.
func (m *ListKVMap[K, V]) isNil(v V) bool {
if m.nilChecker != nil {
return m.nilChecker(v)
}
return any(v) == nil
}
// Iterator is alias of IteratorAsc.
func (m *ListKVMap[K, V]) Iterator(f func(key K, value V) bool) {
m.IteratorAsc(f)
@ -325,9 +282,7 @@ func (m *ListKVMap[K, V]) doSetWithLockCheckWithoutLock(key K, value V) V {
if e, ok := m.data[key]; ok {
return e.Value.value
}
if !m.isNil(value) {
m.data[key] = m.list.PushBack(&gListKVMapNode[K, V]{key, value})
}
m.data[key] = m.list.PushBack(&gListKVMapNode[K, V]{key, value})
return value
}
@ -370,9 +325,7 @@ func (m *ListKVMap[K, V]) GetOrSetFuncLock(key K, f func() V) V {
return e.Value.value
}
value := f()
if !m.isNil(value) {
m.data[key] = m.list.PushBack(&gListKVMapNode[K, V]{key, value})
}
m.data[key] = m.list.PushBack(&gListKVMapNode[K, V]{key, value})
return value
}
@ -413,9 +366,7 @@ func (m *ListKVMap[K, V]) SetIfNotExist(key K, value V) bool {
if _, ok := m.data[key]; ok {
return false
}
if !m.isNil(value) {
m.data[key] = m.list.PushBack(&gListKVMapNode[K, V]{key, value})
}
m.data[key] = m.list.PushBack(&gListKVMapNode[K, V]{key, value})
return true
}
@ -433,9 +384,7 @@ func (m *ListKVMap[K, V]) SetIfNotExistFunc(key K, f func() V) bool {
return false
}
value := f()
if !m.isNil(value) {
m.data[key] = m.list.PushBack(&gListKVMapNode[K, V]{key, value})
}
m.data[key] = m.list.PushBack(&gListKVMapNode[K, V]{key, value})
return true
}
@ -456,9 +405,7 @@ func (m *ListKVMap[K, V]) SetIfNotExistFuncLock(key K, f func() V) bool {
return false
}
value := f()
if !m.isNil(value) {
m.data[key] = m.list.PushBack(&gListKVMapNode[K, V]{key, value})
}
m.data[key] = m.list.PushBack(&gListKVMapNode[K, V]{key, value})
return true
}

View File

@ -774,6 +774,13 @@ func Test_KVMap_MarshalJSON(t *testing.T) {
t.Assert(data["a"], 1)
t.Assert(data["b"], 2)
})
gtest.C(t, func(t *gtest.T) {
var m gmap.KVMap[int, int]
m.Set(1, 10)
b, err := json.Marshal(m)
t.AssertNil(err)
t.Assert(string(b), `{"1":10}`)
})
}
func Test_KVMap_UnmarshalJSON(t *testing.T) {
@ -891,7 +898,7 @@ func Test_KVMap_GetOrSet_NilValue(t *testing.T) {
v := m.GetOrSet("a", nil)
t.Assert(v, nil)
// nil interface value should not be stored
t.Assert(m.Contains("a"), false)
t.Assert(m.Contains("a"), true)
})
}
@ -903,7 +910,7 @@ func Test_KVMap_GetOrSetFunc_NilValue(t *testing.T) {
v := m.GetOrSetFunc("a", func() any { return nil })
t.Assert(v, nil)
// nil interface value should not be stored
t.Assert(m.Contains("a"), false)
t.Assert(m.Contains("a"), true)
})
}
@ -922,7 +929,7 @@ func Test_KVMap_GetOrSetFuncLock_NilData(t *testing.T) {
v := m.GetOrSetFuncLock("a", func() any { return nil })
t.Assert(v, nil)
// nil interface value should not be stored
t.Assert(m.Contains("a"), false)
t.Assert(m.Contains("a"), true)
})
}
@ -1630,67 +1637,3 @@ func Test_KVMap_Flip_String(t *testing.T) {
t.Assert(m.Get("val2"), "key2")
})
}
// Test TypedNil with custom nil checker for pointers
func Test_KVMap_TypedNil(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
type Student struct {
Name string
Age int
}
m1 := gmap.NewKVMap[int, *Student](true)
for i := 0; i < 10; i++ {
m1.GetOrSetFuncLock(i, func() *Student {
if i%2 == 0 {
return &Student{}
}
return nil
})
}
t.Assert(m1.Size(), 10)
m2 := gmap.NewKVMap[int, *Student](true)
m2.RegisterNilChecker(func(student *Student) bool {
return student == nil
})
for i := 0; i < 10; i++ {
m2.GetOrSetFuncLock(i, func() *Student {
if i%2 == 0 {
return &Student{}
}
return nil
})
}
t.Assert(m2.Size(), 5)
})
}
func Test_NewKVMapWithChecker_TypedNil(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
type Student struct {
Name string
Age int
}
m1 := gmap.NewKVMap[int, *Student](true)
for i := 0; i < 10; i++ {
m1.GetOrSetFuncLock(i, func() *Student {
if i%2 == 0 {
return &Student{}
}
return nil
})
}
t.Assert(m1.Size(), 10)
m2 := gmap.NewKVMapWithChecker[int, *Student](func(student *Student) bool {
return student == nil
}, true)
for i := 0; i < 10; i++ {
m2.GetOrSetFuncLock(i, func() *Student {
if i%2 == 0 {
return &Student{}
}
return nil
})
}
t.Assert(m2.Size(), 5)
})
}

View File

@ -817,7 +817,7 @@ func Test_ListKVMap_GetOrSet_NilValue(t *testing.T) {
v := m.GetOrSet("a", nil)
t.Assert(v, nil)
// nil interface value should not be stored
t.Assert(m.Contains("a"), false)
t.Assert(m.Contains("a"), true)
})
}
@ -1292,7 +1292,7 @@ func Test_ListKVMap_GetOrSetFuncLock_NilData(t *testing.T) {
v := m.GetOrSetFuncLock("a", func() any { return nil })
t.Assert(v, nil)
// nil interface value should not be stored
t.Assert(m.Contains("a"), false)
t.Assert(m.Contains("a"), true)
})
}
@ -1341,67 +1341,3 @@ func Test_ListKVMap_UnmarshalValue_NilData(t *testing.T) {
t.Assert(m.Get("b"), "2")
})
}
// Test typed nil values
func Test_ListKVMap_TypedNil(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
type Student struct {
Name string
Age int
}
m1 := gmap.NewListKVMap[int, *Student](true)
for i := 0; i < 10; i++ {
m1.GetOrSetFuncLock(i, func() *Student {
if i%2 == 0 {
return &Student{}
}
return nil
})
}
t.Assert(m1.Size(), 10)
m2 := gmap.NewListKVMap[int, *Student](true)
m2.RegisterNilChecker(func(student *Student) bool {
return student == nil
})
for i := 0; i < 10; i++ {
m2.GetOrSetFuncLock(i, func() *Student {
if i%2 == 0 {
return &Student{}
}
return nil
})
}
t.Assert(m2.Size(), 5)
})
}
func Test_NewListKVMapWithChecker_TypedNil(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
type Student struct {
Name string
Age int
}
m1 := gmap.NewListKVMap[int, *Student](true)
for i := 0; i < 10; i++ {
m1.GetOrSetFuncLock(i, func() *Student {
if i%2 == 0 {
return &Student{}
}
return nil
})
}
t.Assert(m1.Size(), 10)
m2 := gmap.NewListKVMapWithChecker[int, *Student](func(student *Student) bool {
return student == nil
}, true)
for i := 0; i < 10; i++ {
m2.GetOrSetFuncLock(i, func() *Student {
if i%2 == 0 {
return &Student{}
}
return nil
})
}
t.Assert(m2.Size(), 5)
})
}

View File

@ -86,7 +86,7 @@ func (set *Set) AddIfNotExistFunc(item any, f func() bool) bool {
}
// AddIfNotExistFuncLock checks whether item exists in the set,
// it adds the item to set and returns true if it does not exists in the set and
// it adds the item to set and returns true if it does not exist in the set and
// function `f` returns true, or else it does nothing and returns false.
//
// Note that, if `item` is nil, it does nothing and returns false. The function `f`

View File

@ -15,14 +15,10 @@ import (
"github.com/gogf/gf/v2/util/gconv"
)
// NilChecker is a function that checks whether the given value is nil.
type NilChecker[T any] func(T) bool
// TSet[T] is consisted of any items.
// TSet is a generic set implementation that holds unique items of type T.
type TSet[T comparable] struct {
mu rwmutex.RWMutex
data map[T]struct{}
nilChecker NilChecker[T]
mu rwmutex.RWMutex
data map[T]struct{}
}
// NewTSet creates and returns a new set, which contains un-repeated items.
@ -34,15 +30,6 @@ func NewTSet[T comparable](safe ...bool) *TSet[T] {
}
}
// NewTSetWithChecker creates and returns a new set with a custom nil checker.
// The parameter `nilChecker` is a function used to determine if a value is nil.
// The parameter `safe` is used to specify whether using set in concurrent-safety mode.
func NewTSetWithChecker[T comparable](checker NilChecker[T], safe ...bool) *TSet[T] {
s := NewTSet[T](safe...)
s.RegisterNilChecker(checker)
return s
}
// NewTSetFrom returns a new set from `items`.
// `items` - A slice of type T.
func NewTSetFrom[T comparable](items []T, safe ...bool) *TSet[T] {
@ -56,36 +43,6 @@ func NewTSetFrom[T comparable](items []T, safe ...bool) *TSet[T] {
}
}
// NewTSetWithCheckerFrom returns a new set from `items` with a custom nil checker.
// The parameter `items` is a slice of elements to be added to the set.
// The parameter `checker` is a function used to determine if a value is nil.
// The parameter `safe` is used to specify whether using set in concurrent-safety mode.
func NewTSetWithCheckerFrom[T comparable](items []T, checker NilChecker[T], safe ...bool) *TSet[T] {
set := NewTSetWithChecker[T](checker, safe...)
set.Add(items...)
return set
}
// RegisterNilChecker registers a custom nil checker function for the set elements.
// This function is used to determine if an element should be considered as nil.
// The nil checker function takes an element of type T and returns a boolean indicating
// whether the element should be treated as nil.
func (set *TSet[T]) RegisterNilChecker(nilChecker NilChecker[T]) {
set.mu.Lock()
defer set.mu.Unlock()
set.nilChecker = nilChecker
}
// isNil checks whether the given value is nil.
// It first checks if a custom nil checker function is registered and uses it if available,
// otherwise it performs a standard nil check using any(v) == nil.
func (set *TSet[T]) isNil(v T) bool {
if set.nilChecker != nil {
return set.nilChecker(v)
}
return any(v) == nil
}
// Iterator iterates the set readonly with given callback function `f`,
// if `f` returns true then continue iterating; or false to stop.
func (set *TSet[T]) Iterator(f func(v T) bool) {
@ -114,9 +71,6 @@ func (set *TSet[T]) Add(items ...T) {
//
// Note that, if `item` is nil, it does nothing and returns false.
func (set *TSet[T]) AddIfNotExist(item T) bool {
if set.isNil(item) {
return false
}
if !set.Contains(item) {
set.mu.Lock()
defer set.mu.Unlock()
@ -138,9 +92,6 @@ func (set *TSet[T]) AddIfNotExist(item T) bool {
// Note that, if `item` is nil, it does nothing and returns false. The function `f`
// is executed without writing lock.
func (set *TSet[T]) AddIfNotExistFunc(item T, f func() bool) bool {
if set.isNil(item) {
return false
}
if !set.Contains(item) {
if f() {
set.mu.Lock()
@ -158,15 +109,12 @@ func (set *TSet[T]) AddIfNotExistFunc(item T, f func() bool) bool {
}
// AddIfNotExistFuncLock checks whether item exists in the set,
// it adds the item to set and returns true if it does not exists in the set and
// it adds the item to set and returns true if it does not exist in the set and
// function `f` returns true, or else it does nothing and returns false.
//
// Note that, if `item` is nil, it does nothing and returns false. The function `f`
// is executed within writing lock.
func (set *TSet[T]) AddIfNotExistFuncLock(item T, f func() bool) bool {
if set.isNil(item) {
return false
}
if !set.Contains(item) {
set.mu.Lock()
defer set.mu.Unlock()

View File

@ -419,6 +419,7 @@ func TestSet_AddIfNotExist(t *testing.T) {
t.Assert(s.AddIfNotExist(2), true)
t.Assert(s.Contains(2), true)
t.Assert(s.AddIfNotExist(2), false)
t.Assert(s.AddIfNotExist(nil), true)
t.Assert(s.AddIfNotExist(nil), false)
t.Assert(s.Contains(2), true)
})
@ -497,7 +498,18 @@ func TestSet_AddIfNotExistFuncLock(t *testing.T) {
})
gtest.C(t, func(t *gtest.T) {
s := gset.New(true)
t.Assert(s.AddIfNotExistFuncLock(nil, func() bool { return true }), false)
t.Assert(
s.AddIfNotExistFuncLock(nil, func() bool {
return true
}),
true,
)
t.Assert(
s.AddIfNotExistFuncLock(nil, func() bool {
return true
}),
false,
)
s1 := gset.Set{}
t.Assert(s1.AddIfNotExistFuncLock(1, func() bool { return true }), true)
})

View File

@ -591,42 +591,3 @@ func TestTSet_RLockFunc(t *testing.T) {
t.Assert(sum, 6)
})
}
func Test_TSet_TypedNil(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
type Student struct {
Name string
Age int
}
set := gset.NewTSet[*Student](true)
var s *Student = nil
exist := set.AddIfNotExist(s)
t.Assert(exist, true)
set2 := gset.NewTSet[*Student](true)
set2.RegisterNilChecker(func(student *Student) bool {
return student == nil
})
exist2 := set2.AddIfNotExist(s)
t.Assert(exist2, false)
})
}
func Test_NewTSetWithChecker_TypedNil(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
type Student struct {
Name string
Age int
}
set := gset.NewTSet[*Student](true)
var s *Student = nil
exist := set.AddIfNotExist(s)
t.Assert(exist, true)
set2 := gset.NewTSetWithChecker[*Student](func(student *Student) bool {
return student == nil
}, true)
exist2 := set2.AddIfNotExist(s)
t.Assert(exist2, false)
})
}

View File

@ -18,15 +18,11 @@ import (
"github.com/gogf/gf/v2/util/gconv"
)
// NilChecker is a function that checks whether the given value is nil.
type NilChecker[V any] func(V) bool
// AVLKVTree holds elements of the AVL tree.
type AVLKVTree[K comparable, V any] struct {
mu rwmutex.RWMutex
comparator func(v1, v2 K) int
tree *avltree.Tree[K, V]
nilChecker NilChecker[V]
}
// AVLKVTreeNode is a single element within the tree.
@ -47,15 +43,6 @@ func NewAVLKVTree[K comparable, V any](comparator func(v1, v2 K) int, safe ...bo
}
}
// NewAVLKVTreeWithChecker instantiates an AVL tree with the custom key comparator and nil checker.
// The parameter `safe` is used to specify whether using tree in concurrent-safety, which is false in default.
// The parameter `checker` is used to specify whether the given value is nil.
func NewAVLKVTreeWithChecker[K comparable, V any](comparator func(v1, v2 K) int, checker NilChecker[V], safe ...bool) *AVLKVTree[K, V] {
t := NewAVLKVTree[K, V](comparator, safe...)
t.RegisterNilChecker(checker)
return t
}
// NewAVLKVTreeFrom instantiates an AVL tree with the custom key comparator and data map.
//
// The parameter `safe` is used to specify whether using tree in concurrent-safety, which is false in default.
@ -67,37 +54,6 @@ func NewAVLKVTreeFrom[K comparable, V any](comparator func(v1, v2 K) int, data m
return tree
}
// NewAVLKVTreeWithCheckerFrom instantiates an AVL tree with the custom key comparator, nil checker and data map.
// The parameter `safe` is used to specify whether using tree in concurrent-safety, which is false in default.
// The parameter `checker` is used to specify whether the given value is nil.
func NewAVLKVTreeWithCheckerFrom[K comparable, V any](comparator func(v1, v2 K) int, data map[K]V, checker NilChecker[V], safe ...bool) *AVLKVTree[K, V] {
tree := NewAVLKVTreeWithChecker[K, V](comparator, checker, safe...)
for k, v := range data {
tree.doSet(k, v)
}
return tree
}
// RegisterNilChecker registers a custom nil checker function for the map values.
// This function is used to determine if a value should be considered as nil.
// The nil checker function takes a value of type V and returns a boolean indicating
// whether the value should be treated as nil.
func (tree *AVLKVTree[K, V]) RegisterNilChecker(nilChecker NilChecker[V]) {
tree.mu.Lock()
defer tree.mu.Unlock()
tree.nilChecker = nilChecker
}
// isNil checks whether the given value is nil.
// It first checks if a custom nil checker function is registered and uses it if available,
// otherwise it performs a standard nil check using any(v) == nil.
func (tree *AVLKVTree[K, V]) isNil(value V) bool {
if tree.nilChecker != nil {
return tree.nilChecker(value)
}
return any(value) == nil
}
// Clone clones and returns a new tree from current tree.
func (tree *AVLKVTree[K, V]) Clone() *AVLKVTree[K, V] {
if tree == nil {
@ -562,9 +518,6 @@ func (tree *AVLKVTree[K, V]) Flip(comparator ...func(v1, v2 K) int) {
//
// It returns value with given `key`.
func (tree *AVLKVTree[K, V]) doSet(key K, value V) V {
if tree.isNil(value) {
return value
}
tree.tree.Put(key, value)
return value
}

View File

@ -24,7 +24,6 @@ type BKVTree[K comparable, V any] struct {
comparator func(v1, v2 K) int
m int // order (maximum number of children)
tree *btree.Tree[K, V]
nilChecker NilChecker[V]
}
// BKVTreeEntry represents the key-value pair contained within nodes.
@ -46,15 +45,6 @@ func NewBKVTree[K comparable, V any](m int, comparator func(v1, v2 K) int, safe
}
}
// NewBKVTreeWithChecker instantiates a B-tree with `m` (maximum number of children), a custom key comparator and nil checker.
// The parameter `safe` is used to specify whether using tree in concurrent-safety, which is false in default.
// The parameter `checker` is used to specify whether the given value is nil.
func NewBKVTreeWithChecker[K comparable, V any](m int, comparator func(v1, v2 K) int, checker NilChecker[V], safe ...bool) *BKVTree[K, V] {
t := NewBKVTree[K, V](m, comparator, safe...)
t.RegisterNilChecker(checker)
return t
}
// NewBKVTreeFrom instantiates a B-tree with `m` (maximum number of children), a custom key comparator and data map.
// The parameter `safe` is used to specify whether using tree in concurrent-safety,
// which is false in default.
@ -66,37 +56,6 @@ func NewBKVTreeFrom[K comparable, V any](m int, comparator func(v1, v2 K) int, d
return tree
}
// NewBKVTreeWithCheckerFrom instantiates a B-tree with `m` (maximum number of children), a custom key comparator, nil checker and data map.
// The parameter `safe` is used to specify whether using tree in concurrent-safety, which is false in default.
// The parameter `checker` is used to specify whether the given value is nil.
func NewBKVTreeWithCheckerFrom[K comparable, V any](m int, comparator func(v1, v2 K) int, data map[K]V, checker NilChecker[V], safe ...bool) *BKVTree[K, V] {
tree := NewBKVTreeWithChecker[K, V](m, comparator, checker, safe...)
for k, v := range data {
tree.doSet(k, v)
}
return tree
}
// RegisterNilChecker registers a custom nil checker function for the map values.
// This function is used to determine if a value should be considered as nil.
// The nil checker function takes a value of type V and returns a boolean indicating
// whether the value should be treated as nil.
func (tree *BKVTree[K, V]) RegisterNilChecker(nilChecker NilChecker[V]) {
tree.mu.Lock()
defer tree.mu.Unlock()
tree.nilChecker = nilChecker
}
// isNil checks whether the given value is nil.
// It first checks if a custom nil checker function is registered and uses it if available,
// otherwise it performs a standard nil check using any(v) == nil.
func (tree *BKVTree[K, V]) isNil(value V) bool {
if tree.nilChecker != nil {
return tree.nilChecker(value)
}
return any(value) == nil
}
// Clone clones and returns a new tree from current tree.
func (tree *BKVTree[K, V]) Clone() *BKVTree[K, V] {
if tree == nil {
@ -494,9 +453,6 @@ func (tree *BKVTree[K, V]) Right() *BKVTreeEntry[K, V] {
//
// It returns value with given `key`.
func (tree *BKVTree[K, V]) doSet(key K, value V) V {
if tree.isNil(value) {
return value
}
tree.tree.Put(key, value)
return value
}

View File

@ -24,7 +24,6 @@ type RedBlackKVTree[K comparable, V any] struct {
mu rwmutex.RWMutex
comparator func(v1, v2 K) int
tree *redblacktree.Tree[K, V]
nilChecker NilChecker[V]
}
// RedBlackKVTreeNode is a single element within the tree.
@ -42,15 +41,6 @@ func NewRedBlackKVTree[K comparable, V any](comparator func(v1, v2 K) int, safe
return &tree
}
// NewRedBlackKVTreeWithChecker instantiates a red-black tree with the custom key comparator and `nilChecker`.
// The parameter `safe` is used to specify whether using tree in concurrent-safety, which is false in default.
// The parameter `checker` is used to specify whether the given value is nil.
func NewRedBlackKVTreeWithChecker[K comparable, V any](comparator func(v1, v2 K) int, checker NilChecker[V], safe ...bool) *RedBlackKVTree[K, V] {
t := NewRedBlackKVTree[K, V](comparator, safe...)
t.RegisterNilChecker(checker)
return t
}
// NewRedBlackKVTreeFrom instantiates a red-black tree with the custom key comparator and `data` map.
// The parameter `safe` is used to specify whether using tree in concurrent-safety,
// which is false in default.
@ -60,17 +50,6 @@ func NewRedBlackKVTreeFrom[K comparable, V any](comparator func(v1, v2 K) int, d
return &tree
}
// NewRedBlackKVTreeWithCheckerFrom instantiates a red-black tree with the custom key comparator, `data` map and `nilChecker`.
// The parameter `safe` is used to specify whether using tree in concurrent-safety, which is false in default.
// The parameter `checker` is used to specify whether the given value is nil.
func NewRedBlackKVTreeWithCheckerFrom[K comparable, V any](comparator func(v1, v2 K) int, data map[K]V, checker NilChecker[V], safe ...bool) *RedBlackKVTree[K, V] {
t := NewRedBlackKVTreeWithChecker[K, V](comparator, checker, safe...)
for k, v := range data {
t.doSet(k, v)
}
return t
}
// RedBlackKVTreeInit instantiates a red-black tree with the custom key comparator.
// The parameter `safe` is used to specify whether using tree in concurrent-safety,
// which is false in default.
@ -96,26 +75,6 @@ func RedBlackKVTreeInitFrom[K comparable, V any](tree *RedBlackKVTree[K, V], com
}
}
// RegisterNilChecker registers a custom nil checker function for the map values.
// This function is used to determine if a value should be considered as nil.
// The nil checker function takes a value of type V and returns a boolean indicating
// whether the value should be treated as nil.
func (tree *RedBlackKVTree[K, V]) RegisterNilChecker(nilChecker NilChecker[V]) {
tree.mu.Lock()
defer tree.mu.Unlock()
tree.nilChecker = nilChecker
}
// isNil checks whether the given value is nil.
// It first checks if a custom nil checker function is registered and uses it if available,
// otherwise it performs a standard nil check using any(v) == nil.
func (tree *RedBlackKVTree[K, V]) isNil(value V) bool {
if tree.nilChecker != nil {
return tree.nilChecker(value)
}
return any(value) == nil
}
// SetComparator sets/changes the comparator for sorting.
func (tree *RedBlackKVTree[K, V]) SetComparator(comparator func(a, b K) int) {
tree.comparator = comparator
@ -633,9 +592,6 @@ func (tree *RedBlackKVTree[K, V]) UnmarshalValue(value any) (err error) {
//
// It returns value with given `key`.
func (tree *RedBlackKVTree[K, V]) doSet(key K, value V) (ret V) {
if tree.isNil(value) {
return
}
tree.tree.Put(key, value)
return value
}

View File

@ -1,210 +0,0 @@
// Copyright GoFrame 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 gtree_test
import (
"testing"
"github.com/gogf/gf/v2/container/gtree"
"github.com/gogf/gf/v2/test/gtest"
"github.com/gogf/gf/v2/util/gutil"
)
func Test_KVAVLTree_TypedNil(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
type Student struct {
Name string
Age int
}
avlTree := gtree.NewAVLKVTree[int, *Student](gutil.ComparatorTStr[int], true)
for i := 0; i < 10; i++ {
if i%2 == 0 {
avlTree.Set(i, &Student{})
} else {
var s *Student = nil
avlTree.Set(i, s)
}
}
t.Assert(avlTree.Size(), 10)
avlTree2 := gtree.NewAVLKVTree[int, *Student](gutil.ComparatorTStr[int], true)
avlTree2.RegisterNilChecker(func(student *Student) bool {
return student == nil
})
for i := 0; i < 10; i++ {
if i%2 == 0 {
avlTree2.Set(i, &Student{})
} else {
var s *Student = nil
avlTree2.Set(i, s)
}
}
t.Assert(avlTree2.Size(), 5)
})
}
func Test_KVBTree_TypedNil(t *testing.T) {
type Student struct {
Name string
Age int
}
gtest.C(t, func(t *gtest.T) {
btree := gtree.NewBKVTree[int, *Student](100, gutil.ComparatorTStr[int], true)
for i := 0; i < 10; i++ {
if i%2 == 0 {
btree.Set(i, &Student{})
} else {
var s *Student = nil
btree.Set(i, s)
}
}
t.Assert(btree.Size(), 10)
btree2 := gtree.NewBKVTree[int, *Student](100, gutil.ComparatorTStr[int], true)
btree2.RegisterNilChecker(func(student *Student) bool {
return student == nil
})
for i := 0; i < 10; i++ {
if i%2 == 0 {
btree2.Set(i, &Student{})
} else {
var s *Student = nil
btree2.Set(i, s)
}
}
t.Assert(btree2.Size(), 5)
})
}
func Test_KVRedBlackTree_TypedNil(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
type Student struct {
Name string
Age int
}
redBlackTree := gtree.NewRedBlackKVTree[int, *Student](gutil.ComparatorTStr[int], true)
for i := 0; i < 10; i++ {
if i%2 == 0 {
redBlackTree.Set(i, &Student{})
} else {
var s *Student = nil
redBlackTree.Set(i, s)
}
}
t.Assert(redBlackTree.Size(), 10)
redBlackTree2 := gtree.NewRedBlackKVTree[int, *Student](gutil.ComparatorTStr[int], true)
redBlackTree2.RegisterNilChecker(func(student *Student) bool {
return student == nil
})
for i := 0; i < 10; i++ {
if i%2 == 0 {
redBlackTree2.Set(i, &Student{})
} else {
var s *Student = nil
redBlackTree2.Set(i, s)
}
}
t.Assert(redBlackTree2.Size(), 5)
})
}
func Test_NewKVAVLTreeWithChecker_TypedNil(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
type Student struct {
Name string
Age int
}
avlTree := gtree.NewAVLKVTree[int, *Student](gutil.ComparatorTStr[int], true)
for i := 0; i < 10; i++ {
if i%2 == 0 {
avlTree.Set(i, &Student{})
} else {
var s *Student = nil
avlTree.Set(i, s)
}
}
t.Assert(avlTree.Size(), 10)
avlTree2 := gtree.NewAVLKVTreeWithChecker[int, *Student](gutil.ComparatorTStr[int], func(student *Student) bool {
return student == nil
}, true)
for i := 0; i < 10; i++ {
if i%2 == 0 {
avlTree2.Set(i, &Student{})
} else {
var s *Student = nil
avlTree2.Set(i, s)
}
}
t.Assert(avlTree2.Size(), 5)
})
}
func Test_NewKVBTreeWithChecker_TypedNil(t *testing.T) {
type Student struct {
Name string
Age int
}
gtest.C(t, func(t *gtest.T) {
btree := gtree.NewBKVTree[int, *Student](100, gutil.ComparatorTStr[int], true)
for i := 0; i < 10; i++ {
if i%2 == 0 {
btree.Set(i, &Student{})
} else {
var s *Student = nil
btree.Set(i, s)
}
}
t.Assert(btree.Size(), 10)
btree2 := gtree.NewBKVTreeWithChecker[int, *Student](100, gutil.ComparatorTStr[int], func(student *Student) bool {
return student == nil
}, true)
for i := 0; i < 10; i++ {
if i%2 == 0 {
btree2.Set(i, &Student{})
} else {
var s *Student = nil
btree2.Set(i, s)
}
}
t.Assert(btree2.Size(), 5)
})
}
func Test_NewRedBlackKVTreeWithChecker_TypedNil(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
type Student struct {
Name string
Age int
}
redBlackTree := gtree.NewRedBlackKVTree[int, *Student](gutil.ComparatorTStr[int], true)
for i := 0; i < 10; i++ {
if i%2 == 0 {
redBlackTree.Set(i, &Student{})
} else {
var s *Student = nil
redBlackTree.Set(i, s)
}
}
t.Assert(redBlackTree.Size(), 10)
redBlackTree2 := gtree.NewRedBlackKVTreeWithChecker[int, *Student](gutil.ComparatorTStr[int], func(student *Student) bool {
return student == nil
}, true)
for i := 0; i < 10; i++ {
if i%2 == 0 {
redBlackTree2.Set(i, &Student{})
} else {
var s *Student = nil
redBlackTree2.Set(i, s)
}
}
t.Assert(redBlackTree2.Size(), 5)
})
}

View File

@ -179,7 +179,7 @@ func (c *Client) updateLocalValue(ctx context.Context) (err error) {
}
// AddWatcher adds a watcher for the specified configuration file.
func (c *Client) AddWatcher(name string, f func(ctx context.Context)) {
func (c *Client) AddWatcher(name string, f gcfg.WatcherFunc) {
c.watchers.Add(name, f)
}
@ -193,6 +193,11 @@ func (c *Client) GetWatcherNames() []string {
return c.watchers.GetNames()
}
// IsWatching checks whether the watcher with the specified name is registered.
func (c *Client) IsWatching(name string) bool {
return c.watchers.IsWatching(name)
}
// notifyWatchers notifies all watchers.
func (c *Client) notifyWatchers(ctx context.Context) {
c.watchers.Notify(ctx)

View File

@ -207,7 +207,7 @@ func (c *Client) startAsynchronousWatch(plan *watch.Plan) {
}
// AddWatcher adds a watcher for the specified configuration file.
func (c *Client) AddWatcher(name string, f func(ctx context.Context)) {
func (c *Client) AddWatcher(name string, f gcfg.WatcherFunc) {
c.watchers.Add(name, f)
}
@ -221,6 +221,11 @@ func (c *Client) GetWatcherNames() []string {
return c.watchers.GetNames()
}
// IsWatching checks whether the watcher with the specified name is registered.
func (c *Client) IsWatching(name string) bool {
return c.watchers.IsWatching(name)
}
// notifyWatchers notifies all watchers.
func (c *Client) notifyWatchers(ctx context.Context) {
c.watchers.Notify(ctx)

View File

@ -199,7 +199,7 @@ func (c *Client) startAsynchronousWatch(ctx context.Context, namespace string, w
}
// AddWatcher adds a watcher for the specified configuration file.
func (c *Client) AddWatcher(name string, f func(ctx context.Context)) {
func (c *Client) AddWatcher(name string, f gcfg.WatcherFunc) {
c.watchers.Add(name, f)
}
@ -213,6 +213,11 @@ func (c *Client) GetWatcherNames() []string {
return c.watchers.GetNames()
}
// IsWatching checks whether the watcher with the specified name is registered.
func (c *Client) IsWatching(name string) bool {
return c.watchers.IsWatching(name)
}
// notifyWatchers notifies all watchers.
func (c *Client) notifyWatchers(ctx context.Context) {
c.watchers.Notify(ctx)

View File

@ -152,7 +152,7 @@ func (c *Client) addWatcher() error {
}
// AddWatcher adds a watcher for the specified configuration file.
func (c *Client) AddWatcher(name string, f func(ctx context.Context)) {
func (c *Client) AddWatcher(name string, f gcfg.WatcherFunc) {
c.watchers.Add(name, f)
}
@ -166,6 +166,11 @@ func (c *Client) GetWatcherNames() []string {
return c.watchers.GetNames()
}
// IsWatching checks whether the watcher with the specified name is registered.
func (c *Client) IsWatching(name string) bool {
return c.watchers.IsWatching(name)
}
// notifyWatchers notifies all watchers.
func (c *Client) notifyWatchers(ctx context.Context) {
c.watchers.Notify(ctx)

View File

@ -187,7 +187,7 @@ func (c *Client) startAsynchronousWatch(ctx context.Context, changeChan <-chan m
}
// AddWatcher adds a watcher for the specified configuration file.
func (c *Client) AddWatcher(name string, f func(ctx context.Context)) {
func (c *Client) AddWatcher(name string, f gcfg.WatcherFunc) {
c.watchers.Add(name, f)
}
@ -201,6 +201,11 @@ func (c *Client) GetWatcherNames() []string {
return c.watchers.GetNames()
}
// IsWatching checks whether the watcher with the specified name is registered.
func (c *Client) IsWatching(name string) bool {
return c.watchers.IsWatching(name)
}
// notifyWatchers notifies all watchers.
func (c *Client) notifyWatchers(ctx context.Context) {
c.watchers.Notify(ctx)

View File

@ -108,7 +108,7 @@ func (d *Driver) doMergeInsert(
one = list[0]
oneLen = len(one)
charL, charR = d.GetChars()
conflictKeySet = gset.New(false)
conflictKeySet = gset.NewStrSet(false)
// queryHolders: Handle data with Holder that need to be merged
// queryValues: Handle data that need to be merged

View File

@ -307,7 +307,7 @@ func (d *Driver) doMergeInsert(
one = list[0]
oneLen = len(one)
charL, charR = d.GetChars()
conflictKeySet = gset.New(false)
conflictKeySet = gset.NewStrSet(false)
// queryHolders: Handle data with Holder that need to be merged
// queryValues: Handle data that need to be merged

View File

@ -102,7 +102,7 @@ func (d *Driver) doMergeInsert(
one = list[0]
oneLen = len(one)
charL, charR = d.GetChars()
conflictKeySet = gset.New(false)
conflictKeySet = gset.NewStrSet(false)
// queryHolders: Handle data with Holder that need to be merged
// queryValues: Handle data that need to be merged

View File

@ -181,7 +181,7 @@ func (d *Driver) doMergeInsert(
one = list[0]
oneLen = len(one)
charL, charR = d.GetChars()
conflictKeySet = gset.New(false)
conflictKeySet = gset.NewStrSet(false)
// queryHolders: Handle data with Holder that need to be upsert
// queryValues: Handle data that need to be upsert

View File

@ -1,24 +1,25 @@
# GoFrame Nacos Registry
Use `nacos` as service registration and discovery management.
## Installation
```
go get -u -v github.com/gogf/gf/contrib/registry/nacos/v2
```
suggested using `go.mod`:
```
require github.com/gogf/gf/contrib/registry/nacos/v2 latest
```
## Example
### Reference example
[server](../../../example/registry/nacos/http/server/main.go)
```go
package main
@ -44,6 +45,7 @@ func main() {
```
[client](../../../example/registry/nacos/http/client/main.go)
```go
package main
@ -78,4 +80,3 @@ func main() {
## License
`GoFrame Nacos` is licensed under the [MIT License](../../../LICENSE), 100% free and open-source, forever.

View File

@ -4,7 +4,7 @@ go 1.23.0
require (
github.com/gogf/gf/v2 v2.9.8
github.com/nacos-group/nacos-sdk-go/v2 v2.3.3
github.com/nacos-group/nacos-sdk-go/v2 v2.3.5
)
require (

View File

@ -267,8 +267,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nacos-group/nacos-sdk-go/v2 v2.3.3 h1:lvkBZcYkKENLVR1ubO+vGxTP2L4VtVSArLvYZKuu4Pk=
github.com/nacos-group/nacos-sdk-go/v2 v2.3.3/go.mod h1:ygUBdt7eGeYBt6Lz2HO3wx7crKXk25Mp80568emGMWU=
github.com/nacos-group/nacos-sdk-go/v2 v2.3.5 h1:Hux7C4N4rWhwBF5Zm4yyYskrs9VTgrRTA8DZjoEhQTs=
github.com/nacos-group/nacos-sdk-go/v2 v2.3.5/go.mod h1:ygUBdt7eGeYBt6Lz2HO3wx7crKXk25Mp80568emGMWU=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=

View File

@ -82,7 +82,7 @@ func New(address string, opts ...constant.ClientOption) (reg *Registry) {
return
}
// New creates and returns registry with Config.
// NewWithConfig creates and returns registry with Config.
func NewWithConfig(ctx context.Context, config Config) (reg *Registry, err error) {
// Data validation.
err = g.Validator().Data(config).Run(ctx)

View File

@ -19,7 +19,7 @@ import (
)
// Search searches and returns services with specified condition.
func (reg *Registry) Search(ctx context.Context, in gsvc.SearchInput) (result []gsvc.Service, err error) {
func (reg *Registry) Search(_ context.Context, in gsvc.SearchInput) (result []gsvc.Service, err error) {
if in.Prefix == "" && in.Name != "" {
in.Prefix = gsvc.NewServiceWithName(in.Name).GetPrefix()
}

View File

@ -17,7 +17,7 @@ import (
// Register registers `service` to Registry.
// Note that it returns a new Service if it changes the input Service with custom one.
func (reg *Registry) Register(ctx context.Context, service gsvc.Service) (registered gsvc.Service, err error) {
func (reg *Registry) Register(_ context.Context, service gsvc.Service) (registered gsvc.Service, err error) {
metadata := map[string]string{}
endpoints := service.GetEndpoints()
@ -67,7 +67,7 @@ func (reg *Registry) Register(ctx context.Context, service gsvc.Service) (regist
}
// Deregister off-lines and removes `service` from the Registry.
func (reg *Registry) Deregister(ctx context.Context, service gsvc.Service) (err error) {
func (reg *Registry) Deregister(_ context.Context, service gsvc.Service) (err error) {
c := reg.client
for _, endpoint := range service.GetEndpoints() {

View File

@ -513,18 +513,18 @@ type StatsItem interface {
// Core is the base struct for database management.
type Core struct {
db DB // DB interface object.
ctx context.Context // Context for chaining operation only. Do not set a default value in Core initialization.
group string // Configuration group name.
schema string // Custom schema for this object.
debug *gtype.Bool // Enable debug mode for the database, which can be changed in runtime.
cache *gcache.Cache // Cache manager, SQL result cache only.
links *gmap.Map // links caches all created links by node.
logger glog.ILogger // Logger for logging functionality.
config *ConfigNode // Current config node.
localTypeMap *gmap.StrAnyMap // Local type map for database field type conversion.
dynamicConfig dynamicConfig // Dynamic configurations, which can be changed in runtime.
innerMemCache *gcache.Cache // Internal memory cache for storing temporary data.
db DB // DB interface object.
ctx context.Context // Context for chaining operation only. Do not set a default value in Core initialization.
group string // Configuration group name.
schema string // Custom schema for this object.
debug *gtype.Bool // Enable debug mode for the database, which can be changed in runtime.
cache *gcache.Cache // Cache manager, SQL result cache only.
links *gmap.KVMap[ConfigNode, *sql.DB] // links caches all created links by node.
logger glog.ILogger // Logger for logging functionality.
config *ConfigNode // Current config node.
localTypeMap *gmap.StrAnyMap // Local type map for database field type conversion.
dynamicConfig dynamicConfig // Dynamic configurations, which can be changed in runtime.
innerMemCache *gcache.Cache // Internal memory cache for storing temporary data.
}
type dynamicConfig struct {
@ -867,10 +867,8 @@ const (
)
var (
// checker is the checker function for instances map.
checker = func(v DB) bool { return v == nil }
// instances is the management map for instances.
instances = gmap.NewKVMapWithChecker[string, DB](checker, true)
instances = gmap.NewKVMap[string, DB](true)
// driverMap manages all custom registered driver.
driverMap = map[string]Driver{}
@ -960,7 +958,7 @@ func newDBByConfigNode(node *ConfigNode, group string) (db DB, err error) {
group: group,
debug: gtype.NewBool(),
cache: gcache.New(),
links: gmap.New(true),
links: gmap.NewKVMap[ConfigNode, *sql.DB](true),
logger: glog.New(),
config: node,
localTypeMap: gmap.NewStrAnyMap(true),
@ -1127,7 +1125,7 @@ func (c *Core) getSqlDb(master bool, schema ...string) (sqlDb *sql.DB, err error
// Cache the underlying connection pool object by node.
var (
instanceCacheFunc = func() any {
instanceCacheFunc = func() *sql.DB {
if sqlDb, err = c.db.Open(node); err != nil {
return nil
}
@ -1159,7 +1157,7 @@ func (c *Core) getSqlDb(master bool, schema ...string) (sqlDb *sql.DB, err error
)
if instanceValue != nil && sqlDb == nil {
// It reads from instance map.
sqlDb = instanceValue.(*sql.DB)
sqlDb = instanceValue
}
if node.Debug {
c.db.SetDebug(node.Debug)

View File

@ -113,19 +113,17 @@ func (c *Core) Close(ctx context.Context) (err error) {
if err = c.cache.Close(ctx); err != nil {
return err
}
c.links.LockFunc(func(m map[any]any) {
c.links.LockFunc(func(m map[ConfigNode]*sql.DB) {
for k, v := range m {
if db, ok := v.(*sql.DB); ok {
err = db.Close()
if err != nil {
err = gerror.WrapCode(gcode.CodeDbOperationError, err, `db.Close failed`)
}
intlog.Printf(ctx, `close link: %s, err: %v`, k, err)
if err != nil {
return
}
delete(m, k)
err = v.Close()
if err != nil {
err = gerror.WrapCode(gcode.CodeDbOperationError, err, `db.Close failed`)
}
intlog.Printf(ctx, `close link: %s, err: %v`, gconv.String(k), err)
if err != nil {
return
}
delete(m, k)
}
})
return

View File

@ -30,14 +30,14 @@ func (item *localStatsItem) Stats() sql.DBStats {
// Stats retrieves and returns the pool stat for all nodes that have been established.
func (c *Core) Stats(ctx context.Context) []StatsItem {
var items = make([]StatsItem, 0)
c.links.Iterator(func(k, v any) bool {
var (
node = k.(ConfigNode)
sqlDB = v.(*sql.DB)
)
c.links.Iterator(func(k ConfigNode, v *sql.DB) bool {
// Create a local copy of k to avoid loop variable address re-use issue
// In Go, loop variables are re-used with the same memory address across iterations,
// directly using &k would cause all localStatsItem instances to share the same address
node := k
items = append(items, &localStatsItem{
node: &node,
stats: sqlDB.Stats(),
stats: v.Stats(),
})
return true
})

View File

@ -50,10 +50,8 @@ const (
)
var (
// configChecker checks whether the *Config is nil.
configChecker = func(v *Config) bool { return v == nil }
// Configuration groups.
localConfigMap = gmap.NewKVMapWithChecker[string, *Config](configChecker, true)
localConfigMap = gmap.NewKVMap[string, *Config](true)
)
// SetConfig sets the global configuration for specified group.

View File

@ -14,9 +14,7 @@ import (
)
var (
// checker is the checker function for instances map.
checker = func(v *Redis) bool { return v == nil }
localInstances = gmap.NewKVMapWithChecker[string, *Redis](checker, true)
localInstances = gmap.NewKVMap[string, *Redis](true)
)
// Instance returns an instance of redis client with specified group.

1
go.sum
View File

@ -4,6 +4,7 @@ github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyM
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU=
github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=

View File

@ -14,11 +14,9 @@ const (
)
var (
// checker is used for checking whether the value is nil.
checker = func(v *Manager) bool { return v == nil }
// instances is the instances map for management
// for multiple i18n instance by name.
instances = gmap.NewKVMapWithChecker[string, *Manager](checker, true)
instances = gmap.NewKVMap[string, *Manager](true)
)
// Instance returns an instance of Resource.

View File

@ -176,12 +176,9 @@ var (
// It is used for quick HTTP method searching using map.
methodsMap = make(map[string]struct{})
// checker is used for checking whether the value is nil.
checker = func(v *Server) bool { return v == nil }
// serverMapping stores more than one server instances for current processes.
// The key is the name of the server, and the value is its instance.
serverMapping = gmap.NewKVMapWithChecker[string, *Server](checker, true)
serverMapping = gmap.NewKVMap[string, *Server](true)
// serverRunning marks the running server counts.
// If there is no successful server running or all servers' shutdown, this value is 0.

View File

@ -30,9 +30,8 @@ const (
)
var (
poolChecker = func(v *gpool.Pool) bool { return v == nil }
// addressPoolMap is a mapping for address to its pool object.
addressPoolMap = gmap.NewKVMapWithChecker[string, *gpool.Pool](poolChecker, true)
addressPoolMap = gmap.NewKVMap[string, *gpool.Pool](true)
)
// NewPoolConn creates and returns a connection with pool feature.

View File

@ -39,10 +39,8 @@ type Server struct {
// Map for name to server, for singleton purpose.
var (
// checker is used for checking whether the value is nil.
checker = func(v *Server) bool { return v == nil }
// serverMapping is the map for name to server.
serverMapping = gmap.NewKVMapWithChecker[any, *Server](checker, true)
serverMapping = gmap.NewKVMap[any, *Server](true)
)
// GetServer returns the TCP server with specified `name`,

View File

@ -47,10 +47,8 @@ type Server struct {
type ServerHandler func(conn *ServerConn)
var (
// checker is used for checking whether the value is nil.
checker = func(v *Server) bool { return v == nil }
// serverMapping is used for instance name to its UDP server mappings.
serverMapping = gmap.NewKVMapWithChecker[string, *Server](checker, true)
serverMapping = gmap.NewKVMap[string, *Server](true)
)
// GetServer creates and returns an udp server instance with given name.

View File

@ -13,9 +13,6 @@ import (
"github.com/gogf/gf/v2/container/gmap"
)
// checker is used to check if the value is nil.
var checker = func(v *glist.Element) bool { return v == nil }
// memoryLru holds LRU info.
// It uses list.List from stdlib for its underlying doubly linked list.
type memoryLru struct {
@ -29,7 +26,7 @@ type memoryLru struct {
func newMemoryLru(cap int) *memoryLru {
lru := &memoryLru{
cap: cap,
data: gmap.NewKVMapWithChecker[any, *glist.Element](checker, false),
data: gmap.NewKVMap[any, *glist.Element](false),
list: glist.New(false),
}
return lru

View File

@ -29,12 +29,17 @@ type Adapter interface {
Data(ctx context.Context) (data map[string]any, err error)
}
// WatcherFunc is the callback function type for configuration watchers.
type WatcherFunc = func(context.Context)
// WatcherAdapter is the interface for configuration watcher.
type WatcherAdapter interface {
// AddWatcher adds a watcher function for specified `pattern` and `resource`.
AddWatcher(name string, fn func(ctx context.Context))
AddWatcher(name string, fn WatcherFunc)
// RemoveWatcher removes the watcher function for specified `pattern` and `resource`.
RemoveWatcher(name string)
// GetWatcherNames returns all watcher names.
GetWatcherNames() []string
// IsWatching checks and returns whether the specified `pattern` is watching.
IsWatching(name string) bool
}

View File

@ -86,7 +86,7 @@ func (a *AdapterContent) Data(ctx context.Context) (data map[string]any, err err
}
// AddWatcher adds a watcher for the specified configuration file.
func (a *AdapterContent) AddWatcher(name string, fn func(ctx context.Context)) {
func (a *AdapterContent) AddWatcher(name string, fn WatcherFunc) {
a.watchers.Add(name, fn)
}
@ -100,6 +100,11 @@ func (a *AdapterContent) GetWatcherNames() []string {
return a.watchers.GetNames()
}
// IsWatching checks and returns whether the specified `name` is watching.
func (a *AdapterContent) IsWatching(name string) bool {
return a.watchers.IsWatching(name)
}
// notifyWatchers notifies all watchers.
func (a *AdapterContent) notifyWatchers(ctx context.Context) {
a.watchers.Notify(ctx)

View File

@ -32,11 +32,11 @@ var (
// AdapterFile implements interface Adapter using file.
type AdapterFile struct {
defaultFileNameOrPath *gtype.String // Default configuration file name or file path.
searchPaths *garray.StrArray // Searching the path array.
jsonMap *gmap.StrAnyMap // The pared JSON objects for configuration files.
violenceCheck bool // Whether it does violence check in value index searching. It affects the performance when set true(false in default).
watchers *WatcherRegistry // Watchers for watching file changes.
defaultFileNameOrPath *gtype.String // Default configuration file name or file path.
searchPaths *garray.StrArray // Searching the path array.
jsonMap *gmap.KVMap[string, *gjson.Json] // The parsed JSON objects for configuration files.
violenceCheck bool // Whether it does violence check in value index searching. It affects the performance when set true(false in default).
watchers *WatcherRegistry // Watchers for watching file changes.
}
const (
@ -46,9 +46,8 @@ const (
var (
supportedFileTypes = []string{"toml", "yaml", "yml", "json", "ini", "xml", "properties"} // All supported file types suffixes.
checker = func(v *Config) bool { return v == nil }
localInstances = gmap.NewKVMapWithChecker[string, *Config](checker, true) // Instances map containing configuration instances.
customConfigContentMap = gmap.NewStrStrMap(true) // Customized configuration content.
localInstances = gmap.NewKVMap[string, *Config](true) // Instances map containing configuration instances.
customConfigContentMap = gmap.NewStrStrMap(true) // Customized configuration content.
// Prefix array for trying searching in resource manager.
resourceTryFolders = []string{
@ -78,7 +77,7 @@ func NewAdapterFile(fileNameOrPath ...string) (*AdapterFile, error) {
config := &AdapterFile{
defaultFileNameOrPath: gtype.NewString(usedFileNameOrPath),
searchPaths: garray.NewStrArray(true),
jsonMap: gmap.NewStrAnyMap(true),
jsonMap: gmap.NewKVMap[string, *gjson.Json](true),
watchers: NewWatcherRegistry(),
}
// Customized dir path from env/cmd.
@ -257,7 +256,7 @@ func (a *AdapterFile) getJson(fileNameOrPath ...string) (configJson *gjson.Json,
usedFileNameOrPath = fileNameOrPath[0]
}
// It uses JSON map to cache specified configuration file content.
result := a.jsonMap.GetOrSetFuncLock(usedFileNameOrPath, func() any {
result := a.jsonMap.GetOrSetFuncLock(usedFileNameOrPath, func() *gjson.Json {
var (
content string
filePath string
@ -326,13 +325,13 @@ func (a *AdapterFile) getJson(fileNameOrPath ...string) (configJson *gjson.Json,
return configJson
})
if result != nil {
return result.(*gjson.Json), err
return result, err
}
return
}
// AddWatcher adds a watcher for the specified configuration file.
func (a *AdapterFile) AddWatcher(name string, fn func(ctx context.Context)) {
func (a *AdapterFile) AddWatcher(name string, fn WatcherFunc) {
a.watchers.Add(name, fn)
}
@ -346,6 +345,11 @@ func (a *AdapterFile) GetWatcherNames() []string {
return a.watchers.GetNames()
}
// IsWatching checks and returns whether the specified `name` is watching.
func (a *AdapterFile) IsWatching(name string) bool {
return a.watchers.IsWatching(name)
}
// notifyWatchers notifies all watchers.
func (a *AdapterFile) notifyWatchers(ctx context.Context) {
a.watchers.Notify(ctx)

253
os/gcfg/gcfg_loader.go Normal file
View File

@ -0,0 +1,253 @@
// Copyright GoFrame 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 gcfg
import (
"context"
"sync"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/internal/intlog"
)
// Loader is a generic configuration manager that provides
// configuration loading, watching and management similar to Spring Boot's @ConfigurationProperties
type Loader[T any] struct {
config *Config // The configuration instance to watch
propertyKey string // The property key pattern to watch
targetStruct *T // The target struct pointer to bind configuration to
mutex sync.RWMutex // Mutex for thread-safe operations
onChange func(T) error // Callback function when configuration changes
converter func(data any, target *T) error // Optional custom converter function
watchErrorFunc func(ctx context.Context, err error) // Optional error handling function for watch operations
reuse bool // reuse the same target struct, default is false to avoid data race
watcherName string // watcher name
}
// NewLoader creates a new Loader instance
// config: the configuration instance to watch for changes
// propertyKey: the property key pattern to watch (use "" or "." to watch all configuration)
// targetStruct: pointer to the struct that will receive the configuration values
func NewLoader[T any](config *Config, propertyKey string, targetStruct ...*T) *Loader[T] {
if len(targetStruct) > 0 {
return &Loader[T]{
config: config,
propertyKey: propertyKey,
targetStruct: targetStruct[0],
reuse: false,
}
}
return &Loader[T]{
config: config,
propertyKey: propertyKey,
targetStruct: new(T),
reuse: false,
}
}
// NewLoaderWithAdapter creates a new Loader instance
// adapter: the adapter instance to use for loading and watching configuration
// propertyKey: the property key pattern to watch (use "" or "." to watch all configuration)
// targetStruct: pointer to the struct that will receive the configuration values
func NewLoaderWithAdapter[T any](adapter Adapter, propertyKey string, targetStruct ...*T) *Loader[T] {
return NewLoader(NewWithAdapter(adapter), propertyKey, targetStruct...)
}
// OnChange sets the callback function that will be called when configuration changes
// The callback function receives the updated configuration struct and can return an error
func (l *Loader[T]) OnChange(fn func(updated T) error) *Loader[T] {
l.mutex.Lock()
defer l.mutex.Unlock()
l.onChange = fn
return l
}
// Load loads configuration from the config instance and binds it to the target struct
// The context is passed to the underlying configuration adapter
func (l *Loader[T]) Load(ctx context.Context) error {
l.mutex.Lock()
defer l.mutex.Unlock()
// Get configuration data
var data *gvar.Var
if l.propertyKey == "" || l.propertyKey == "." {
// Get all configuration data
configData, err := l.config.Data(ctx)
if err != nil {
return err
}
data = gvar.New(configData)
} else {
// Get specific property
configValue, err := l.config.Get(ctx, l.propertyKey)
if err != nil {
return err
}
if configValue != nil {
data = configValue
} else {
data = gvar.New(nil)
}
}
// Use custom converter if provided, otherwise use default gconv.Scan
if l.converter != nil && data != nil {
if l.reuse {
if err := l.converter(data.Val(), l.targetStruct); err != nil {
return err
}
} else {
var newConfig T
if err := l.converter(data.Val(), &newConfig); err != nil {
return err
}
l.targetStruct = &newConfig
}
} else {
if data != nil {
if l.reuse {
if err := data.Scan(l.targetStruct); err != nil {
return err
}
} else {
var newConfig T
if err := data.Scan(&newConfig); err != nil {
return err
}
l.targetStruct = &newConfig
}
}
}
// Call change callback if exists
if l.onChange != nil {
return l.onChange(*l.targetStruct)
}
return nil
}
// MustLoad is like Load but panics if there is an error
func (l *Loader[T]) MustLoad(ctx context.Context) {
if err := l.Load(ctx); err != nil {
panic(err)
}
}
// Watch starts watching for configuration changes and automatically updates the target struct
// name: the name of the watcher, which is used to identify this watcher
// This method sets up a watcher that will call Load() when configuration changes are detected
func (l *Loader[T]) Watch(ctx context.Context, name string) error {
if name == "" {
return gerror.New("Watcher name cannot be empty")
}
adapter := l.config.GetAdapter()
if watcherAdapter, ok := adapter.(WatcherAdapter); ok {
watcherAdapter.AddWatcher(name, func(ctx context.Context) {
// Reload configuration when change is detected
if err := l.Load(ctx); err != nil {
// Use the configured error handler if available, otherwise execute default logging
if l.watchErrorFunc != nil {
l.watchErrorFunc(ctx, err)
} else {
// Default logging using intlog (internal logging for development)
intlog.Errorf(ctx, "Configuration load failed in watcher %s: %v", name, err)
}
}
})
l.watcherName = name
return nil
}
return gerror.New("Watcher adapter not found")
}
// MustWatch is like Watch but panics if there is an error
func (l *Loader[T]) MustWatch(ctx context.Context, name string) {
if err := l.Watch(ctx, name); err != nil {
panic(err)
}
}
// MustLoadAndWatch is a convenience method that calls MustLoad and MustWatch
func (l *Loader[T]) MustLoadAndWatch(ctx context.Context, name string) {
l.MustLoad(ctx)
l.MustWatch(ctx, name)
}
// Get returns the current configuration struct
// This method is thread-safe and returns a copy of the current configuration
func (l *Loader[T]) Get() T {
l.mutex.RLock()
defer l.mutex.RUnlock()
return *l.targetStruct
}
// GetPointer returns a pointer to the current configuration struct
// This method is thread-safe and returns a pointer to the current configuration
// The returned pointer is safe for read operations but should not be modified
func (l *Loader[T]) GetPointer() *T {
l.mutex.RLock()
defer l.mutex.RUnlock()
return l.targetStruct
}
// SetConverter sets a custom converter function that will be used during Load operations
// The converter function receives the source data and the target struct pointer
func (l *Loader[T]) SetConverter(converter func(data any, target *T) error) *Loader[T] {
l.mutex.Lock()
defer l.mutex.Unlock()
l.converter = converter
return l
}
// SetWatchErrorHandler sets an error handling function that will be called when Load operations fail during Watch
func (l *Loader[T]) SetWatchErrorHandler(errorFunc func(ctx context.Context, err error)) *Loader[T] {
l.mutex.Lock()
defer l.mutex.Unlock()
l.watchErrorFunc = errorFunc
return l
}
// SetReuseTargetStruct sets whether to reuse the same target struct or create a new one on updates
func (l *Loader[T]) SetReuseTargetStruct(reuse bool) *Loader[T] {
l.mutex.Lock()
defer l.mutex.Unlock()
l.reuse = reuse
return l
}
// StopWatch stops watching for configuration changes and removes the associated watcher
func (l *Loader[T]) StopWatch(ctx context.Context) (bool, error) {
l.mutex.Lock()
defer l.mutex.Unlock()
if l.watcherName == "" {
return false, gerror.New("No watcher name specified")
}
adapter := l.config.GetAdapter()
if watcherAdapter, ok := adapter.(WatcherAdapter); ok {
watcherAdapter.RemoveWatcher(l.watcherName)
l.watcherName = ""
return true, nil
}
return false, gerror.New("Watcher adapter not found")
}
// IsWatching returns true if the loader is currently watching for configuration changes
func (l *Loader[T]) IsWatching() bool {
l.mutex.RLock()
defer l.mutex.RUnlock()
if l.watcherName == "" {
return false
}
adapter := l.config.GetAdapter()
if watcherAdapter, ok := adapter.(WatcherAdapter); ok {
return watcherAdapter.IsWatching(l.watcherName)
}
return false
}

View File

@ -17,18 +17,23 @@ import (
// It provides a unified implementation of watcher management to avoid code duplication
// across different adapter implementations.
type WatcherRegistry struct {
watchers *gmap.StrAnyMap // Watchers map storing watcher callbacks.
watchers *gmap.KVMap[string, WatcherFunc] // Watchers map storing watcher callbacks.
}
// NewWatcherRegistry creates and returns a new WatcherRegistry instance.
func NewWatcherRegistry() *WatcherRegistry {
return &WatcherRegistry{
watchers: gmap.NewStrAnyMap(true),
watchers: gmap.NewKVMap[string, WatcherFunc](true),
}
}
// IsWatching checks whether the watcher with the specified name is registered.
func (r *WatcherRegistry) IsWatching(name string) bool {
return r.watchers.Contains(name)
}
// Add adds a watcher with the specified name and callback function.
func (r *WatcherRegistry) Add(name string, fn func(ctx context.Context)) {
func (r *WatcherRegistry) Add(name string, fn WatcherFunc) {
r.watchers.Set(name, fn)
}
@ -46,17 +51,15 @@ func (r *WatcherRegistry) GetNames() []string {
// Each callback is executed in a separate goroutine with panic recovery to prevent
// one watcher's panic from affecting others.
func (r *WatcherRegistry) Notify(ctx context.Context) {
r.watchers.Iterator(func(k string, v any) bool {
if fn, ok := v.(func(ctx context.Context)); ok {
go func(k string, fn func(ctx context.Context), ctx context.Context) {
defer func() {
if r := recover(); r != nil {
intlog.Errorf(ctx, "watcher %s panic: %v", k, r)
}
}()
fn(ctx)
}(k, fn, ctx)
}
r.watchers.Iterator(func(k string, fn WatcherFunc) bool {
go func(k string, fn WatcherFunc, ctx context.Context) {
defer func() {
if r := recover(); r != nil {
intlog.Errorf(ctx, "watcher %s panic: %v", k, r)
}
}()
fn(ctx)
}(k, fn, ctx)
return true
})
}

View File

@ -0,0 +1,345 @@
// Copyright GoFrame 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 gcfg_test
import (
"context"
"errors"
"strings"
"testing"
"time"
"github.com/gogf/gf/v2/container/gtype"
"github.com/gogf/gf/v2/os/gcfg"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/test/gtest"
"github.com/gogf/gf/v2/util/gconv"
"github.com/gogf/gf/v2/util/guid"
)
// TestConfig is a test struct for configuration binding
type TestConfig struct {
Name string `json:"name" yaml:"name"`
Age int `json:"age" yaml:"age"`
Enabled bool `json:"enabled" yaml:"enabled"`
Features []string `json:"features" yaml:"features"`
Server ServerConfig `json:"server" yaml:"server"`
}
// TestConfig2 is a test struct for configuration binding
type TestConfig2 struct {
Name string `json:"name" yaml:"name"`
Age int `json:"age" yaml:"age"`
Enabled bool `json:"enabled" yaml:"enabled"`
Features string `json:"features" yaml:"features"`
Server ServerConfig `json:"server" yaml:"server"`
}
// TestConfig3 is a test struct for configuration binding
type TestConfig3 struct {
Name string `json:"name" yaml:"name"`
Age int `json:"age" yaml:"age"`
Enabled bool `json:"enabled" yaml:"enabled"`
Features []string `json:"features" yaml:"features"`
Server ServerConfig `json:"server" yaml:"server"`
Other string `json:"other" yaml:"other"`
}
type ServerConfig struct {
Host string `json:"host" yaml:"host"`
Port int `json:"port" yaml:"port"`
}
var configContent = `
name: "test-app"
age: 25
enabled: true
features: ["feature1", "feature2", "feature3"]
server:
host: "localhost"
port: 8080
`
func TestLoader_Load(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
configFile = "./" + guid.S() + ".yaml"
err = gfile.PutContents(configFile, configContent)
)
t.AssertNil(err)
defer gfile.RemoveFile(configFile)
// Create a new config instance
cfg, err := gcfg.NewAdapterFile(configFile)
t.AssertNil(err)
// Create loader
loader := gcfg.NewLoaderWithAdapter[TestConfig](cfg, "")
// Load configuration
err = loader.Load(context.Background())
t.AssertNil(err)
v := loader.Get()
// Check loaded values
t.Assert(v.Name, "test-app")
t.Assert(v.Age, 25)
t.Assert(v.Enabled, true)
t.Assert(v.Server.Host, "localhost")
t.Assert(v.Server.Port, 8080)
t.Assert(len(v.Features), 3)
})
}
func TestLoader_LoadWithDefaultValues(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
configFile = "./" + guid.S() + ".yaml"
err = gfile.PutContents(configFile, configContent)
)
t.AssertNil(err)
defer gfile.RemoveFile(configFile)
// Create a new config instance
cfg, err := gcfg.NewAdapterFile(configFile)
t.AssertNil(err)
// Create target struct
var targetConfig TestConfig3
targetConfig.Other = "other"
// Create loader
loader := gcfg.NewLoaderWithAdapter(cfg, "", &targetConfig)
loader.SetReuseTargetStruct(true)
// Load configuration
err = loader.Load(context.Background())
t.AssertNil(err)
v := loader.Get()
// Check loaded values
t.Assert(v.Name, "test-app")
t.Assert(v.Age, 25)
t.Assert(v.Enabled, true)
t.Assert(v.Server.Host, "localhost")
t.Assert(v.Server.Port, 8080)
t.Assert(len(v.Features), 3)
t.Assert(v.Other, "other")
})
}
func TestLoader_LoadWithPropertyKey(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
configFile = "./" + guid.S() + ".yaml"
err = gfile.PutContents(configFile, configContent)
)
t.AssertNil(err)
defer gfile.RemoveFile(configFile)
// Create a new config instance
cfg, err := gcfg.NewAdapterFile(configFile)
t.AssertNil(err)
// Create loader with specific property key
loader := gcfg.NewLoaderWithAdapter[ServerConfig](cfg, "server")
// Load configuration
err = loader.Load(context.Background())
t.AssertNil(err)
v := loader.Get()
// Check loaded values - only the app section should be loaded
t.Assert(v.Host, "localhost")
t.Assert(v.Port, 8080)
})
}
func TestLoader_WatchAndOnChange(t *testing.T) {
var configContent2 = `
name: test-app-2
age: 200
enabled: true
features: ["feature1", "feature2", "feature3"]
server:
host: localhost
port: 8080
`
gtest.C(t, func(t *gtest.T) {
// Create a new config instance
cfg, err := gcfg.NewAdapterContent(configContent)
t.AssertNil(err)
// Variable to track if callback was called
callbackCalled := gtype.NewBool(false)
// Create loader
loader := gcfg.NewLoaderWithAdapter[TestConfig](cfg, "")
// Set change callback
loader.OnChange(func(updated TestConfig) error {
callbackCalled.Set(true)
return nil
})
// Load configuration
err = loader.Load(context.Background())
t.AssertNil(err)
err = loader.Watch(context.Background(), "test-watcher")
t.AssertNil(err)
v := loader.Get()
t.Assert(v.Name, "test-app")
t.Assert(v.Age, 25)
err = cfg.SetContent(configContent2)
t.AssertNil(err)
time.Sleep(2 * time.Second)
v2 := loader.Get()
t.Assert(v2.Name, "test-app-2")
t.Assert(v2.Age, 200)
t.Assert(callbackCalled.Val(), true)
})
}
func TestLoader_SetConverter(t *testing.T) {
var configContent2 = `
name: test-app-2
age: 200
enabled: true
features: ["feature", "feature", "feature"]
server:
host: localhost
port: 8080
`
gtest.C(t, func(t *gtest.T) {
var (
configFile = "./" + guid.S() + ".yaml"
err = gfile.PutContents(configFile, configContent2)
)
t.AssertNil(err)
defer gfile.RemoveFile(configFile)
// Create a new config instance
cfg, err := gcfg.NewAdapterFile(configFile)
t.AssertNil(err)
// Create loader
loader := gcfg.NewLoaderWithAdapter[TestConfig2](cfg, "features")
// Set custom converter
loader.SetConverter(func(data any, target *TestConfig2) error {
s := gconv.Strings(data)
target.Features = strings.Join(s, ",")
return nil
})
// Load configuration
err = loader.Load(context.Background())
t.AssertNil(err)
v := loader.Get()
// Check converted values
t.Assert(v.Features, "feature,feature,feature")
})
}
func TestLoader_SetWatchErrorHandler(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Create a new config instance with content that will cause converter error
cfg, err := gcfg.NewAdapterContent(configContent)
t.AssertNil(err)
// Create loader
loader := gcfg.NewLoaderWithAdapter[TestConfig](cfg, "")
// Set error handler for watch operations
errorHandled := gtype.NewBool(false)
loader.SetWatchErrorHandler(func(ctx context.Context, err error) {
errorHandled.Set(true)
})
// Set a converter that will fail
loader.SetConverter(func(data any, target *TestConfig) error {
return errors.New("converter error")
})
// Load initially - this should return error without calling error handler
err = loader.Load(context.Background())
t.AssertNE(err, nil)
t.Assert(err.Error(), "converter error")
// Error handler should NOT be called during direct Load
t.Assert(errorHandled.Val(), false)
// Start watching - now errors during Load should trigger the error handler
err = loader.Watch(context.Background(), "test-error-handler")
t.AssertNil(err)
// Reset
errorHandled.Set(false)
// Trigger a config change - this will call Load internally and should trigger error handler
err = cfg.SetContent(configContent)
t.AssertNil(err)
// Wait for watcher to process the change
time.Sleep(1 * time.Second)
// Error handler should be called during Watch's Load
t.Assert(errorHandled.Val(), true)
})
}
func TestLoader_IsWatchingAndStopWatch(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Create a new config instance
cfg, err := gcfg.NewAdapterContent(configContent)
t.AssertNil(err)
// Create loader
loader := gcfg.NewLoaderWithAdapter[TestConfig](cfg, "")
// Initially, should not be watching
t.Assert(loader.IsWatching(), false)
// Load configuration
err = loader.Load(context.Background())
t.AssertNil(err)
// Start watching
err = loader.Watch(context.Background(), "test-stopwatch-watcher")
t.AssertNil(err)
// Now should be watching
t.Assert(loader.IsWatching(), true)
// Stop watching
stopped, err := loader.StopWatch(context.Background())
t.AssertNil(err)
t.Assert(stopped, true)
// Should not be watching anymore
t.Assert(loader.IsWatching(), false)
})
}
func TestLoader_StopWatchWithoutWatcher(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Create a new config instance
cfg, err := gcfg.NewAdapterContent(configContent)
t.AssertNil(err)
// Create loader without starting to watch
loader := gcfg.NewLoaderWithAdapter[TestConfig](cfg, "")
// Initially, should not be watching
t.Assert(loader.IsWatching(), false)
// Try to stop watching when not watching
stopped, err := loader.StopWatch(context.Background())
t.AssertNE(err, nil)
t.Assert(stopped, false)
t.Assert(err.Error(), "No watcher name specified")
})
}

View File

@ -36,8 +36,6 @@ type File struct {
}
var (
// checker is used for checking whether the value is nil.
checker = func(v *Pool) bool { return v == nil }
// Global file pointer pool.
pools = gmap.NewKVMapWithChecker[string, *Pool](checker, true)
pools = gmap.NewKVMap[string, *Pool](true)
)

View File

@ -82,12 +82,10 @@ const (
)
var (
callBacksChecker = func(v *glist.TList[*Callback]) bool { return v == nil } // callBacksChecker checks whether the value is nil.
callbackIdMapChecker = func(v *Callback) bool { return v == nil } // callbackIdMapChecker checks whether the value is nil.
mu sync.Mutex // Mutex for concurrent safety of defaultWatcher.
defaultWatcher *Watcher // Default watcher.
callbackIdMap = gmap.NewKVMapWithChecker[int, *Callback](callbackIdMapChecker, true) // Global callback id to callback function mapping.
callbackIdGenerator = gtype.NewInt() // Atomic id generator for callback.
mu sync.Mutex // Mutex for concurrent safety of defaultWatcher.
defaultWatcher *Watcher // Default watcher.
callbackIdMap = gmap.NewKVMap[int, *Callback](true) // Global callback id to callback function mapping.
callbackIdGenerator = gtype.NewInt() // Atomic id generator for callback.
)
// New creates and returns a new watcher.
@ -101,7 +99,7 @@ func New() (*Watcher, error) {
events: gqueue.NewTQueue[*Event](),
nameSet: gset.NewStrSet(true),
closeChan: make(chan struct{}),
callbacks: gmap.NewKVMapWithChecker[string, *glist.TList[*Callback]](callBacksChecker, true),
callbacks: gmap.NewKVMap[string, *glist.TList[*Callback]](true),
}
if watcher, err := fsnotify.NewWatcher(); err == nil {
w.watcher = watcher

View File

@ -14,10 +14,8 @@ const (
)
var (
// Checker function for instances map.
checker = func(v *Logger) bool { return v == nil }
// Instances map.
instances = gmap.NewKVMapWithChecker[string, *Logger](checker, true)
instances = gmap.NewKVMap[string, *Logger](true)
)
// Instance returns an instance of Logger with default settings.

View File

@ -12,8 +12,6 @@ import (
"github.com/gogf/gf/v2/container/gmap"
)
var checker = func(v *sync.RWMutex) bool { return v == nil }
// Locker is a memory based locker.
// Note that there's no cache expire mechanism for mutex in locker.
// You need remove certain mutex manually when you do not want use it anymore.
@ -25,7 +23,7 @@ type Locker struct {
// A memory locker can lock/unlock with dynamic string key.
func New() *Locker {
return &Locker{
m: gmap.NewKVMapWithChecker[string, *sync.RWMutex](checker, true),
m: gmap.NewKVMap[string, *sync.RWMutex](true),
}
}

View File

@ -43,11 +43,9 @@ const (
)
var (
// checker is used for checking whether the value is nil.
checker = func(v *gqueue.TQueue[*MsgRequest]) bool { return v == nil }
// commReceiveQueues is the group name to queue map for storing received data.
// The value of the map is type of *gqueue.TQueue[*MsgRequest].
commReceiveQueues = gmap.NewKVMapWithChecker[string, *gqueue.TQueue[*MsgRequest]](checker, true)
commReceiveQueues = gmap.NewKVMap[string, *gqueue.TQueue[*MsgRequest]](true)
// commPidFolderPath specifies the folder path storing pid to port mapping files.
commPidFolderPath string

View File

@ -14,10 +14,8 @@ const (
)
var (
// checker checks whether the value is nil.
checker = func(v *Resource) bool { return v == nil }
// Instances map.
instances = gmap.NewKVMapWithChecker[string, *Resource](checker, true)
instances = gmap.NewKVMap[string, *Resource](true)
)
// Instance returns an instance of Resource.

View File

@ -39,10 +39,8 @@ type SPathCacheItem struct {
}
var (
// checker is the checking function for checking the value is nil or not.
checker = func(v *SPath) bool { return v == nil }
// Path to searching object mapping, used for instance management.
pathsMap = gmap.NewKVMapWithChecker[string, *SPath](checker, true)
pathsMap = gmap.NewKVMap[string, *SPath](true)
)
// New creates and returns a new path searching manager.

View File

@ -41,8 +41,7 @@ const (
var (
// Default view object.
defaultViewObj *View
fileCacheItemChecker = func(v *fileCacheItem) bool { return v == nil }
defaultViewObj *View
)
// checkAndInitDefaultView checks and initializes the default view object.
@ -70,7 +69,7 @@ func New(path ...string) *View {
searchPaths: garray.NewStrArray(),
data: make(map[string]any),
funcMap: make(map[string]any),
fileCacheMap: gmap.NewKVMapWithChecker[string, *fileCacheItem](fileCacheItemChecker, true),
fileCacheMap: gmap.NewKVMap[string, *fileCacheItem](true),
config: DefaultConfig(),
}
if len(path) > 0 && len(path[0]) > 0 {

View File

@ -14,9 +14,8 @@ const (
)
var (
checker = func(v *View) bool { return v == nil }
// Instances map.
instances = gmap.NewKVMapWithChecker[string, *View](checker, true)
instances = gmap.NewKVMap[string, *View](true)
)
// Instance returns an instance of View with default settings.