Compare commits

..

27 Commits

Author SHA1 Message Date
3f1c2bcd29 Merge branch 'master' of github.com:gogf/gf into fix/4193-2 2026-05-18 20:36:26 +00:00
6a7b6e729b Merge branch 'master' of github.com:gogf/gf into fix/4193-2 2026-01-23 16:43:25 +08:00
73211707fb refactor(container): add default nil checker, rename RegisterNilChecker to SetNilChecker, migrate instance containers to type-safe generics (#4630)
## 变更说明

本 PR 主要对代码库进行了重构,以提升类型安全性和优化连接管理实现。

### 详细变更

#### 1. 数据库连接管理优化
- 修改 `RegisterNilChecker`方法返回实例以支持链式调用,涉及
`KVMap`、`ListKVMap`、`TSet`、`AVLKVTree`、`BKVTree`、`RedBlackKVTree`
等多个容器类型
- 更新 `Core`结构体中 `links`字段类型为类型安全的 `KVMap[ConfigNode, *sql.DB]`
- 添加专门的链接检查器函数用于连接池管理
- 使用泛型 `KVMap`替代原始 map 类型提升类型安全性
- 简化连接关闭逻辑并移除不必要的类型断言
- 优化统计功能中的迭代器实现提高性能

#### 2. 数据库驱动类型安全增强
- 将 dm、gaussdb、mssql、oracle 驱动中的 `conflictKeySet` 从 `gset.New`修改为
`gset.NewStrSet`
- 统一使用字符串集合类型以提高类型安全性

#### 3. 配置文件适配器类型安全改进
- 将 `jsonMap`从 `StrAnyMap` 类型更改为泛型 `KVMap[string, *gjson.Json]` 类型
- 添加 `jsonMapChecker` 函数用于 JSON 对象验证
- 使用 `NewKVMapWithChecker` 替代 `NewStrAnyMap` 提高类型安全性
- 简化数据库链接关闭日志中的键值转换逻辑

## 影响范围

- 数据库连接管理模块
- 多个数据库驱动实现
- 配置文件管理系统

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: John Guo <john@johng.cn>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-23 16:37:38 +08:00
609f44c5fe fix(cmd/gf): fix genservice losing versioned import paths (#4242) (#4638)
## Summary
- Fix `gf gen service` incorrectly handling versioned imports (e.g.,
`github.com/minio/minio-go/v7` → `github.com/minio/minio-go`)
- The root cause was faulty package name inference from import paths -
Go allows package names to differ from directory names
- Solution: Keep all non-anonymous imports and let gofmt clean up unused
ones

## Changes
- Simplified `calculateImportedItems` function in
`genservice_calculate.go`
- Added test case for versioned imports and aliased imports

## Test plan
- [x] All existing genservice tests pass (`Test_Gen_Service_Default`,
`Test_Issue3328`, `Test_Issue3835`)
- [x] New test `Test_Issue4242` verifies both versioned imports and
aliased imports are preserved
- [x] Verified generated files match expected output exactly

Closes #4242

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-22 20:45:19 +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
110e3fbf16 feat(cmd/gendao): add wildcard pattern support for tables configuration (#4632)
## Summary
- Add wildcard pattern support (`*` and `?`) for `tables` configuration
- Fix `tablesEx` wildcard to use exact match (`^$`) for consistency
- Add warning when exact table name does not exist
- Add unit tests and integration tests for MySQL and PostgreSQL

## Changes
| Configuration | Before | After |
|---------------|--------|-------|
| `tables: "user_*"` | Not supported | Matches tables starting with
"user_" |
| `tables: "*"` | Not supported | Matches all tables |
| `tablesEx: "user_*"` | Partial match | Exact match (consistent with
tables) |

## Features
- `*` matches any characters (e.g., `user_*` matches `user_info`,
`user_log`)
- `?` matches single character (e.g., `user_???` matches `user_log` but
not `user_info`)
- Mixed patterns and exact names supported (e.g., `tables:
"user_*,config"`)
- Non-existent exact table names are skipped with warning message

## Test plan
- [x] Unit tests for `containsWildcard`, `patternToRegex`,
`filterTablesByPatterns` (11 cases)
- [x] Integration tests for MySQL (5 cases)
- [x] Integration tests for PostgreSQL (1 case with tables + tablesEx)
- [x] Standard SQL syntax for cross-database compatibility

Closes #4629

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-21 19:16:12 +08:00
095c69c424 fix(cmd/gf): fix gf env and gf build --dumpEnv command error (#4635)
## Summary
- Fix `gf env` and `gf build --dumpEnv` command failing when `go env`
outputs warning messages
- When `go env` outputs warnings (e.g., invalid characters in
environment variables), it returns non-zero exit code but still provides
valid output
- The original code would fail in this case

## Changes
- Only fail when `go env` returns empty output, allow non-zero exit code
with valid output
- Skip lines that don't match `key=value` format instead of failing with
Fatal error
- Add debug log for skipped lines to help troubleshooting
- Add unit tests for env command

## Related Issue
Fixes #4469

## Test plan
- [x] `gf env` command works correctly even when `go env` outputs
warnings
- [x] `gf build --dumpEnv` works correctly
- [x] Added unit tests pass
2026-01-21 19:15:57 +08:00
cee6f499fc fix(cmd/gf): fix gf gen enums output path error when using relative path (#4636)
## Summary
- Fix `gf gen enums` output file created at wrong location when using
relative path
- Output was incorrectly relative to source directory instead of current
working directory
- Add `defer gfile.Chdir(originPwd)` to restore original working
directory

## Root Cause
The code calls `gfile.Chdir(realPath)` to change to source directory
before `gfile.PutContents(in.Path, ...)`, causing relative output path
to be resolved relative to source directory.

## Solution
- Convert output path to absolute using `gfile.Abs()` before `Chdir`
- Restore original working directory with `defer` (following `genpb.go`
pattern)

## Test Cases
- `Test_Gen_Enums_Issue4387_RelativePath` - standard project with
relative path
- `Test_Gen_Enums_AbsolutePath` - absolute path (should work as before)
- `Test_Gen_Enums_Issue4387_Monorepo` - monorepo mode (`cd app/xxx && gf
gen enums`)

Closes #4387
2026-01-21 19:15:42 +08:00
73560cfe31 fix(cmd/gendao): fix overlapping shardingPattern matching issue (#4631)
## Summary
- Fix overlapping shardingPattern matching issue where shorter patterns
incorrectly match tables meant for longer patterns
- Sort shardingPattern by length descending so longer (more specific)
patterns are matched first
- Add break after successful pattern match to prevent tables from
matching multiple patterns

## Problem
When `shardingPattern` contains overlapping prefixes like `["a_?",
"a_b_?", "a_c_?"]`:
- Tables `a_b_1`, `a_b_2` should match `a_b_?` and generate `a_b.go`
- Tables `a_c_1`, `a_c_2` should match `a_c_?` and generate `a_c.go`
- Tables `a_1`, `a_2` should match `a_?` and generate `a.go`

But without this fix, `a_?` (converted to regex `a_(.+)`) would match
`a_b_1` first, causing `a_b_?` and `a_c_?` patterns to fail to generate
their respective dao files.

## Solution
1. Sort `shardingPattern` by length descending before matching
2. Add `break` after a table matches a pattern to prevent multiple
matches

## Test plan
- [x] Added integration test `Test_Gen_Dao_Sharding_Overlapping` with
overlapping patterns
- [x] Added SQL test data file `sharding_overlapping.sql`
- [x] Verified 3 separate dao files are generated: `a.go`, `a_b.go`,
`a_c.go`

Fixes #4603
2026-01-21 19:15:06 +08:00
9a7df9944c revert(os/gcfg): restore config file priority over env/cmd in GetWithEnv and GetWithCmd (#4647)
## Summary
- Reverts the behavior change introduced in PR #4587 (commit caea7ea4b)
- Restores v2.9.7 priority behavior:
  - `GetWithEnv`: config file > environment variable > default value
  - `GetWithCmd`: config file > command line option > default value

## Related Issue
Closes #4074

## Changes
- `os/gcfg/gcfg.go`: Restore original logic that checks config file
first, then falls back to env/cmd
- `os/gcfg/gcfg_z_example_test.go`: Restore original example test
expectations
2026-01-21 19:14:03 +08:00
dd02af1b2f test(cmd/gf): enhance integration tests for gen service command (#4645)
## Summary
- Add 2 new integration test cases for `gf gen service` command
- `Test_Gen_Service_CamelCase`: tests `DstFileNameCase: "Camel"` option
to generate service files with CamelCase naming
- `Test_Gen_Service_PackagesFilter`: tests `Packages` filter option to
generate service files only for specified packages

## Test Plan
- [x] Run `go test -v -run "Test_Gen_Service" ./...` - all 5 tests pass
(3 existing + 2 new)
2026-01-21 19:12:37 +08:00
626fc629ef test(cmd/gf): enhance integration tests for gen pb command (#4644)
## Summary
- Add 2 new integration test cases for `gf gen pb` command
- `TestGenPb_MultipleTags`: tests multiple validation tags (v:required,
v:#Id > 0, v:email) and dc tags
- `TestGenPb_NestedMessage`: tests nested message structures with
various tag types

## Test Data
- Add `testdata/genpb/multiple_tags.proto` - proto file with multiple
tag annotations
- Add `testdata/genpb/nested_message.proto` - proto file with nested
message structures

## Test Plan
- [x] Run `go test -v -run "TestGenPb" ./...` - all 4 tests pass (2
existing + 2 new)
2026-01-21 19:11:45 +08:00
2d05fb426f test(cmd/gf): enhance unit tests for fix command (#4643)
## Summary
- Enhance unit tests for the `fix` command's `doFixV25Content` function
- 5 new test cases added (total: 6)

## New Test Cases

| Test | Description |
|------|-------------|
| Test_Fix_doFixV25Content_WithReplacement | Verify actual replacement
is made |
| Test_Fix_doFixV25Content_NoMatch | Handle content without patterns |
| Test_Fix_doFixV25Content_MultipleMatches | Handle multiple occurrences
|
| Test_Fix_doFixV25Content_EmptyContent | Handle empty content |
| Test_Fix_doFixV25Content_ComplexPath | Handle complex URL paths |

## Test plan
- [x] All 6 tests pass locally
- [x] Only added new test cases to existing test file
- [x] No modifications to non-test code
2026-01-21 19:10:56 +08:00
bf2997e9cc test(cmd/gf): add unit tests for pack command (#4642)
## Summary
- Add comprehensive unit tests for the `pack` command which handles
resource file packing
- 8 new test cases covering core functionality

## Test Coverage

| Test | Description |
|------|-------------|
| Test_Pack_ToGoFile | Pack files to .go file |
| Test_Pack_ToBinaryFile | Pack files to binary file |
| Test_Pack_MultipleSources | Pack multiple source directories |
| Test_Pack_WithPrefix | Pack with prefix option |
| Test_Pack_WithKeepPath | Pack with keepPath option |
| Test_Pack_AutoPackageName | Auto-detect package name from directory |
| Test_Pack_EmptySource | Handle empty source directory |
| Test_Pack_NestedDirectories | Handle deeply nested directory structure
|

## Test plan
- [x] All 8 tests pass locally
- [x] No modifications to existing code
- [x] New test file only: `cmd_z_unit_pack_test.go`
2026-01-21 19:10:20 +08:00
82d4d77e56 test(cmd/gf): add unit tests for genenums package (#4641)
## Summary
- Add comprehensive unit tests for the `genenums` package which handles
enum parsing and JSON export
- 13 new test cases covering core functionality

## Test Coverage

| Function | Tests | Description |
|----------|-------|-------------|
| `NewEnumsParser` | 2 | Parser initialization |
| `Export` | 7 | JSON export with various types |
| `ParsePackages` | 2 | Integration with Go packages |
| `EnumItem` | 1 | Data structure |
| `getStandardPackages` | 1 | Standard library detection |

## Test plan
- [x] All 13 tests pass locally
- [x] No modifications to existing code
- [x] New test file only: `genenums_z_unit_test.go`
2026-01-21 19:09:38 +08:00
4f43b40a18 test(cmd/gf): add unit tests for geninit package (#4640)
## Summary
- Add comprehensive unit tests for the `geninit` package which handles
project initialization from templates
- 17 new test cases covering core functionality

## Test Coverage

| Function | Tests | Description |
|----------|-------|-------------|
| `ParseGitURL` | 7 | Git URL parsing with various formats |
| `IsSubdirRepo` | 3 | Subdirectory detection |
| `GetModuleNameFromGoMod` | 3 | Module name extraction |
| `ASTReplacer` | 2 | Import path replacement |
| `findGoFiles` | 2 | Go file discovery |

## Test plan
- [x] All 17 tests pass locally
- [x] No modifications to existing code
- [x] New test file only: `geninit_z_unit_test.go`
2026-01-21 19:07:52 +08:00
f3f2cb3c57 refactor(encoding/gjson): enhance auto type checks when loading data without type specified (#4637)
This pull request improves YAML support for i18n translation files and
refactors content type detection and loading logic in the `gjson`
package. The main changes include more robust detection of YAML, TOML,
INI, and Properties formats, refactoring of content type handling, and
the addition of new tests to ensure correct parsing of YAML-based i18n
resources.

### Improved content type detection and loading

* Refactored content type detection logic in `gjson` to use dedicated
functions for XML, YAML, TOML, INI, and Properties formats, making the
detection more reliable and maintainable.
* Changed the content loading mechanism in `gjson` to use specific
decode functions (`gxml.Decode`, `gyaml.Decode`, etc.) for each format
instead of converting everything to JSON first, improving accuracy and
extensibility.
* Updated type definitions and struct field comments in `gjson.go` for
clarity and consistency, including changing `ContentType` to a type
alias and improving documentation.
[[1]](diffhunk://#diff-0e4432d7e4cf171c0339e01b1842530432b986948d7f839a155543623236a03fL24-R24)
[[2]](diffhunk://#diff-0e4432d7e4cf171c0339e01b1842530432b986948d7f839a155543623236a03fL38-R71)

### i18n YAML support

* Modified i18n manager to use the new `gjson.LoadPath` method for
loading translation files, ensuring correct parsing of YAML files for
i18n.
* Added new test cases and test data for loading and verifying YAML i18n
files, including edge cases and real-world translation strings.
[[1]](diffhunk://#diff-e6eacc5abab33c149f9b39d8ebe300cf4d0abe907434605991984a5969e8707dR262-R283)
[[2]](diffhunk://#diff-1bfd438797c1f9ef18ab3cb00d23ae95202e85e2362c39c3df4f1a29c55733feR421-R430)
[[3]](diffhunk://#diff-a3ee37ff2a67c9e1ba2e1617e0f5fd63eb261ad7760a07423f703538138c2decR1-R16)

### Minor improvements

* Simplified file loading logic in `gjson.LoadPath` by removing caching
and directly reading file bytes, which streamlines the code and avoids
potential cache issues.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-20 19:25:23 +08:00
102c3b6cb0 fix(util/gconv): fix incompatable converting to nil pointer target from older version implement (#4224)
fixed: https://github.com/gogf/gf/issues/4218
2026-01-20 10:57:32 +08:00
07662ce9a8 Update util/gconv/internal/converter/converter_struct.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-05 10:17:16 +08:00
50c5c33367 fix: 修复 Test_Issue4193 测试用例的结构问题 2025-09-05 10:15:45 +08:00
91c6f25dd1 Merge branch 'master' into fix/4193-2 2025-09-05 10:11:19 +08:00
15bbcc8f53 Merge branch 'master' into fix/4193-2 2025-03-27 10:54:40 +08:00
53f6697d4b style: 移除非必要修改 2025-03-27 10:41:12 +08:00
c9939fcf7f up 2025-03-26 22:27:48 +08:00
a9953a4729 up 2025-03-26 21:32:56 +08:00
59941167ea fix(net/ghttp): panic when post empty string parameter to *ghttp.UploadFile 2025-03-26 21:26:09 +08:00
96 changed files with 3910 additions and 2260 deletions

View File

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

3
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -156,3 +156,130 @@ func Test_Issue3835(t *testing.T) {
t.Assert(gfile.GetContents(genFile), gfile.GetContents(expectFile))
})
}
func Test_Gen_Service_CamelCase(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
path = gfile.Temp(guid.S())
dstFolder = path + filepath.FromSlash("/service")
srvFolder = gtest.DataPath("genservice", "logic")
in = genservice.CGenServiceInput{
SrcFolder: srvFolder,
DstFolder: dstFolder,
DstFileNameCase: "Camel",
WatchFile: "",
StPattern: "",
Packages: nil,
ImportPrefix: "",
Clear: false,
}
)
err := gutil.FillStructWithDefault(&in)
t.AssertNil(err)
err = gfile.Mkdir(path)
t.AssertNil(err)
defer gfile.Remove(path)
// Clean up generated logic.go
genSrv := srvFolder + filepath.FromSlash("/logic.go")
defer gfile.Remove(genSrv)
_, err = genservice.CGenService{}.Service(ctx, in)
t.AssertNil(err)
// Files should be in CamelCase
files, err := gfile.ScanDir(dstFolder, "*.go", true)
t.AssertNil(err)
t.Assert(files, []string{
dstFolder + filepath.FromSlash("/Article.go"),
dstFolder + filepath.FromSlash("/Base.go"),
dstFolder + filepath.FromSlash("/Delivery.go"),
dstFolder + filepath.FromSlash("/User.go"),
})
})
}
func Test_Gen_Service_PackagesFilter(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
path = gfile.Temp(guid.S())
dstFolder = path + filepath.FromSlash("/service")
srvFolder = gtest.DataPath("genservice", "logic")
in = genservice.CGenServiceInput{
SrcFolder: srvFolder,
DstFolder: dstFolder,
DstFileNameCase: "Snake",
WatchFile: "",
StPattern: "",
Packages: []string{"user"},
ImportPrefix: "",
Clear: false,
}
)
err := gutil.FillStructWithDefault(&in)
t.AssertNil(err)
err = gfile.Mkdir(path)
t.AssertNil(err)
defer gfile.Remove(path)
// Clean up generated logic.go
genSrv := srvFolder + filepath.FromSlash("/logic.go")
defer gfile.Remove(genSrv)
_, err = genservice.CGenService{}.Service(ctx, in)
t.AssertNil(err)
// Only user.go should be generated
files, err := gfile.ScanDir(dstFolder, "*.go", true)
t.AssertNil(err)
t.Assert(len(files), 1)
t.Assert(files[0], dstFolder+filepath.FromSlash("/user.go"))
})
}
// https://github.com/gogf/gf/issues/4242
// Test that versioned imports and aliased imports are correctly preserved.
// The issue is that imports like "github.com/minio/minio-go/v7" were being
// incorrectly handled because the package name (minio) differs from
// the directory name (minio-go).
func Test_Issue4242(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
path = gfile.Temp(guid.S())
dstFolder = path + filepath.FromSlash("/service")
srvFolder = gtest.DataPath("issue", "4242", "logic")
in = genservice.CGenServiceInput{
SrcFolder: srvFolder,
DstFolder: dstFolder,
DstFileNameCase: "Snake",
WatchFile: "",
StPattern: "",
Packages: nil,
ImportPrefix: "",
Clear: false,
}
)
err := gutil.FillStructWithDefault(&in)
t.AssertNil(err)
err = gfile.Mkdir(path)
t.AssertNil(err)
defer gfile.Remove(path)
_, err = genservice.CGenService{}.Service(ctx, in)
t.AssertNil(err)
// Test versioned imports
t.Assert(
gfile.GetContents(dstFolder+filepath.FromSlash("/issue_4242.go")),
gfile.GetContents(gtest.DataPath("issue", "4242", "service", "issue_4242.go")),
)
// Test aliased imports
t.Assert(
gfile.GetContents(dstFolder+filepath.FromSlash("/issue_4242_alias.go")),
gfile.GetContents(gtest.DataPath("issue", "4242", "service", "issue_4242_alias.go")),
)
})
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,7 +12,6 @@ import (
"github.com/gogf/gf/v2/container/garray"
"github.com/gogf/gf/v2/container/gmap"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
@ -37,21 +36,14 @@ func (c CGenService) calculateImportedItems(
}
for _, item := range pkgItems {
alias := item.Alias
// If the alias is _, it means that the package is not generated.
if alias == "_" {
// Skip anonymous imports
if item.Alias == "_" {
mlog.Debugf(`ignore anonymous package: %s`, item.RawImport)
continue
}
// If the alias is empty, it will use the package name as the alias.
if alias == "" {
alias = gfile.Basename(gstr.Trim(item.Path, `"`))
}
if !gstr.Contains(allFuncParamType.String(), alias) {
mlog.Debugf(`ignore unused package: %s`, item.RawImport)
continue
}
// Keep all imports, let gofmt clean up unused ones.
// We cannot accurately infer package name from import path
// (e.g., path "minio-go" but package name is "minio").
srcImportedPackages.Add(item.RawImport)
}
return nil

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,34 @@
package issue4242
import (
"context"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/issue/4242/service"
"github.com/gogf/gf/contrib/drivers/mysql/v2"
)
func init() {
service.RegisterIssue4242(New())
}
type sIssue4242 struct {
}
func New() *sIssue4242 {
return &sIssue4242{}
}
// GetDriver tests versioned import path is preserved.
func (s *sIssue4242) GetDriver(ctx context.Context) (d mysql.Driver, err error) {
return mysql.Driver{}, nil
}
// GetRequest tests another versioned import.
func (s *sIssue4242) GetRequest(ctx context.Context) (*ghttp.Request, error) {
g.Log().Info(ctx, "getting request")
return nil, nil
}

View File

@ -0,0 +1,37 @@
package issue4242alias
import (
"context"
// Anonymous import (should be skipped)
_ "github.com/gogf/gf/v2/os/gres"
// Versioned import without alias
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/issue/4242/service"
// Explicit alias import
mysqlDriver "github.com/gogf/gf/contrib/drivers/mysql/v2"
)
func init() {
service.RegisterIssue4242Alias(New())
}
type sIssue4242Alias struct {
}
func New() *sIssue4242Alias {
return &sIssue4242Alias{}
}
// GetDriver tests explicit alias import.
func (s *sIssue4242Alias) GetDriver(ctx context.Context) (d mysqlDriver.Driver, err error) {
return mysqlDriver.Driver{}, nil
}
// GetRequest tests versioned import.
func (s *sIssue4242Alias) GetRequest(ctx context.Context) (*ghttp.Request, error) {
return nil, nil
}

View File

@ -0,0 +1,10 @@
// ==========================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// ==========================================================================
package logic
import (
_ "github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/issue/4242/logic/issue4242"
_ "github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/issue/4242/logic/issue4242alias"
)

View File

@ -0,0 +1,37 @@
// ================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// You can delete these comments if you wish manually maintain this interface file.
// ================================================================================
package service
import (
"context"
"github.com/gogf/gf/contrib/drivers/mysql/v2"
"github.com/gogf/gf/v2/net/ghttp"
)
type (
IIssue4242 interface {
// GetDriver tests versioned import path is preserved.
GetDriver(ctx context.Context) (d mysql.Driver, err error)
// GetRequest tests another versioned import.
GetRequest(ctx context.Context) (*ghttp.Request, error)
}
)
var (
localIssue4242 IIssue4242
)
func Issue4242() IIssue4242 {
if localIssue4242 == nil {
panic("implement not found for interface IIssue4242, forgot register?")
}
return localIssue4242
}
func RegisterIssue4242(i IIssue4242) {
localIssue4242 = i
}

View File

@ -0,0 +1,37 @@
// ================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// You can delete these comments if you wish manually maintain this interface file.
// ================================================================================
package service
import (
"context"
mysqlDriver "github.com/gogf/gf/contrib/drivers/mysql/v2"
"github.com/gogf/gf/v2/net/ghttp"
)
type (
IIssue4242Alias interface {
// GetDriver tests explicit alias import.
GetDriver(ctx context.Context) (d mysqlDriver.Driver, err error)
// GetRequest tests versioned import.
GetRequest(ctx context.Context) (*ghttp.Request, error)
}
)
var (
localIssue4242Alias IIssue4242Alias
)
func Issue4242Alias() IIssue4242Alias {
if localIssue4242Alias == nil {
panic("implement not found for interface IIssue4242Alias, forgot register?")
}
return localIssue4242Alias
}
func RegisterIssue4242Alias(i IIssue4242Alias) {
localIssue4242Alias = i
}

View File

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

View File

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

View File

@ -749,7 +749,9 @@ func (a *TArray[T]) String() string {
}
// MarshalJSON implements the interface MarshalJSON for json.Marshal.
// Note that do not use pointer as its receiver here.
// DO NOT change this receiver to pointer type, as the TArray can be used as a var defined variable, like:
// var a TArray[int]
// Please refer to corresponding tests for more details.
func (a TArray[T]) MarshalJSON() ([]byte, error) {
a.mu.RLock()
defer a.mu.RUnlock()

View File

@ -46,7 +46,7 @@ func NewSortedTArray[T comparable](comparator func(a, b T) int, safe ...bool) *S
return NewSortedTArraySize(0, comparator, safe...)
}
// NewSortedTArraySize create and returns an sorted array with given size and cap.
// NewSortedTArraySize create and returns a sorted array with given size and cap.
// The parameter `safe` is used to specify whether using array in concurrent-safety,
// which is false in default.
func NewSortedTArraySize[T comparable](cap int, comparator func(a, b T) int, safe ...bool) *SortedTArray[T] {
@ -718,7 +718,9 @@ func (a *SortedTArray[T]) String() string {
}
// MarshalJSON implements the interface MarshalJSON for json.Marshal.
// Note that do not use pointer as its receiver here.
// DO NOT change this receiver to pointer type, as the TArray can be used as a var defined variable, like:
// var a SortedTArray[int]
// Please refer to corresponding tests for more details.
func (a SortedTArray[T]) MarshalJSON() ([]byte, error) {
a.mu.RLock()
defer a.mu.RUnlock()

View File

@ -22,8 +22,11 @@ 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
mu rwmutex.RWMutex
data map[K]V
// nilChecker is the custom nil checker function.
// It uses empty.IsNil if it's nil.
nilChecker NilChecker[V]
}
@ -58,15 +61,15 @@ func NewKVMapFrom[K comparable, V any](data map[K]V, safe ...bool) *KVMap[K, V]
// 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)
m.SetNilChecker(checker)
return m
}
// RegisterNilChecker registers a custom nil checker function for the map values.
// SetNilChecker 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]) {
func (m *KVMap[K, V]) SetNilChecker(nilChecker NilChecker[V]) {
m.mu.Lock()
defer m.mu.Unlock()
m.nilChecker = nilChecker
@ -74,12 +77,12 @@ func (m *KVMap[K, V]) RegisterNilChecker(nilChecker NilChecker[V]) {
// 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.
// otherwise it falls back to the default empty.IsNil function.
func (m *KVMap[K, V]) isNil(v V) bool {
if m.nilChecker != nil {
return m.nilChecker(v)
}
return any(v) == nil
return empty.IsNil(v)
}
// Iterator iterates the hash map readonly with custom callback function `f`.
@ -242,11 +245,12 @@ func (m *KVMap[K, V]) Pops(size int) map[K]V {
return newMap
}
// doSetWithLockCheck checks whether value of the key exists with mutex.Lock,
// if not exists, set value to the map with given `key`,
// or else just return the existing value.
// doSetWithLockCheck sets value with given `value` if it does not exist,
// and then returns this value and whether it exists.
//
// It returns value with given `key`.
// It is a helper function for GetOrSet* functions.
//
// Note that, it does not add the value to the map if the given `value` is nil.
func (m *KVMap[K, V]) doSetWithLockCheck(key K, value V) (val V, ok bool) {
m.mu.Lock()
defer m.mu.Unlock()
@ -274,6 +278,8 @@ func (m *KVMap[K, V]) GetOrSet(key K, value V) V {
// GetOrSetFunc returns the value by key,
// or sets value with returned value of callback function `f` if it does not exist
// and then returns this value.
//
// Note that, it does not add the value to the map if the returned value of `f` is nil.
func (m *KVMap[K, V]) GetOrSetFunc(key K, f func() V) V {
v, _ := m.doSetWithLockCheck(key, f())
return v
@ -285,6 +291,8 @@ func (m *KVMap[K, V]) GetOrSetFunc(key K, f func() V) V {
//
// GetOrSetFuncLock differs with GetOrSetFunc function is that it executes function `f`
// with mutex.Lock of the hash map.
//
// Note that, it does not add the value to the map if the returned value of `f` is nil.
func (m *KVMap[K, V]) GetOrSetFuncLock(key K, f func() V) V {
m.mu.Lock()
defer m.mu.Unlock()
@ -524,6 +532,9 @@ func (m *KVMap[K, V]) String() string {
}
// MarshalJSON implements the interface MarshalJSON for json.Marshal.
// DO NOT change this receiver to pointer type, as the KVMap can be used as a var defined variable, like:
// var m gmap.KVMap[int, string]
// Please refer to corresponding tests for more details.
func (m KVMap[K, V]) MarshalJSON() ([]byte, error) {
return json.Marshal(gconv.Map(m.Map()))
}

View File

@ -56,7 +56,7 @@ func NewListKVMap[K comparable, V any](safe ...bool) *ListKVMap[K, V] {
// 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)
m.SetNilChecker(checker)
return m
}
@ -81,11 +81,11 @@ func NewListKVMapWithCheckerFrom[K comparable, V any](data map[K]V, nilChecker N
return m
}
// RegisterNilChecker registers a custom nil checker function for the map values.
// SetNilChecker 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]) {
func (m *ListKVMap[K, V]) SetNilChecker(nilChecker NilChecker[V]) {
m.mu.Lock()
defer m.mu.Unlock()
m.nilChecker = nilChecker
@ -93,12 +93,12 @@ func (m *ListKVMap[K, V]) RegisterNilChecker(nilChecker NilChecker[V]) {
// 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.
// otherwise it falls back to the default empty.IsNil function.
func (m *ListKVMap[K, V]) isNil(v V) bool {
if m.nilChecker != nil {
return m.nilChecker(v)
}
return any(v) == nil
return empty.IsNil(v)
}
// Iterator is alias of IteratorAsc.
@ -402,6 +402,8 @@ func (m *ListKVMap[K, V]) GetVarOrSetFuncLock(key K, f func() V) *gvar.Var {
// SetIfNotExist sets `value` to the map if the `key` does not exist, and then returns true.
// It returns false if `key` exists, and `value` would be ignored.
//
// Note that it does not add the value to the map if `value` is nil.
func (m *ListKVMap[K, V]) SetIfNotExist(key K, value V) bool {
m.mu.Lock()
defer m.mu.Unlock()
@ -421,6 +423,8 @@ func (m *ListKVMap[K, V]) SetIfNotExist(key K, value V) bool {
// SetIfNotExistFunc sets value with return value of callback function `f`, and then returns true.
// It returns false if `key` exists, and `value` would be ignored.
//
// Note that, it does not add the value to the map if the returned value of `f` is nil.
func (m *ListKVMap[K, V]) SetIfNotExistFunc(key K, f func() V) bool {
m.mu.Lock()
defer m.mu.Unlock()
@ -444,6 +448,8 @@ func (m *ListKVMap[K, V]) SetIfNotExistFunc(key K, f func() V) bool {
//
// SetIfNotExistFuncLock differs with SetIfNotExistFunc function is that
// it executes function `f` with mutex.Lock of the map.
//
// Note that, it does not add the value to the map if the returned value of `f` is nil.
func (m *ListKVMap[K, V]) SetIfNotExistFuncLock(key K, f func() V) bool {
m.mu.Lock()
defer m.mu.Unlock()
@ -609,6 +615,9 @@ func (m *ListKVMap[K, V]) String() string {
}
// MarshalJSON implements the interface MarshalJSON for json.Marshal.
// DO NOT change this receiver to pointer type, as the ListKVMap can be used as a var defined variable, like:
// var m gmap.ListKVMap[string]string
// Please refer to corresponding tests for more details.
func (m ListKVMap[K, V]) MarshalJSON() (jsonBytes []byte, err error) {
if m.data == nil {
return []byte("{}"), nil

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) {
@ -1647,9 +1654,10 @@ func Test_KVMap_TypedNil(t *testing.T) {
return nil
})
}
t.Assert(m1.Size(), 10)
t.Assert(m1.Size(), 5)
m2 := gmap.NewKVMap[int, *Student](true)
m2.RegisterNilChecker(func(student *Student) bool {
m2.SetNilChecker(func(student *Student) bool {
return student == nil
})
for i := 0; i < 10; i++ {
@ -1679,7 +1687,8 @@ func Test_NewKVMapWithChecker_TypedNil(t *testing.T) {
return nil
})
}
t.Assert(m1.Size(), 10)
t.Assert(m1.Size(), 5)
m2 := gmap.NewKVMapWithChecker[int, *Student](func(student *Student) bool {
return student == nil
}, true)

View File

@ -183,77 +183,6 @@ func Test_ListKVMap_SetIfNotExistFuncLock_MultipleKeys(t *testing.T) {
})
}
// Test_ListKVMap_GetOrSetFuncLock_NilValue tests that nil values are handled correctly.
func Test_ListKVMap_GetOrSetFuncLock_NilValue(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
m := gmap.NewListKVMap[string, *int](true)
key := "nilKey"
callCount := int32(0)
var wg sync.WaitGroup
goroutines := 50
wg.Add(goroutines)
for i := 0; i < goroutines; i++ {
go func() {
defer wg.Done()
m.GetOrSetFuncLock(key, func() *int {
atomic.AddInt32(&callCount, 1)
return nil
})
}()
}
wg.Wait()
// Callback should be called once
t.Assert(atomic.LoadInt32(&callCount), 1)
// Typed nil pointer (*int)(nil) is stored because any(value) != nil for typed nil
// This is a Go language feature: typed nil is not the same as interface nil
t.Assert(m.Contains(key), true)
t.Assert(m.Get(key), (*int)(nil))
t.Assert(m.Size(), 1)
})
}
// Test_ListKVMap_SetIfNotExistFuncLock_NilValue tests that nil values are handled correctly.
func Test_ListKVMap_SetIfNotExistFuncLock_NilValue(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
m := gmap.NewListKVMap[string, *string](true)
key := "nilKey"
callCount := int32(0)
successCount := int32(0)
var wg sync.WaitGroup
goroutines := 50
wg.Add(goroutines)
for i := 0; i < goroutines; i++ {
go func() {
defer wg.Done()
success := m.SetIfNotExistFuncLock(key, func() *string {
atomic.AddInt32(&callCount, 1)
return nil
})
if success {
atomic.AddInt32(&successCount, 1)
}
}()
}
wg.Wait()
// Callback should be called once
t.Assert(atomic.LoadInt32(&callCount), 1)
// Should report success once
t.Assert(atomic.LoadInt32(&successCount), 1)
// Typed nil pointer (*string)(nil) is stored because any(value) != nil for typed nil
t.Assert(m.Contains(key), true)
t.Assert(m.Get(key), (*string)(nil))
t.Assert(m.Size(), 1)
})
}
// Test_ListKVMap_GetOrSetFuncLock_ExistingKey tests behavior when key already exists.
func Test_ListKVMap_GetOrSetFuncLock_ExistingKey(t *testing.T) {
gtest.C(t, func(t *gtest.T) {

View File

@ -1159,6 +1159,13 @@ func Test_ListKVMap_MarshalJSON_Error(t *testing.T) {
t.AssertNil(err)
t.Assert(string(b), `{"a":"1"}`)
})
gtest.C(t, func(t *gtest.T) {
var m gmap.ListKVMap[int, int]
m.Set(1, 10)
b, err := json.Marshal(m)
t.AssertNil(err)
t.Assert(string(b), `{"1":10}`)
})
}
// Test empty map operations
@ -1358,9 +1365,10 @@ func Test_ListKVMap_TypedNil(t *testing.T) {
return nil
})
}
t.Assert(m1.Size(), 10)
t.Assert(m1.Size(), 5)
m2 := gmap.NewListKVMap[int, *Student](true)
m2.RegisterNilChecker(func(student *Student) bool {
m2.SetNilChecker(func(student *Student) bool {
return student == nil
})
for i := 0; i < 10; i++ {
@ -1390,7 +1398,8 @@ func Test_NewListKVMapWithChecker_TypedNil(t *testing.T) {
return nil
})
}
t.Assert(m1.Size(), 10)
t.Assert(m1.Size(), 5)
m2 := gmap.NewListKVMapWithChecker[int, *Student](func(student *Student) bool {
return student == nil
}, true)

View File

@ -9,6 +9,7 @@ package gset
import (
"bytes"
"github.com/gogf/gf/v2/internal/empty"
"github.com/gogf/gf/v2/internal/json"
"github.com/gogf/gf/v2/internal/rwmutex"
"github.com/gogf/gf/v2/text/gstr"
@ -39,7 +40,7 @@ func NewTSet[T comparable](safe ...bool) *TSet[T] {
// 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)
s.SetNilChecker(checker)
return s
}
@ -66,11 +67,11 @@ func NewTSetWithCheckerFrom[T comparable](items []T, checker NilChecker[T], safe
return set
}
// RegisterNilChecker registers a custom nil checker function for the set elements.
// SetNilChecker 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]) {
func (set *TSet[T]) SetNilChecker(nilChecker NilChecker[T]) {
set.mu.Lock()
defer set.mu.Unlock()
set.nilChecker = nilChecker
@ -78,12 +79,12 @@ func (set *TSet[T]) RegisterNilChecker(nilChecker NilChecker[T]) {
// 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.
// otherwise it falls back to the default empty.IsNil function.
func (set *TSet[T]) isNil(v T) bool {
if set.nilChecker != nil {
return set.nilChecker(v)
}
return any(v) == nil
return empty.IsNil(v)
}
// Iterator iterates the set readonly with given callback function `f`,
@ -109,7 +110,7 @@ func (set *TSet[T]) Add(items ...T) {
}
// AddIfNotExist checks whether item exists in the set,
// it adds the item to set and returns true if it does not exists in the set,
// it adds the item to set and returns true if it does not exist in the set,
// or else it does nothing and returns false.
//
// Note that, if `item` is nil, it does nothing and returns false.

View File

@ -601,10 +601,10 @@ func Test_TSet_TypedNil(t *testing.T) {
set := gset.NewTSet[*Student](true)
var s *Student = nil
exist := set.AddIfNotExist(s)
t.Assert(exist, true)
t.Assert(exist, false)
set2 := gset.NewTSet[*Student](true)
set2.RegisterNilChecker(func(student *Student) bool {
set2.SetNilChecker(func(student *Student) bool {
return student == nil
})
exist2 := set2.AddIfNotExist(s)
@ -621,7 +621,7 @@ func Test_NewTSetWithChecker_TypedNil(t *testing.T) {
set := gset.NewTSet[*Student](true)
var s *Student = nil
exist := set.AddIfNotExist(s)
t.Assert(exist, true)
t.Assert(exist, false)
set2 := gset.NewTSetWithChecker[*Student](func(student *Student) bool {
return student == nil

View File

@ -12,6 +12,7 @@ import (
"github.com/emirpasic/gods/v2/trees/avltree"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/internal/empty"
"github.com/gogf/gf/v2/internal/json"
"github.com/gogf/gf/v2/internal/rwmutex"
"github.com/gogf/gf/v2/text/gstr"
@ -52,7 +53,7 @@ func NewAVLKVTree[K comparable, V any](comparator func(v1, v2 K) int, safe ...bo
// 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)
t.SetNilChecker(checker)
return t
}
@ -78,11 +79,11 @@ func NewAVLKVTreeWithCheckerFrom[K comparable, V any](comparator func(v1, v2 K)
return tree
}
// RegisterNilChecker registers a custom nil checker function for the map values.
// SetNilChecker 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]) {
func (tree *AVLKVTree[K, V]) SetNilChecker(nilChecker NilChecker[V]) {
tree.mu.Lock()
defer tree.mu.Unlock()
tree.nilChecker = nilChecker
@ -90,12 +91,12 @@ func (tree *AVLKVTree[K, V]) RegisterNilChecker(nilChecker NilChecker[V]) {
// 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 {
// otherwise it falls back to the default empty.IsNil function.
func (tree *AVLKVTree[K, V]) isNil(v V) bool {
if tree.nilChecker != nil {
return tree.nilChecker(value)
return tree.nilChecker(v)
}
return any(value) == nil
return empty.IsNil(v)
}
// Clone clones and returns a new tree from current tree.

View File

@ -12,6 +12,7 @@ import (
"github.com/emirpasic/gods/v2/trees/btree"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/internal/empty"
"github.com/gogf/gf/v2/internal/json"
"github.com/gogf/gf/v2/internal/rwmutex"
"github.com/gogf/gf/v2/text/gstr"
@ -51,7 +52,7 @@ func NewBKVTree[K comparable, V any](m int, comparator func(v1, v2 K) int, safe
// 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)
t.SetNilChecker(checker)
return t
}
@ -77,11 +78,11 @@ func NewBKVTreeWithCheckerFrom[K comparable, V any](m int, comparator func(v1, v
return tree
}
// RegisterNilChecker registers a custom nil checker function for the map values.
// SetNilChecker 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]) {
func (tree *BKVTree[K, V]) SetNilChecker(nilChecker NilChecker[V]) {
tree.mu.Lock()
defer tree.mu.Unlock()
tree.nilChecker = nilChecker
@ -89,12 +90,12 @@ func (tree *BKVTree[K, V]) RegisterNilChecker(nilChecker NilChecker[V]) {
// 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 {
// otherwise it falls back to the default empty.IsNil function.
func (tree *BKVTree[K, V]) isNil(v V) bool {
if tree.nilChecker != nil {
return tree.nilChecker(value)
return tree.nilChecker(v)
}
return any(value) == nil
return empty.IsNil(v)
}
// Clone clones and returns a new tree from current tree.

View File

@ -12,6 +12,7 @@ import (
"github.com/emirpasic/gods/v2/trees/redblacktree"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/internal/empty"
"github.com/gogf/gf/v2/internal/json"
"github.com/gogf/gf/v2/internal/rwmutex"
"github.com/gogf/gf/v2/text/gstr"
@ -47,7 +48,7 @@ func NewRedBlackKVTree[K comparable, V any](comparator func(v1, v2 K) int, safe
// 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)
t.SetNilChecker(checker)
return t
}
@ -96,11 +97,11 @@ func RedBlackKVTreeInitFrom[K comparable, V any](tree *RedBlackKVTree[K, V], com
}
}
// RegisterNilChecker registers a custom nil checker function for the map values.
// SetNilChecker 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]) {
func (tree *RedBlackKVTree[K, V]) SetNilChecker(nilChecker NilChecker[V]) {
tree.mu.Lock()
defer tree.mu.Unlock()
tree.nilChecker = nilChecker
@ -108,12 +109,12 @@ func (tree *RedBlackKVTree[K, V]) RegisterNilChecker(nilChecker NilChecker[V]) {
// 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 {
// otherwise it falls back to the default empty.IsNil function.
func (tree *RedBlackKVTree[K, V]) isNil(v V) bool {
if tree.nilChecker != nil {
return tree.nilChecker(value)
return tree.nilChecker(v)
}
return any(value) == nil
return empty.IsNil(v)
}
// SetComparator sets/changes the comparator for sorting.

View File

@ -29,9 +29,10 @@ func Test_KVAVLTree_TypedNil(t *testing.T) {
avlTree.Set(i, s)
}
}
t.Assert(avlTree.Size(), 10)
t.Assert(avlTree.Size(), 5)
avlTree2 := gtree.NewAVLKVTree[int, *Student](gutil.ComparatorTStr[int], true)
avlTree2.RegisterNilChecker(func(student *Student) bool {
avlTree2.SetNilChecker(func(student *Student) bool {
return student == nil
})
for i := 0; i < 10; i++ {
@ -62,9 +63,10 @@ func Test_KVBTree_TypedNil(t *testing.T) {
btree.Set(i, s)
}
}
t.Assert(btree.Size(), 10)
t.Assert(btree.Size(), 5)
btree2 := gtree.NewBKVTree[int, *Student](100, gutil.ComparatorTStr[int], true)
btree2.RegisterNilChecker(func(student *Student) bool {
btree2.SetNilChecker(func(student *Student) bool {
return student == nil
})
for i := 0; i < 10; i++ {
@ -95,10 +97,10 @@ func Test_KVRedBlackTree_TypedNil(t *testing.T) {
redBlackTree.Set(i, s)
}
}
t.Assert(redBlackTree.Size(), 10)
redBlackTree2 := gtree.NewRedBlackKVTree[int, *Student](gutil.ComparatorTStr[int], true)
t.Assert(redBlackTree.Size(), 5)
redBlackTree2.RegisterNilChecker(func(student *Student) bool {
redBlackTree2 := gtree.NewRedBlackKVTree[int, *Student](gutil.ComparatorTStr[int], true)
redBlackTree2.SetNilChecker(func(student *Student) bool {
return student == nil
})
for i := 0; i < 10; i++ {
@ -128,7 +130,8 @@ func Test_NewKVAVLTreeWithChecker_TypedNil(t *testing.T) {
avlTree.Set(i, s)
}
}
t.Assert(avlTree.Size(), 10)
t.Assert(avlTree.Size(), 5)
avlTree2 := gtree.NewAVLKVTreeWithChecker[int, *Student](gutil.ComparatorTStr[int], func(student *Student) bool {
return student == nil
}, true)
@ -160,7 +163,8 @@ func Test_NewKVBTreeWithChecker_TypedNil(t *testing.T) {
btree.Set(i, s)
}
}
t.Assert(btree.Size(), 10)
t.Assert(btree.Size(), 5)
btree2 := gtree.NewBKVTreeWithChecker[int, *Student](100, gutil.ComparatorTStr[int], func(student *Student) bool {
return student == nil
}, true)
@ -192,7 +196,8 @@ func Test_NewRedBlackKVTreeWithChecker_TypedNil(t *testing.T) {
redBlackTree.Set(i, s)
}
}
t.Assert(redBlackTree.Size(), 10)
t.Assert(redBlackTree.Size(), 5)
redBlackTree2 := gtree.NewRedBlackKVTreeWithChecker[int, *Student](gutil.ComparatorTStr[int], func(student *Student) bool {
return student == nil
}, true)

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 {
@ -944,6 +944,9 @@ func NewByGroup(group ...string) (db DB, err error) {
)
}
// linksChecker is the checker function for links map.
var linksChecker = func(v *sql.DB) bool { return v == nil }
// newDBByConfigNode creates and returns an ORM object with given configuration node and group name.
//
// Very Note:
@ -960,7 +963,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.NewKVMapWithChecker[ConfigNode, *sql.DB](linksChecker, true),
logger: glog.New(),
config: node,
localTypeMap: gmap.NewStrAnyMap(true),
@ -1127,7 +1130,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 +1162,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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

16
i18n/gi18n/testdata/issue-yaml/zh.yaml vendored Normal file
View File

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

View File

@ -727,6 +727,36 @@ func Test_Issue4093(t *testing.T) {
})
}
// https://github.com/gogf/gf/issues/4193
func Test_Issue4193(t *testing.T) {
type Req struct {
g.Meta `method:"post" mime:"multipart/form-data"`
File *ghttp.UploadFile `v:"required" type:"file"` // File is required
}
type Res struct{}
s := g.Server(guid.S())
s.BindMiddlewareDefault(ghttp.MiddlewareHandlerResponse)
s.BindHandler("/upload", func(ctx context.Context, req *Req) (res *Res, err error) {
return
})
s.SetDumpRouterMap(false)
s.SetAccessLogEnabled(false)
s.SetErrorLogEnabled(false)
s.Start()
defer s.Shutdown()
time.Sleep(100 * time.Millisecond)
gtest.C(t, func(t *gtest.T) {
client := g.Client()
client.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", s.GetListenedPort()))
content := client.PostContent(ctx, "/upload", g.Map{
"file": "",
})
t.Assert(content, `{"code":51,"message":"The File field is required","data":null}`)
})
}
// Issue4227Req
type Issue4227Req struct {
g.Meta `path:"/hello/:path_param" method:"post"`

View File

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

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 (
@ -58,6 +58,9 @@ var (
// Prefix array for trying searching in the local system.
localSystemTryFolders = []string{"", "config/", "manifest/config"}
// jsonMapChecker is the checker for JSON map.
jsonMapChecker = func(v *gjson.Json) bool { return v == nil }
)
// NewAdapterFile returns a new configuration management object.
@ -78,7 +81,7 @@ func NewAdapterFile(fileNameOrPath ...string) (*AdapterFile, error) {
config := &AdapterFile{
defaultFileNameOrPath: gtype.NewString(usedFileNameOrPath),
searchPaths: garray.NewStrArray(true),
jsonMap: gmap.NewStrAnyMap(true),
jsonMap: gmap.NewKVMapWithChecker[string, *gjson.Json](jsonMapChecker, true),
watchers: NewWatcherRegistry(),
}
// Customized dir path from env/cmd.
@ -257,7 +260,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 +329,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 +349,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

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

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

@ -18,7 +18,7 @@ package gconv
// TODO: change `paramKeyToAttrMap` to `ScanOption` to be more scalable; add `DeepCopy` option for `ScanOption`.
func Scan(srcValue any, dstPointer any, paramKeyToAttrMap ...map[string]string) (err error) {
option := ScanOption{
ContinueOnError: true,
ContinueOnError: false,
}
if len(paramKeyToAttrMap) > 0 {
option.ParamKeyToAttrMap = paramKeyToAttrMap[0]

View File

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

View File

@ -395,7 +395,11 @@ func (c *Converter) doConvertForDefault(in doConvertInput, option ConvertOption)
}
// custom converter.
dstReflectValue, ok, err := c.callCustomConverterWithRefer(fromReflectValue, referReflectValue)
var (
ok bool
dstReflectValue reflect.Value
)
dstReflectValue, ok, err = c.callCustomConverterWithRefer(fromReflectValue, referReflectValue)
if err != nil {
return nil, err
}
@ -415,7 +419,7 @@ func (c *Converter) doConvertForDefault(in doConvertInput, option ConvertOption)
switch referReflectValue.Kind() {
case reflect.Pointer:
// Type converting for custom type pointers.
// Eg:
// Example:
// type PayMode int
// type Req struct{
// Mode *PayMode

View File

@ -45,7 +45,11 @@ func (c *Converter) Float32(anyInput any) (float32, error) {
}
return 0, nil
case reflect.String:
f, err := strconv.ParseFloat(rv.String(), 32)
s := rv.String()
if s == "" {
return 0, nil
}
f, err := strconv.ParseFloat(s, 32)
if err != nil {
return 0, gerror.WrapCodef(
gcode.CodeInvalidParameter, err, "converting string to float32 failed for: %v", anyInput,
@ -68,6 +72,9 @@ func (c *Converter) Float32(anyInput any) (float32, error) {
if err != nil {
return 0, err
}
if s == "" {
return 0, nil
}
v, err := strconv.ParseFloat(s, 32)
if err != nil {
return 0, gerror.WrapCodef(
@ -112,7 +119,11 @@ func (c *Converter) Float64(anyInput any) (float64, error) {
}
return 0, nil
case reflect.String:
f, err := strconv.ParseFloat(rv.String(), 64)
s := rv.String()
if s == "" {
return 0, nil
}
f, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0, gerror.WrapCodef(
gcode.CodeInvalidParameter, err, "converting string to float64 failed for: %v", anyInput,
@ -135,6 +146,9 @@ func (c *Converter) Float64(anyInput any) (float64, error) {
if err != nil {
return 0, err
}
if s == "" {
return 0, nil
}
v, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0, gerror.WrapCodef(

View File

@ -88,6 +88,9 @@ func (c *Converter) doMapConvert(
value any, recursive RecursiveType, mustMapReturn bool, option MapOption,
) (map[string]any, error) {
if value == nil {
if mustMapReturn {
return map[string]any{}, nil
}
return nil, nil
}
// It redirects to its underlying value if it has implemented interface iVal.
@ -119,6 +122,10 @@ func (c *Converter) doMapConvert(
return nil, err
}
} else {
if len(r) == 0 && mustMapReturn {
return map[string]any{}, nil
}
// if r is not empty, which means it fails converting to map.
return nil, nil
}
case []byte:
@ -128,6 +135,10 @@ func (c *Converter) doMapConvert(
return nil, err
}
} else {
if len(r) == 0 && mustMapReturn {
return map[string]any{}, nil
}
// if r is not empty, which means it fails converting to map.
return nil, nil
}
case map[any]any:
@ -328,6 +339,7 @@ func (c *Converter) doMapConvert(
return m, nil
}
return nil, nil
default:
return nil, nil
}

View File

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

View File

@ -169,10 +169,11 @@ func (c *Converter) Struct(params, pointer any, option ...StructOption) (err err
return err
}
if paramsMap == nil {
// fails converting params to map, so it cannot be converted to struct pointer.
return gerror.NewCodef(
gcode.CodeInvalidParameter,
`convert params from "%#v" to "map[string]any" failed`,
params,
`convert params "%v" to "%s" failed`,
params, pointerReflectValue.Type(),
)
}
}
@ -521,8 +522,7 @@ func (c *Converter) bindVarToReflectValue(structFieldValue reflect.Value, value
case reflect.Struct:
// Recursively converting for struct attribute.
if err = c.Struct(value, structFieldValue, option); err != nil {
// Note there's reflect conversion mechanism here.
structFieldValue.Set(reflect.ValueOf(value).Convert(structFieldValue.Type()))
return err
}
// Note that the slice element might be type of struct,
@ -653,6 +653,8 @@ func (c *Converter) bindVarToReflectValue(structFieldValue reflect.Value, value
elem := item.Elem()
if err = c.bindVarToReflectValue(elem, value, option); err == nil {
structFieldValue.Set(elem.Addr())
} else {
return err
}
} else {
// Not empty pointer, it assigns values to it.