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)
|
||||
|
||||
// Verify paths were registered
|
||||
paths := cfg.GetRegisteredPaths("")
|
||||
paths := cfg.GetRegisteredPaths()
|
||||
assert.True(t, paths["db.host"])
|
||||
assert.True(t, paths["db.port"])
|
||||
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
|
||||
for path, item := range c.items {
|
||||
item.currentValue = c.computeValue(path, item)
|
||||
item.currentValue = c.computeValue(item)
|
||||
c.items[path] = item
|
||||
}
|
||||
|
||||
@ -102,7 +102,7 @@ func (c *Config) SetLoadOptions(opts LoadOptions) error {
|
||||
}
|
||||
|
||||
// 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
|
||||
for _, source := range c.options.Sources {
|
||||
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.
|
||||
// To set a value in a specific source, use SetSource instead.
|
||||
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
|
||||
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()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
@ -168,7 +168,7 @@ func (c *Config) SetSource(path string, source Source, value any) error {
|
||||
}
|
||||
|
||||
item.values[source] = value
|
||||
item.currentValue = c.computeValue(path, item)
|
||||
item.currentValue = c.computeValue(item)
|
||||
c.items[path] = item
|
||||
|
||||
// Update source cache
|
||||
@ -240,7 +240,7 @@ func (c *Config) ResetSource(source Source) {
|
||||
// Remove source values from all items
|
||||
for path, item := range c.items {
|
||||
delete(item.values, source)
|
||||
item.currentValue = c.computeValue(path, item)
|
||||
item.currentValue = c.computeValue(item)
|
||||
c.items[path] = item
|
||||
}
|
||||
|
||||
@ -274,7 +274,7 @@ func (c *Config) AsStruct() (any, error) {
|
||||
|
||||
// Target populates the provided struct with current configuration
|
||||
func (c *Config) Target(out any) error {
|
||||
return c.Scan("", out)
|
||||
return c.Scan(out)
|
||||
}
|
||||
|
||||
// populateStruct updates the cached struct representation using unified unmarshal
|
||||
@ -287,7 +287,7 @@ func (c *Config) populateStruct() error {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@ -158,9 +158,9 @@ func TestSourcePrecedence(t *testing.T) {
|
||||
cfg.Register("test.value", "default")
|
||||
|
||||
// Set values in different sources
|
||||
cfg.SetSource("test.value", SourceFile, "from-file")
|
||||
cfg.SetSource("test.value", SourceEnv, "from-env")
|
||||
cfg.SetSource("test.value", SourceCLI, "from-cli")
|
||||
cfg.SetSource(SourceFile, "test.value", "from-file")
|
||||
cfg.SetSource(SourceEnv, "test.value", "from-env")
|
||||
cfg.SetSource(SourceCLI, "test.value", "from-cli")
|
||||
|
||||
// Default precedence: CLI > Env > File > Default
|
||||
val, _ := cfg.Get("test.value")
|
||||
@ -216,20 +216,20 @@ func TestTypeConversion(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test string conversions from environment
|
||||
cfg.SetSource("int", SourceEnv, "100")
|
||||
cfg.SetSource("float", SourceEnv, "2.718")
|
||||
cfg.SetSource("bool", SourceEnv, "false")
|
||||
cfg.SetSource("duration", SourceEnv, "1m30s")
|
||||
cfg.SetSource("time", SourceEnv, "2024-12-25T10:00:00Z")
|
||||
cfg.SetSource("ip", SourceEnv, "192.168.1.1")
|
||||
cfg.SetSource("ipnet", SourceEnv, "10.0.0.0/8")
|
||||
cfg.SetSource("url", SourceEnv, "https://example.com:8080/path")
|
||||
cfg.SetSource("strings", SourceEnv, "x,y,z")
|
||||
cfg.SetSource(SourceEnv, "int", "100")
|
||||
cfg.SetSource(SourceEnv, "float", "2.718")
|
||||
cfg.SetSource(SourceEnv, "bool", "false")
|
||||
cfg.SetSource(SourceEnv, "duration", "1m30s")
|
||||
cfg.SetSource(SourceEnv, "time", "2024-12-25T10:00:00Z")
|
||||
cfg.SetSource(SourceEnv, "ip", "192.168.1.1")
|
||||
cfg.SetSource(SourceEnv, "ipnet", "10.0.0.0/8")
|
||||
cfg.SetSource(SourceEnv, "url", "https://example.com:8080/path")
|
||||
cfg.SetSource(SourceEnv, "strings", "x,y,z")
|
||||
// cfg.SetSource("ints", SourceEnv, "7,8,9") // failure due to mapstructure limitation
|
||||
|
||||
// Scan into struct
|
||||
var result TestConfig
|
||||
err = cfg.Scan("", &result)
|
||||
err = cfg.Scan(&result)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, int64(100), result.IntValue)
|
||||
@ -295,7 +295,7 @@ func TestConcurrentAccess(t *testing.T) {
|
||||
path := fmt.Sprintf("path%d", j)
|
||||
source := sources[j%len(sources)]
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -364,9 +364,9 @@ func TestResetFunctionality(t *testing.T) {
|
||||
cfg.Register("test2", "default2")
|
||||
|
||||
// Set values in different sources
|
||||
cfg.SetSource("test1", SourceFile, "file1")
|
||||
cfg.SetSource("test1", SourceEnv, "env1")
|
||||
cfg.SetSource("test2", SourceCLI, "cli2")
|
||||
cfg.SetSource(SourceFile, "test1", "file1")
|
||||
cfg.SetSource(SourceEnv, "test1", "env1")
|
||||
cfg.SetSource(SourceCLI, "test2", "cli2")
|
||||
|
||||
t.Run("ResetSingleSource", func(t *testing.T) {
|
||||
cfg.ResetSource(SourceEnv)
|
||||
|
||||
@ -93,7 +93,7 @@ func (c *Config) BindFlags(fs *flag.FlagSet) error {
|
||||
fs.Visit(func(f *flag.Flag) {
|
||||
value := f.Value.String()
|
||||
// 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))
|
||||
} else {
|
||||
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,
|
||||
// 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
|
||||
if err := c.Scan(basePath, &target); err != nil {
|
||||
if err := c.Scan(&target, basePath...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &target, nil
|
||||
|
||||
@ -200,7 +200,7 @@ func TestValidation(t *testing.T) {
|
||||
cfg2.Register("test", "default")
|
||||
|
||||
// Value equals default but from different source
|
||||
cfg2.SetSource("test", SourceEnv, "default")
|
||||
cfg2.SetSource(SourceEnv, "test", "default")
|
||||
|
||||
err := cfg2.Validate("test")
|
||||
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.port", 8080)
|
||||
|
||||
cfg.SetSource("server.host", SourceFile, "filehost")
|
||||
cfg.SetSource("server.host", SourceEnv, "envhost")
|
||||
cfg.SetSource("server.port", SourceCLI, "9999")
|
||||
cfg.SetSource(SourceFile, "server.host", "filehost")
|
||||
cfg.SetSource(SourceEnv, "server.host", "envhost")
|
||||
cfg.SetSource(SourceCLI, "server.port", "9999")
|
||||
|
||||
t.Run("Debug", func(t *testing.T) {
|
||||
debug := cfg.Debug()
|
||||
@ -258,8 +258,8 @@ func TestClone(t *testing.T) {
|
||||
cfg.Register("original.value", "default")
|
||||
cfg.Register("shared.value", "shared")
|
||||
|
||||
cfg.SetSource("original.value", SourceFile, "filevalue")
|
||||
cfg.SetSource("shared.value", SourceEnv, "envvalue")
|
||||
cfg.SetSource(SourceFile, "original.value", "filevalue")
|
||||
cfg.SetSource(SourceEnv, "shared.value", "envvalue")
|
||||
|
||||
clone := cfg.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
|
||||
// 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
|
||||
rv := reflect.ValueOf(target)
|
||||
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
|
||||
sectionData := navigateToPath(nestedMap, basePath)
|
||||
sectionData := navigateToPath(nestedMap, path)
|
||||
|
||||
// Ensure we have a map to decode, normalizing if necessary.
|
||||
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.
|
||||
} else {
|
||||
// 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 {
|
||||
return fmt.Errorf("decode failed for path %q: %w", basePath, err)
|
||||
return fmt.Errorf("decode failed for path %q: %w", path, err)
|
||||
}
|
||||
|
||||
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.
|
||||
func normalizeMap(data any) (map[string]any, error) {
|
||||
if data == nil {
|
||||
|
||||
@ -50,22 +50,22 @@ func TestScanWithComplexTypes(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Set values from different sources
|
||||
cfg.SetSource("network.ip", SourceEnv, "192.168.1.100")
|
||||
cfg.SetSource("network.subnet", SourceEnv, "192.168.1.0/24")
|
||||
cfg.SetSource("network.endpoint", SourceEnv, "https://api.example.com:8443/v1")
|
||||
cfg.SetSource("network.timeout", SourceFile, "2m30s")
|
||||
cfg.SetSource("network.retry.count", SourceFile, int64(5))
|
||||
cfg.SetSource("network.retry.interval", SourceFile, "10s")
|
||||
cfg.SetSource("tags", SourceCLI, "prod,staging,test")
|
||||
cfg.SetSource("ports", SourceFile, []any{int64(80), int64(443), int64(8080)})
|
||||
cfg.SetSource("labels", SourceFile, map[string]any{
|
||||
cfg.SetSource(SourceEnv, "network.ip", "192.168.1.100")
|
||||
cfg.SetSource(SourceEnv, "network.subnet", "192.168.1.0/24")
|
||||
cfg.SetSource(SourceEnv, "network.endpoint", "https://api.example.com:8443/v1")
|
||||
cfg.SetSource(SourceFile, "network.timeout", "2m30s")
|
||||
cfg.SetSource(SourceFile, "network.retry.count", int64(5))
|
||||
cfg.SetSource(SourceFile, "network.retry.interval", "10s")
|
||||
cfg.SetSource(SourceCLI, "tags", "prod,staging,test")
|
||||
cfg.SetSource(SourceFile, "ports", []any{int64(80), int64(443), int64(8080)})
|
||||
cfg.SetSource(SourceFile, "labels", map[string]any{
|
||||
"env": "production",
|
||||
"version": "1.2.3",
|
||||
})
|
||||
|
||||
// Scan into struct
|
||||
var result AppConfig
|
||||
err = cfg.Scan("", &result)
|
||||
err = cfg.Scan(&result)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify conversions
|
||||
@ -100,7 +100,7 @@ func TestScanWithBasePath(t *testing.T) {
|
||||
|
||||
// Scan only the server section
|
||||
var server ServerConfig
|
||||
err := cfg.Scan("app.server", &server)
|
||||
err := cfg.Scan(&server, "app.server")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "appserver", server.Host)
|
||||
@ -109,7 +109,7 @@ func TestScanWithBasePath(t *testing.T) {
|
||||
|
||||
// Test non-existent base path
|
||||
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.Equal(t, "", empty.Host)
|
||||
assert.Equal(t, 0, empty.Port)
|
||||
@ -124,9 +124,9 @@ func TestScanFromSource(t *testing.T) {
|
||||
cfg := New()
|
||||
cfg.Register("value", "default")
|
||||
|
||||
cfg.SetSource("value", SourceFile, "fromfile")
|
||||
cfg.SetSource("value", SourceEnv, "fromenv")
|
||||
cfg.SetSource("value", SourceCLI, "fromcli")
|
||||
cfg.SetSource(SourceFile, "value", "fromfile")
|
||||
cfg.SetSource(SourceEnv, "value", "fromenv")
|
||||
cfg.SetSource(SourceCLI, "value", "fromcli")
|
||||
|
||||
tests := []struct {
|
||||
source Source
|
||||
@ -141,7 +141,7 @@ func TestScanFromSource(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(string(tt.source), func(t *testing.T) {
|
||||
var result Config
|
||||
err := cfg.ScanSource("", tt.source, &result)
|
||||
err := cfg.ScanSource(tt.source, &result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, result.Value)
|
||||
})
|
||||
@ -165,7 +165,7 @@ func TestInvalidScanTargets(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := cfg.Scan("", tt.target)
|
||||
err := cfg.Scan(tt.target)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.expectErr)
|
||||
})
|
||||
@ -185,7 +185,7 @@ func TestCustomTypeConversion(t *testing.T) {
|
||||
cfg.Set("ip", "not-an-ip")
|
||||
|
||||
var result Config
|
||||
err := cfg.Scan("", &result)
|
||||
err := cfg.Scan(&result)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid IP address")
|
||||
})
|
||||
@ -199,7 +199,7 @@ func TestCustomTypeConversion(t *testing.T) {
|
||||
cfg.Set("network", "invalid-cidr")
|
||||
|
||||
var result Config
|
||||
err := cfg.Scan("", &result)
|
||||
err := cfg.Scan(&result)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid CIDR")
|
||||
})
|
||||
@ -213,7 +213,7 @@ func TestCustomTypeConversion(t *testing.T) {
|
||||
cfg.Set("endpoint", "://invalid-url")
|
||||
|
||||
var result Config
|
||||
err := cfg.Scan("", &result)
|
||||
err := cfg.Scan(&result)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid URL")
|
||||
})
|
||||
@ -232,7 +232,7 @@ func TestCustomTypeConversion(t *testing.T) {
|
||||
cfg.Set("ip", string(longIP))
|
||||
|
||||
var result Config
|
||||
err := cfg.Scan("", &result)
|
||||
err := cfg.Scan(&result)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid IP length")
|
||||
})
|
||||
@ -251,7 +251,7 @@ func TestCustomTypeConversion(t *testing.T) {
|
||||
cfg.Set("url", longURL)
|
||||
|
||||
var result Config
|
||||
err := cfg.Scan("", &result)
|
||||
err := cfg.Scan(&result)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "URL too long")
|
||||
})
|
||||
@ -286,7 +286,7 @@ func TestZeroFields(t *testing.T) {
|
||||
}{Field: "initial"},
|
||||
}
|
||||
|
||||
err := cfg.Scan("", &result)
|
||||
err := cfg.Scan(&result)
|
||||
require.NoError(t, err)
|
||||
|
||||
// ZeroFields should reset all fields before decoding
|
||||
@ -317,7 +317,7 @@ func TestWeaklyTypedInput(t *testing.T) {
|
||||
cfg.Set("string_from_bool", true)
|
||||
|
||||
var result Config
|
||||
err := cfg.Scan("", &result)
|
||||
err := cfg.Scan(&result)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 42, result.IntFromString)
|
||||
|
||||
@ -43,7 +43,7 @@ portNum := port.(int64) // Won't panic - type is enforced
|
||||
|
||||
```go
|
||||
// Get value from specific source
|
||||
envPort, exists := cfg.GetSource("server.port", config.SourceEnv)
|
||||
envPort, exists := cfg.GetSource(config.SourceEnv, "server.port")
|
||||
if exists {
|
||||
log.Printf("Port from environment: %v", envPort)
|
||||
}
|
||||
@ -68,7 +68,7 @@ var serverConfig struct {
|
||||
} `toml:"tls"`
|
||||
}
|
||||
|
||||
if err := cfg.Scan("server", &serverConfig); err != nil {
|
||||
if err := cfg.Scan(&serverConfig, "server"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@ -158,11 +158,11 @@ if err := cfg.Set("server.port", int64(9090)); err != nil {
|
||||
|
||||
```go
|
||||
// Set value in specific source
|
||||
cfg.SetSource("server.port", config.SourceEnv, "8080")
|
||||
cfg.SetSource("debug", config.SourceCLI, true)
|
||||
cfg.SetSource(config.SourceEnv, "server.port", "8080")
|
||||
cfg.SetSource(config.SourceCLI, "debug", true)
|
||||
|
||||
// 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
|
||||
|
||||
@ -204,7 +204,7 @@ func (c *Config) loadFile(path string) error {
|
||||
delete(item.values, SourceFile)
|
||||
}
|
||||
// Recompute the current value based on new source precedence.
|
||||
item.currentValue = c.computeValue(path, item)
|
||||
item.currentValue = c.computeValue(item)
|
||||
c.items[path] = item
|
||||
}
|
||||
|
||||
@ -261,7 +261,7 @@ func (c *Config) loadEnv(opts LoadOptions) error {
|
||||
item.values = make(map[Source]any)
|
||||
}
|
||||
item.values[SourceEnv] = value // Store as string
|
||||
item.currentValue = c.computeValue(path, item)
|
||||
item.currentValue = c.computeValue(item)
|
||||
c.items[path] = item
|
||||
c.envData[path] = value
|
||||
}
|
||||
@ -296,7 +296,7 @@ func (c *Config) loadCLI(args []string) error {
|
||||
item.values = make(map[Source]any)
|
||||
}
|
||||
item.values[SourceCLI] = value
|
||||
item.currentValue = c.computeValue(path, item)
|
||||
item.currentValue = c.computeValue(item)
|
||||
c.items[path] = item
|
||||
}
|
||||
}
|
||||
|
||||
@ -375,9 +375,9 @@ func TestAtomicSave(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("SaveSpecificSource", func(t *testing.T) {
|
||||
cfg.SetSource("server.host", SourceEnv, "envhost")
|
||||
cfg.SetSource("server.port", SourceEnv, "7777")
|
||||
cfg.SetSource("server.port", SourceFile, "6666")
|
||||
cfg.SetSource(SourceEnv, "server.host", "envhost")
|
||||
cfg.SetSource(SourceEnv, "server.port", "7777")
|
||||
cfg.SetSource(SourceFile, "server.port", "6666")
|
||||
|
||||
savePath := filepath.Join(tmpDir, "env-only.toml")
|
||||
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
|
||||
if value, exists := os.LookupEnv(envVar); exists {
|
||||
parsed := parseValue(value)
|
||||
return c.SetSource(path, SourceEnv, parsed)
|
||||
return c.SetSource(SourceEnv, path, parsed)
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -230,7 +230,7 @@ func (c *Config) registerFields(v reflect.Value, pathPrefix, fieldPath string, e
|
||||
if envTag != "" && err == nil {
|
||||
if value, exists := os.LookupEnv(envTag); exists {
|
||||
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))
|
||||
}
|
||||
}
|
||||
@ -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.
|
||||
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()
|
||||
defer c.mutex.RUnlock()
|
||||
|
||||
result := make(map[string]bool)
|
||||
for path := range c.items {
|
||||
if strings.HasPrefix(path, prefix) {
|
||||
if strings.HasPrefix(path, p) {
|
||||
result[path] = true
|
||||
}
|
||||
}
|
||||
@ -254,13 +259,18 @@ func (c *Config) GetRegisteredPaths(prefix string) map[string]bool {
|
||||
}
|
||||
|
||||
// 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()
|
||||
defer c.mutex.RUnlock()
|
||||
|
||||
result := make(map[string]any)
|
||||
for path, item := range c.items {
|
||||
if strings.HasPrefix(path, prefix) {
|
||||
if strings.HasPrefix(path, p) {
|
||||
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
|
||||
func (c *Config) Scan(basePath string, target any) error {
|
||||
return c.unmarshal(basePath, "", target) // Empty source means use merged state
|
||||
func (c *Config) Scan(target any, basePath ...string) error {
|
||||
return c.unmarshal("", target, basePath...)
|
||||
}
|
||||
|
||||
// ScanSource decodes configuration from specific source using unified unmarshal
|
||||
func (c *Config) ScanSource(basePath string, source Source, target any) error {
|
||||
return c.unmarshal(basePath, source, target)
|
||||
func (c *Config) ScanSource(source Source, target any, basePath ...string) error {
|
||||
return c.unmarshal(source, target, basePath...)
|
||||
}
|
||||
Reference in New Issue
Block a user