diff --git a/contrib/config/apollo/apollo.go b/contrib/config/apollo/apollo.go index 5ac306770a7..0b20df93764 100644 --- a/contrib/config/apollo/apollo.go +++ b/contrib/config/apollo/apollo.go @@ -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) diff --git a/contrib/config/consul/consul.go b/contrib/config/consul/consul.go index d3a7f71c8b9..1fe1a54d24c 100644 --- a/contrib/config/consul/consul.go +++ b/contrib/config/consul/consul.go @@ -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) diff --git a/contrib/config/kubecm/kubecm.go b/contrib/config/kubecm/kubecm.go index f7e1bb102be..86c7a3b4256 100644 --- a/contrib/config/kubecm/kubecm.go +++ b/contrib/config/kubecm/kubecm.go @@ -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) diff --git a/contrib/config/nacos/nacos.go b/contrib/config/nacos/nacos.go index 6d8082b0bd6..611297f2da7 100644 --- a/contrib/config/nacos/nacos.go +++ b/contrib/config/nacos/nacos.go @@ -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) diff --git a/contrib/config/polaris/polaris.go b/contrib/config/polaris/polaris.go index e46180ffe87..ade79f43d31 100644 --- a/contrib/config/polaris/polaris.go +++ b/contrib/config/polaris/polaris.go @@ -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) diff --git a/os/gcfg/gcfg_adaper.go b/os/gcfg/gcfg_adaper.go index df68e2013f2..667c0209adb 100644 --- a/os/gcfg/gcfg_adaper.go +++ b/os/gcfg/gcfg_adaper.go @@ -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 } diff --git a/os/gcfg/gcfg_adapter_content.go b/os/gcfg/gcfg_adapter_content.go index 217207fb9c5..e793c183965 100644 --- a/os/gcfg/gcfg_adapter_content.go +++ b/os/gcfg/gcfg_adapter_content.go @@ -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) diff --git a/os/gcfg/gcfg_adapter_file.go b/os/gcfg/gcfg_adapter_file.go index 459a4096759..b81cf6f6f96 100644 --- a/os/gcfg/gcfg_adapter_file.go +++ b/os/gcfg/gcfg_adapter_file.go @@ -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) diff --git a/os/gcfg/gcfg_loader.go b/os/gcfg/gcfg_loader.go new file mode 100644 index 00000000000..bf1b9130c7a --- /dev/null +++ b/os/gcfg/gcfg_loader.go @@ -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 +} diff --git a/os/gcfg/gcfg_watcher_registry.go b/os/gcfg/gcfg_watcher_registry.go index d5e6ee7323e..c9f091ffd85 100644 --- a/os/gcfg/gcfg_watcher_registry.go +++ b/os/gcfg/gcfg_watcher_registry.go @@ -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 }) } diff --git a/os/gcfg/gcfg_z_unit_loader_test.go b/os/gcfg/gcfg_z_unit_loader_test.go new file mode 100644 index 00000000000..eb3c51f9c46 --- /dev/null +++ b/os/gcfg/gcfg_z_unit_loader_test.go @@ -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") + }) +}