e5.2.1 Improved interfaces of functions using path and prefix.

This commit is contained in:
2025-07-20 20:53:22 -04:00
parent 06cddbe00e
commit 3aa2ab30d6
11 changed files with 105 additions and 114 deletions

View File

@ -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"])

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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...)
} }