e5.2.1 Improved interfaces of functions using path and prefix.
This commit is contained in:
@ -99,7 +99,7 @@ func TestBuilder(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Verify paths were registered
|
// Verify paths were registered
|
||||||
paths := cfg.GetRegisteredPaths("")
|
paths := cfg.GetRegisteredPaths()
|
||||||
assert.True(t, paths["db.host"])
|
assert.True(t, paths["db.host"])
|
||||||
assert.True(t, paths["db.port"])
|
assert.True(t, paths["db.port"])
|
||||||
assert.True(t, paths["cache.ttl"])
|
assert.True(t, paths["cache.ttl"])
|
||||||
|
|||||||
16
config.go
16
config.go
@ -94,7 +94,7 @@ func (c *Config) SetLoadOptions(opts LoadOptions) error {
|
|||||||
|
|
||||||
// Recompute all current values based on new precedence
|
// Recompute all current values based on new precedence
|
||||||
for path, item := range c.items {
|
for path, item := range c.items {
|
||||||
item.currentValue = c.computeValue(path, item)
|
item.currentValue = c.computeValue(item)
|
||||||
c.items[path] = item
|
c.items[path] = item
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ func (c *Config) SetLoadOptions(opts LoadOptions) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// computeValue determines the current value based on precedence
|
// computeValue determines the current value based on precedence
|
||||||
func (c *Config) computeValue(path string, item configItem) any {
|
func (c *Config) computeValue(item configItem) any {
|
||||||
// Check sources in precedence order
|
// Check sources in precedence order
|
||||||
for _, source := range c.options.Sources {
|
for _, source := range c.options.Sources {
|
||||||
if val, exists := item.values[source]; exists && val != nil {
|
if val, exists := item.values[source]; exists && val != nil {
|
||||||
@ -146,11 +146,11 @@ func (c *Config) GetSource(path string, source Source) (any, bool) {
|
|||||||
// By default, this is SourceCLI. Returns an error if the path is not registered.
|
// By default, this is SourceCLI. Returns an error if the path is not registered.
|
||||||
// To set a value in a specific source, use SetSource instead.
|
// To set a value in a specific source, use SetSource instead.
|
||||||
func (c *Config) Set(path string, value any) error {
|
func (c *Config) Set(path string, value any) error {
|
||||||
return c.SetSource(path, c.options.Sources[0], value)
|
return c.SetSource(c.options.Sources[0], path, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetSource sets a value for a specific source
|
// SetSource sets a value for a specific source
|
||||||
func (c *Config) SetSource(path string, source Source, value any) error {
|
func (c *Config) SetSource(source Source, path string, value any) error {
|
||||||
c.mutex.Lock()
|
c.mutex.Lock()
|
||||||
defer c.mutex.Unlock()
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
@ -168,7 +168,7 @@ func (c *Config) SetSource(path string, source Source, value any) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
item.values[source] = value
|
item.values[source] = value
|
||||||
item.currentValue = c.computeValue(path, item)
|
item.currentValue = c.computeValue(item)
|
||||||
c.items[path] = item
|
c.items[path] = item
|
||||||
|
|
||||||
// Update source cache
|
// Update source cache
|
||||||
@ -240,7 +240,7 @@ func (c *Config) ResetSource(source Source) {
|
|||||||
// Remove source values from all items
|
// Remove source values from all items
|
||||||
for path, item := range c.items {
|
for path, item := range c.items {
|
||||||
delete(item.values, source)
|
delete(item.values, source)
|
||||||
item.currentValue = c.computeValue(path, item)
|
item.currentValue = c.computeValue(item)
|
||||||
c.items[path] = item
|
c.items[path] = item
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,7 +274,7 @@ func (c *Config) AsStruct() (any, error) {
|
|||||||
|
|
||||||
// Target populates the provided struct with current configuration
|
// Target populates the provided struct with current configuration
|
||||||
func (c *Config) Target(out any) error {
|
func (c *Config) Target(out any) error {
|
||||||
return c.Scan("", out)
|
return c.Scan(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
// populateStruct updates the cached struct representation using unified unmarshal
|
// populateStruct updates the cached struct representation using unified unmarshal
|
||||||
@ -287,7 +287,7 @@ func (c *Config) populateStruct() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.unmarshal("", "", c.structCache.target); err != nil {
|
if err := c.unmarshal("", c.structCache.target); err != nil {
|
||||||
return fmt.Errorf("failed to populate struct cache: %w", err)
|
return fmt.Errorf("failed to populate struct cache: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -158,9 +158,9 @@ func TestSourcePrecedence(t *testing.T) {
|
|||||||
cfg.Register("test.value", "default")
|
cfg.Register("test.value", "default")
|
||||||
|
|
||||||
// Set values in different sources
|
// Set values in different sources
|
||||||
cfg.SetSource("test.value", SourceFile, "from-file")
|
cfg.SetSource(SourceFile, "test.value", "from-file")
|
||||||
cfg.SetSource("test.value", SourceEnv, "from-env")
|
cfg.SetSource(SourceEnv, "test.value", "from-env")
|
||||||
cfg.SetSource("test.value", SourceCLI, "from-cli")
|
cfg.SetSource(SourceCLI, "test.value", "from-cli")
|
||||||
|
|
||||||
// Default precedence: CLI > Env > File > Default
|
// Default precedence: CLI > Env > File > Default
|
||||||
val, _ := cfg.Get("test.value")
|
val, _ := cfg.Get("test.value")
|
||||||
@ -216,20 +216,20 @@ func TestTypeConversion(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Test string conversions from environment
|
// Test string conversions from environment
|
||||||
cfg.SetSource("int", SourceEnv, "100")
|
cfg.SetSource(SourceEnv, "int", "100")
|
||||||
cfg.SetSource("float", SourceEnv, "2.718")
|
cfg.SetSource(SourceEnv, "float", "2.718")
|
||||||
cfg.SetSource("bool", SourceEnv, "false")
|
cfg.SetSource(SourceEnv, "bool", "false")
|
||||||
cfg.SetSource("duration", SourceEnv, "1m30s")
|
cfg.SetSource(SourceEnv, "duration", "1m30s")
|
||||||
cfg.SetSource("time", SourceEnv, "2024-12-25T10:00:00Z")
|
cfg.SetSource(SourceEnv, "time", "2024-12-25T10:00:00Z")
|
||||||
cfg.SetSource("ip", SourceEnv, "192.168.1.1")
|
cfg.SetSource(SourceEnv, "ip", "192.168.1.1")
|
||||||
cfg.SetSource("ipnet", SourceEnv, "10.0.0.0/8")
|
cfg.SetSource(SourceEnv, "ipnet", "10.0.0.0/8")
|
||||||
cfg.SetSource("url", SourceEnv, "https://example.com:8080/path")
|
cfg.SetSource(SourceEnv, "url", "https://example.com:8080/path")
|
||||||
cfg.SetSource("strings", SourceEnv, "x,y,z")
|
cfg.SetSource(SourceEnv, "strings", "x,y,z")
|
||||||
// cfg.SetSource("ints", SourceEnv, "7,8,9") // failure due to mapstructure limitation
|
// cfg.SetSource("ints", SourceEnv, "7,8,9") // failure due to mapstructure limitation
|
||||||
|
|
||||||
// Scan into struct
|
// Scan into struct
|
||||||
var result TestConfig
|
var result TestConfig
|
||||||
err = cfg.Scan("", &result)
|
err = cfg.Scan(&result)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, int64(100), result.IntValue)
|
assert.Equal(t, int64(100), result.IntValue)
|
||||||
@ -295,7 +295,7 @@ func TestConcurrentAccess(t *testing.T) {
|
|||||||
path := fmt.Sprintf("path%d", j)
|
path := fmt.Sprintf("path%d", j)
|
||||||
source := sources[j%len(sources)]
|
source := sources[j%len(sources)]
|
||||||
value := fmt.Sprintf("source%d-value%d", id, j)
|
value := fmt.Sprintf("source%d-value%d", id, j)
|
||||||
if err := cfg.SetSource(path, source, value); err != nil {
|
if err := cfg.SetSource(source, path, value); err != nil {
|
||||||
errors <- fmt.Errorf("source writer %d: %v", id, err)
|
errors <- fmt.Errorf("source writer %d: %v", id, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -364,9 +364,9 @@ func TestResetFunctionality(t *testing.T) {
|
|||||||
cfg.Register("test2", "default2")
|
cfg.Register("test2", "default2")
|
||||||
|
|
||||||
// Set values in different sources
|
// Set values in different sources
|
||||||
cfg.SetSource("test1", SourceFile, "file1")
|
cfg.SetSource(SourceFile, "test1", "file1")
|
||||||
cfg.SetSource("test1", SourceEnv, "env1")
|
cfg.SetSource(SourceEnv, "test1", "env1")
|
||||||
cfg.SetSource("test2", SourceCLI, "cli2")
|
cfg.SetSource(SourceCLI, "test2", "cli2")
|
||||||
|
|
||||||
t.Run("ResetSingleSource", func(t *testing.T) {
|
t.Run("ResetSingleSource", func(t *testing.T) {
|
||||||
cfg.ResetSource(SourceEnv)
|
cfg.ResetSource(SourceEnv)
|
||||||
|
|||||||
@ -93,7 +93,7 @@ func (c *Config) BindFlags(fs *flag.FlagSet) error {
|
|||||||
fs.Visit(func(f *flag.Flag) {
|
fs.Visit(func(f *flag.Flag) {
|
||||||
value := f.Value.String()
|
value := f.Value.String()
|
||||||
// Let mapstructure handle type conversion
|
// Let mapstructure handle type conversion
|
||||||
if err := c.SetSource(f.Name, SourceCLI, value); err != nil {
|
if err := c.SetSource(SourceCLI, f.Name, value); err != nil {
|
||||||
errors = append(errors, fmt.Errorf("flag %s: %w", f.Name, err))
|
errors = append(errors, fmt.Errorf("flag %s: %w", f.Name, err))
|
||||||
} else {
|
} else {
|
||||||
needsInvalidation = true
|
needsInvalidation = true
|
||||||
@ -276,9 +276,9 @@ func GetTyped[T any](c *Config, path string) (T, error) {
|
|||||||
|
|
||||||
// ScanTyped is a generic wrapper around Scan. It allocates a new instance of type T,
|
// ScanTyped is a generic wrapper around Scan. It allocates a new instance of type T,
|
||||||
// populates it with configuration data from the given base path, and returns a pointer to it.
|
// populates it with configuration data from the given base path, and returns a pointer to it.
|
||||||
func ScanTyped[T any](c *Config, basePath string) (*T, error) {
|
func ScanTyped[T any](c *Config, basePath ...string) (*T, error) {
|
||||||
var target T
|
var target T
|
||||||
if err := c.Scan(basePath, &target); err != nil {
|
if err := c.Scan(&target, basePath...); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &target, nil
|
return &target, nil
|
||||||
|
|||||||
@ -200,7 +200,7 @@ func TestValidation(t *testing.T) {
|
|||||||
cfg2.Register("test", "default")
|
cfg2.Register("test", "default")
|
||||||
|
|
||||||
// Value equals default but from different source
|
// Value equals default but from different source
|
||||||
cfg2.SetSource("test", SourceEnv, "default")
|
cfg2.SetSource(SourceEnv, "test", "default")
|
||||||
|
|
||||||
err := cfg2.Validate("test")
|
err := cfg2.Validate("test")
|
||||||
assert.NoError(t, err) // Should pass because env provided value
|
assert.NoError(t, err) // Should pass because env provided value
|
||||||
@ -213,9 +213,9 @@ func TestDebugAndDump(t *testing.T) {
|
|||||||
cfg.Register("server.host", "localhost")
|
cfg.Register("server.host", "localhost")
|
||||||
cfg.Register("server.port", 8080)
|
cfg.Register("server.port", 8080)
|
||||||
|
|
||||||
cfg.SetSource("server.host", SourceFile, "filehost")
|
cfg.SetSource(SourceFile, "server.host", "filehost")
|
||||||
cfg.SetSource("server.host", SourceEnv, "envhost")
|
cfg.SetSource(SourceEnv, "server.host", "envhost")
|
||||||
cfg.SetSource("server.port", SourceCLI, "9999")
|
cfg.SetSource(SourceCLI, "server.port", "9999")
|
||||||
|
|
||||||
t.Run("Debug", func(t *testing.T) {
|
t.Run("Debug", func(t *testing.T) {
|
||||||
debug := cfg.Debug()
|
debug := cfg.Debug()
|
||||||
@ -258,8 +258,8 @@ func TestClone(t *testing.T) {
|
|||||||
cfg.Register("original.value", "default")
|
cfg.Register("original.value", "default")
|
||||||
cfg.Register("shared.value", "shared")
|
cfg.Register("shared.value", "shared")
|
||||||
|
|
||||||
cfg.SetSource("original.value", SourceFile, "filevalue")
|
cfg.SetSource(SourceFile, "original.value", "filevalue")
|
||||||
cfg.SetSource("shared.value", SourceEnv, "envvalue")
|
cfg.SetSource(SourceEnv, "shared.value", "envvalue")
|
||||||
|
|
||||||
clone := cfg.Clone()
|
clone := cfg.Clone()
|
||||||
require.NotNil(t, clone)
|
require.NotNil(t, clone)
|
||||||
|
|||||||
49
decode.go
49
decode.go
@ -14,7 +14,18 @@ import (
|
|||||||
|
|
||||||
// unmarshal is the single authoritative function for decoding configuration
|
// unmarshal is the single authoritative function for decoding configuration
|
||||||
// into target structures. All public decoding methods delegate to this.
|
// into target structures. All public decoding methods delegate to this.
|
||||||
func (c *Config) unmarshal(basePath string, source Source, target any) error {
|
func (c *Config) unmarshal(source Source, target any, basePath ...string) error {
|
||||||
|
// Parse variadic basePath
|
||||||
|
path := ""
|
||||||
|
switch len(basePath) {
|
||||||
|
case 0:
|
||||||
|
// Use default empty path
|
||||||
|
case 1:
|
||||||
|
path = basePath[0]
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("too many basePath arguments: expected 0 or 1, got %d", len(basePath))
|
||||||
|
}
|
||||||
|
|
||||||
// Validate target
|
// Validate target
|
||||||
rv := reflect.ValueOf(target)
|
rv := reflect.ValueOf(target)
|
||||||
if rv.Kind() != reflect.Ptr || rv.IsNil() {
|
if rv.Kind() != reflect.Ptr || rv.IsNil() {
|
||||||
@ -42,7 +53,7 @@ func (c *Config) unmarshal(basePath string, source Source, target any) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Navigate to basePath section
|
// Navigate to basePath section
|
||||||
sectionData := navigateToPath(nestedMap, basePath)
|
sectionData := navigateToPath(nestedMap, path)
|
||||||
|
|
||||||
// Ensure we have a map to decode, normalizing if necessary.
|
// Ensure we have a map to decode, normalizing if necessary.
|
||||||
sectionMap, err := normalizeMap(sectionData)
|
sectionMap, err := normalizeMap(sectionData)
|
||||||
@ -51,7 +62,7 @@ func (c *Config) unmarshal(basePath string, source Source, target any) error {
|
|||||||
sectionMap = make(map[string]any) // Empty section is valid.
|
sectionMap = make(map[string]any) // Empty section is valid.
|
||||||
} else {
|
} else {
|
||||||
// Path points to a non-map value, which is an error for Scan.
|
// Path points to a non-map value, which is an error for Scan.
|
||||||
return fmt.Errorf("path %q refers to non-map value (type %T)", basePath, sectionData)
|
return fmt.Errorf("path %q refers to non-map value (type %T)", path, sectionData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,42 +80,12 @@ func (c *Config) unmarshal(basePath string, source Source, target any) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := decoder.Decode(sectionMap); err != nil {
|
if err := decoder.Decode(sectionMap); err != nil {
|
||||||
return fmt.Errorf("decode failed for path %q: %w", basePath, err)
|
return fmt.Errorf("decode failed for path %q: %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// // Ensure we have a map to decode
|
|
||||||
// sectionMap, ok := sectionData.(map[string]any)
|
|
||||||
// if !ok {
|
|
||||||
// if sectionData == nil {
|
|
||||||
// sectionMap = make(map[string]any) // Empty section
|
|
||||||
// } else {
|
|
||||||
// return fmt.Errorf("path %q refers to non-map value (type %T)", basePath, sectionData)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Create decoder with comprehensive hooks
|
|
||||||
// decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
|
||||||
// Result: target,
|
|
||||||
// TagName: c.tagName,
|
|
||||||
// WeaklyTypedInput: true,
|
|
||||||
// DecodeHook: c.getDecodeHook(),
|
|
||||||
// ZeroFields: true,
|
|
||||||
// Metadata: nil,
|
|
||||||
// })
|
|
||||||
// if err != nil {
|
|
||||||
// return fmt.Errorf("decoder creation failed: %w", err)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if err := decoder.Decode(sectionMap); err != nil {
|
|
||||||
// return fmt.Errorf("decode failed for path %q: %w", basePath, err)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
// normalizeMap ensures that the input data is a map[string]any for the decoder.
|
// normalizeMap ensures that the input data is a map[string]any for the decoder.
|
||||||
func normalizeMap(data any) (map[string]any, error) {
|
func normalizeMap(data any) (map[string]any, error) {
|
||||||
if data == nil {
|
if data == nil {
|
||||||
|
|||||||
@ -50,22 +50,22 @@ func TestScanWithComplexTypes(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Set values from different sources
|
// Set values from different sources
|
||||||
cfg.SetSource("network.ip", SourceEnv, "192.168.1.100")
|
cfg.SetSource(SourceEnv, "network.ip", "192.168.1.100")
|
||||||
cfg.SetSource("network.subnet", SourceEnv, "192.168.1.0/24")
|
cfg.SetSource(SourceEnv, "network.subnet", "192.168.1.0/24")
|
||||||
cfg.SetSource("network.endpoint", SourceEnv, "https://api.example.com:8443/v1")
|
cfg.SetSource(SourceEnv, "network.endpoint", "https://api.example.com:8443/v1")
|
||||||
cfg.SetSource("network.timeout", SourceFile, "2m30s")
|
cfg.SetSource(SourceFile, "network.timeout", "2m30s")
|
||||||
cfg.SetSource("network.retry.count", SourceFile, int64(5))
|
cfg.SetSource(SourceFile, "network.retry.count", int64(5))
|
||||||
cfg.SetSource("network.retry.interval", SourceFile, "10s")
|
cfg.SetSource(SourceFile, "network.retry.interval", "10s")
|
||||||
cfg.SetSource("tags", SourceCLI, "prod,staging,test")
|
cfg.SetSource(SourceCLI, "tags", "prod,staging,test")
|
||||||
cfg.SetSource("ports", SourceFile, []any{int64(80), int64(443), int64(8080)})
|
cfg.SetSource(SourceFile, "ports", []any{int64(80), int64(443), int64(8080)})
|
||||||
cfg.SetSource("labels", SourceFile, map[string]any{
|
cfg.SetSource(SourceFile, "labels", map[string]any{
|
||||||
"env": "production",
|
"env": "production",
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
})
|
})
|
||||||
|
|
||||||
// Scan into struct
|
// Scan into struct
|
||||||
var result AppConfig
|
var result AppConfig
|
||||||
err = cfg.Scan("", &result)
|
err = cfg.Scan(&result)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Verify conversions
|
// Verify conversions
|
||||||
@ -100,7 +100,7 @@ func TestScanWithBasePath(t *testing.T) {
|
|||||||
|
|
||||||
// Scan only the server section
|
// Scan only the server section
|
||||||
var server ServerConfig
|
var server ServerConfig
|
||||||
err := cfg.Scan("app.server", &server)
|
err := cfg.Scan(&server, "app.server")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "appserver", server.Host)
|
assert.Equal(t, "appserver", server.Host)
|
||||||
@ -109,7 +109,7 @@ func TestScanWithBasePath(t *testing.T) {
|
|||||||
|
|
||||||
// Test non-existent base path
|
// Test non-existent base path
|
||||||
var empty ServerConfig
|
var empty ServerConfig
|
||||||
err = cfg.Scan("app.nonexistent", &empty)
|
err = cfg.Scan(&empty, "app.nonexistent")
|
||||||
assert.NoError(t, err) // Should not error, just empty
|
assert.NoError(t, err) // Should not error, just empty
|
||||||
assert.Equal(t, "", empty.Host)
|
assert.Equal(t, "", empty.Host)
|
||||||
assert.Equal(t, 0, empty.Port)
|
assert.Equal(t, 0, empty.Port)
|
||||||
@ -124,9 +124,9 @@ func TestScanFromSource(t *testing.T) {
|
|||||||
cfg := New()
|
cfg := New()
|
||||||
cfg.Register("value", "default")
|
cfg.Register("value", "default")
|
||||||
|
|
||||||
cfg.SetSource("value", SourceFile, "fromfile")
|
cfg.SetSource(SourceFile, "value", "fromfile")
|
||||||
cfg.SetSource("value", SourceEnv, "fromenv")
|
cfg.SetSource(SourceEnv, "value", "fromenv")
|
||||||
cfg.SetSource("value", SourceCLI, "fromcli")
|
cfg.SetSource(SourceCLI, "value", "fromcli")
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
source Source
|
source Source
|
||||||
@ -141,7 +141,7 @@ func TestScanFromSource(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(string(tt.source), func(t *testing.T) {
|
t.Run(string(tt.source), func(t *testing.T) {
|
||||||
var result Config
|
var result Config
|
||||||
err := cfg.ScanSource("", tt.source, &result)
|
err := cfg.ScanSource(tt.source, &result)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, tt.expected, result.Value)
|
assert.Equal(t, tt.expected, result.Value)
|
||||||
})
|
})
|
||||||
@ -165,7 +165,7 @@ func TestInvalidScanTargets(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
err := cfg.Scan("", tt.target)
|
err := cfg.Scan(tt.target)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), tt.expectErr)
|
assert.Contains(t, err.Error(), tt.expectErr)
|
||||||
})
|
})
|
||||||
@ -185,7 +185,7 @@ func TestCustomTypeConversion(t *testing.T) {
|
|||||||
cfg.Set("ip", "not-an-ip")
|
cfg.Set("ip", "not-an-ip")
|
||||||
|
|
||||||
var result Config
|
var result Config
|
||||||
err := cfg.Scan("", &result)
|
err := cfg.Scan(&result)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "invalid IP address")
|
assert.Contains(t, err.Error(), "invalid IP address")
|
||||||
})
|
})
|
||||||
@ -199,7 +199,7 @@ func TestCustomTypeConversion(t *testing.T) {
|
|||||||
cfg.Set("network", "invalid-cidr")
|
cfg.Set("network", "invalid-cidr")
|
||||||
|
|
||||||
var result Config
|
var result Config
|
||||||
err := cfg.Scan("", &result)
|
err := cfg.Scan(&result)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "invalid CIDR")
|
assert.Contains(t, err.Error(), "invalid CIDR")
|
||||||
})
|
})
|
||||||
@ -213,7 +213,7 @@ func TestCustomTypeConversion(t *testing.T) {
|
|||||||
cfg.Set("endpoint", "://invalid-url")
|
cfg.Set("endpoint", "://invalid-url")
|
||||||
|
|
||||||
var result Config
|
var result Config
|
||||||
err := cfg.Scan("", &result)
|
err := cfg.Scan(&result)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "invalid URL")
|
assert.Contains(t, err.Error(), "invalid URL")
|
||||||
})
|
})
|
||||||
@ -232,7 +232,7 @@ func TestCustomTypeConversion(t *testing.T) {
|
|||||||
cfg.Set("ip", string(longIP))
|
cfg.Set("ip", string(longIP))
|
||||||
|
|
||||||
var result Config
|
var result Config
|
||||||
err := cfg.Scan("", &result)
|
err := cfg.Scan(&result)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "invalid IP length")
|
assert.Contains(t, err.Error(), "invalid IP length")
|
||||||
})
|
})
|
||||||
@ -251,7 +251,7 @@ func TestCustomTypeConversion(t *testing.T) {
|
|||||||
cfg.Set("url", longURL)
|
cfg.Set("url", longURL)
|
||||||
|
|
||||||
var result Config
|
var result Config
|
||||||
err := cfg.Scan("", &result)
|
err := cfg.Scan(&result)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "URL too long")
|
assert.Contains(t, err.Error(), "URL too long")
|
||||||
})
|
})
|
||||||
@ -286,7 +286,7 @@ func TestZeroFields(t *testing.T) {
|
|||||||
}{Field: "initial"},
|
}{Field: "initial"},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := cfg.Scan("", &result)
|
err := cfg.Scan(&result)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// ZeroFields should reset all fields before decoding
|
// ZeroFields should reset all fields before decoding
|
||||||
@ -317,7 +317,7 @@ func TestWeaklyTypedInput(t *testing.T) {
|
|||||||
cfg.Set("string_from_bool", true)
|
cfg.Set("string_from_bool", true)
|
||||||
|
|
||||||
var result Config
|
var result Config
|
||||||
err := cfg.Scan("", &result)
|
err := cfg.Scan(&result)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, 42, result.IntFromString)
|
assert.Equal(t, 42, result.IntFromString)
|
||||||
|
|||||||
@ -43,7 +43,7 @@ portNum := port.(int64) // Won't panic - type is enforced
|
|||||||
|
|
||||||
```go
|
```go
|
||||||
// Get value from specific source
|
// Get value from specific source
|
||||||
envPort, exists := cfg.GetSource("server.port", config.SourceEnv)
|
envPort, exists := cfg.GetSource(config.SourceEnv, "server.port")
|
||||||
if exists {
|
if exists {
|
||||||
log.Printf("Port from environment: %v", envPort)
|
log.Printf("Port from environment: %v", envPort)
|
||||||
}
|
}
|
||||||
@ -68,7 +68,7 @@ var serverConfig struct {
|
|||||||
} `toml:"tls"`
|
} `toml:"tls"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cfg.Scan("server", &serverConfig); err != nil {
|
if err := cfg.Scan(&serverConfig, "server"); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,11 +158,11 @@ if err := cfg.Set("server.port", int64(9090)); err != nil {
|
|||||||
|
|
||||||
```go
|
```go
|
||||||
// Set value in specific source
|
// Set value in specific source
|
||||||
cfg.SetSource("server.port", config.SourceEnv, "8080")
|
cfg.SetSource(config.SourceEnv, "server.port", "8080")
|
||||||
cfg.SetSource("debug", config.SourceCLI, true)
|
cfg.SetSource(config.SourceCLI, "debug", true)
|
||||||
|
|
||||||
// File source typically set via LoadFile, but can be manual
|
// File source typically set via LoadFile, but can be manual
|
||||||
cfg.SetSource("feature.enabled", config.SourceFile, true)
|
cfg.SetSource(config.SourceFile, "feature.enabled", true)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Batch Updates
|
### Batch Updates
|
||||||
|
|||||||
@ -204,7 +204,7 @@ func (c *Config) loadFile(path string) error {
|
|||||||
delete(item.values, SourceFile)
|
delete(item.values, SourceFile)
|
||||||
}
|
}
|
||||||
// Recompute the current value based on new source precedence.
|
// Recompute the current value based on new source precedence.
|
||||||
item.currentValue = c.computeValue(path, item)
|
item.currentValue = c.computeValue(item)
|
||||||
c.items[path] = item
|
c.items[path] = item
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,7 +261,7 @@ func (c *Config) loadEnv(opts LoadOptions) error {
|
|||||||
item.values = make(map[Source]any)
|
item.values = make(map[Source]any)
|
||||||
}
|
}
|
||||||
item.values[SourceEnv] = value // Store as string
|
item.values[SourceEnv] = value // Store as string
|
||||||
item.currentValue = c.computeValue(path, item)
|
item.currentValue = c.computeValue(item)
|
||||||
c.items[path] = item
|
c.items[path] = item
|
||||||
c.envData[path] = value
|
c.envData[path] = value
|
||||||
}
|
}
|
||||||
@ -296,7 +296,7 @@ func (c *Config) loadCLI(args []string) error {
|
|||||||
item.values = make(map[Source]any)
|
item.values = make(map[Source]any)
|
||||||
}
|
}
|
||||||
item.values[SourceCLI] = value
|
item.values[SourceCLI] = value
|
||||||
item.currentValue = c.computeValue(path, item)
|
item.currentValue = c.computeValue(item)
|
||||||
c.items[path] = item
|
c.items[path] = item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -375,9 +375,9 @@ func TestAtomicSave(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("SaveSpecificSource", func(t *testing.T) {
|
t.Run("SaveSpecificSource", func(t *testing.T) {
|
||||||
cfg.SetSource("server.host", SourceEnv, "envhost")
|
cfg.SetSource(SourceEnv, "server.host", "envhost")
|
||||||
cfg.SetSource("server.port", SourceEnv, "7777")
|
cfg.SetSource(SourceEnv, "server.port", "7777")
|
||||||
cfg.SetSource("server.port", SourceFile, "6666")
|
cfg.SetSource(SourceFile, "server.port", "6666")
|
||||||
|
|
||||||
savePath := filepath.Join(tmpDir, "env-only.toml")
|
savePath := filepath.Join(tmpDir, "env-only.toml")
|
||||||
err := cfg.SaveSource(savePath, SourceEnv)
|
err := cfg.SaveSource(savePath, SourceEnv)
|
||||||
|
|||||||
30
register.go
30
register.go
@ -46,7 +46,7 @@ func (c *Config) RegisterWithEnv(path string, defaultValue any, envVar string) e
|
|||||||
// Check if the environment variable exists and load it
|
// Check if the environment variable exists and load it
|
||||||
if value, exists := os.LookupEnv(envVar); exists {
|
if value, exists := os.LookupEnv(envVar); exists {
|
||||||
parsed := parseValue(value)
|
parsed := parseValue(value)
|
||||||
return c.SetSource(path, SourceEnv, parsed)
|
return c.SetSource(SourceEnv, path, parsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -230,7 +230,7 @@ func (c *Config) registerFields(v reflect.Value, pathPrefix, fieldPath string, e
|
|||||||
if envTag != "" && err == nil {
|
if envTag != "" && err == nil {
|
||||||
if value, exists := os.LookupEnv(envTag); exists {
|
if value, exists := os.LookupEnv(envTag); exists {
|
||||||
parsed := parseValue(value)
|
parsed := parseValue(value)
|
||||||
if setErr := c.SetSource(currentPath, SourceEnv, parsed); setErr != nil {
|
if setErr := c.SetSource(SourceEnv, currentPath, parsed); setErr != nil {
|
||||||
*errors = append(*errors, fmt.Sprintf("field %s%s env %s: %v", fieldPath, field.Name, envTag, setErr))
|
*errors = append(*errors, fmt.Sprintf("field %s%s env %s: %v", fieldPath, field.Name, envTag, setErr))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -239,13 +239,18 @@ func (c *Config) registerFields(v reflect.Value, pathPrefix, fieldPath string, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetRegisteredPaths returns all registered configuration paths with the specified prefix.
|
// GetRegisteredPaths returns all registered configuration paths with the specified prefix.
|
||||||
func (c *Config) GetRegisteredPaths(prefix string) map[string]bool {
|
func (c *Config) GetRegisteredPaths(prefix ...string) map[string]bool {
|
||||||
|
p := ""
|
||||||
|
if len(prefix) > 0 {
|
||||||
|
p = prefix[0]
|
||||||
|
}
|
||||||
|
|
||||||
c.mutex.RLock()
|
c.mutex.RLock()
|
||||||
defer c.mutex.RUnlock()
|
defer c.mutex.RUnlock()
|
||||||
|
|
||||||
result := make(map[string]bool)
|
result := make(map[string]bool)
|
||||||
for path := range c.items {
|
for path := range c.items {
|
||||||
if strings.HasPrefix(path, prefix) {
|
if strings.HasPrefix(path, p) {
|
||||||
result[path] = true
|
result[path] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -254,13 +259,18 @@ func (c *Config) GetRegisteredPaths(prefix string) map[string]bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetRegisteredPathsWithDefaults returns paths with their default values
|
// GetRegisteredPathsWithDefaults returns paths with their default values
|
||||||
func (c *Config) GetRegisteredPathsWithDefaults(prefix string) map[string]any {
|
func (c *Config) GetRegisteredPathsWithDefaults(prefix ...string) map[string]any {
|
||||||
|
p := ""
|
||||||
|
if len(prefix) > 0 {
|
||||||
|
p = prefix[0]
|
||||||
|
}
|
||||||
|
|
||||||
c.mutex.RLock()
|
c.mutex.RLock()
|
||||||
defer c.mutex.RUnlock()
|
defer c.mutex.RUnlock()
|
||||||
|
|
||||||
result := make(map[string]any)
|
result := make(map[string]any)
|
||||||
for path, item := range c.items {
|
for path, item := range c.items {
|
||||||
if strings.HasPrefix(path, prefix) {
|
if strings.HasPrefix(path, p) {
|
||||||
result[path] = item.defaultValue
|
result[path] = item.defaultValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -269,11 +279,11 @@ func (c *Config) GetRegisteredPathsWithDefaults(prefix string) map[string]any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan decodes configuration into target using the unified unmarshal function
|
// Scan decodes configuration into target using the unified unmarshal function
|
||||||
func (c *Config) Scan(basePath string, target any) error {
|
func (c *Config) Scan(target any, basePath ...string) error {
|
||||||
return c.unmarshal(basePath, "", target) // Empty source means use merged state
|
return c.unmarshal("", target, basePath...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ScanSource decodes configuration from specific source using unified unmarshal
|
// ScanSource decodes configuration from specific source using unified unmarshal
|
||||||
func (c *Config) ScanSource(basePath string, source Source, target any) error {
|
func (c *Config) ScanSource(source Source, target any, basePath ...string) error {
|
||||||
return c.unmarshal(basePath, source, target)
|
return c.unmarshal(source, target, basePath...)
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user