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>
This commit is contained in:
Lance Add
2026-01-22 19:04:52 +08:00
committed by GitHub
parent 110e3fbf16
commit b4053ed32e
11 changed files with 663 additions and 22 deletions

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

@ -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

@ -332,7 +332,7 @@ func (a *AdapterFile) getJson(fileNameOrPath ...string) (configJson *gjson.Json,
}
// 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 +346,11 @@ func (a *AdapterFile) GetWatcherNames() []string {
return a.watchers.GetNames()
}
// IsWatching checks and returns whether the specified `name` is watching.
func (a *AdapterFile) IsWatching(name string) bool {
return a.watchers.IsWatching(name)
}
// notifyWatchers notifies all watchers.
func (a *AdapterFile) notifyWatchers(ctx context.Context) {
a.watchers.Notify(ctx)

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

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

View File

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

View File

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