diff --git a/config.go b/config.go index 8494f44e..e98927c3 100644 --- a/config.go +++ b/config.go @@ -124,7 +124,7 @@ func (c *Config) Unmarshal(path string, target any) error { } c.nocopy.Check() - value := c.providers.value() + value := c.providers.sub(c.splitPath(path)) if value == nil { // To support zero Config return nil } @@ -133,7 +133,7 @@ func (c *Config) Unmarshal(path string, target any) error { if converter == nil { // To support zero Config converter = defaultConverter } - if err := converter.Convert(c.sub(value, path), target); err != nil { + if err := converter.Convert(value, target); err != nil { return fmt.Errorf("decode: %w", err) } @@ -148,12 +148,15 @@ func (c *Config) log(ctx context.Context, level slog.Level, message string, attr logger.LogAttrs(ctx, level, message, attrs...) } -func (c *Config) sub(values map[string]any, path string) any { +func (c *Config) splitPath(path string) []string { + if path == "" { + return nil + } if !c.caseSensitive { path = defaultKeyMap(path) } - return maps.Sub(values, path, c.delim()) + return strings.Split(path, c.delim()) } func (c *Config) delim() string { @@ -179,13 +182,13 @@ func (c *Config) Explain(path string) string { } c.nocopy.Check() - value := c.providers.value() + value := c.providers.sub(c.splitPath(path)) if value == nil { // To support zero Config return path + " has no configuration.\n\n" } explanation := &strings.Builder{} - c.explain(explanation, path, c.sub(value, path)) + c.explain(explanation, path, value) return explanation.String() } @@ -210,7 +213,7 @@ func (c *Config) explain(explanation *strings.Builder, path string, value any) { } var loaders []loaderValue c.providers.traverse(func(provider *provider) { - if v := c.sub(*provider.values.Load(), path); v != nil { + if v := maps.Sub(*provider.values.Load(), c.splitPath(path)); v != nil { loaders = append(loaders, loaderValue{provider.loader, v}) } }) @@ -282,13 +285,13 @@ func (p *providers) traverse(action func(*provider)) { } } -func (p *providers) value() map[string]any { +func (p *providers) sub(path []string) any { val := p.values.Load() if val == nil { return nil } - return *val + return maps.Sub(*val, path) } //nolint:gochecknoglobals diff --git a/internal/maps/sub.go b/internal/maps/sub.go index 808a5a4e..2458a082 100644 --- a/internal/maps/sub.go +++ b/internal/maps/sub.go @@ -3,21 +3,21 @@ package maps -import "strings" +import "slices" -func Sub(values map[string]any, path string, delimiter string) any { - if path == "" { +func Sub(values map[string]any, path []string) any { + path = slices.Compact(path) + if len(path) == 0 { return values } - key, path, _ := strings.Cut(path, delimiter) - _, value := Unpack(values[key]) - if path == "" { + _, value := Unpack(values[path[0]]) + if len(path) == 1 { return value } if mp, ok := value.(map[string]any); ok { - return Sub(mp, path, delimiter) + return Sub(mp, path[1:]) } return nil diff --git a/internal/maps/sub_test.go b/internal/maps/sub_test.go index cef809c0..e959522f 100644 --- a/internal/maps/sub_test.go +++ b/internal/maps/sub_test.go @@ -16,67 +16,65 @@ func TestSub(t *testing.T) { testcases := []struct { description string values map[string]any - path string + path []string expected any }{ { description: "nil values", values: nil, - path: "a.b", + path: []string{"a", "b"}, expected: nil, }, { description: "empty values", values: map[string]any{}, - path: "a.b", + path: []string{"a", "b"}, expected: nil, }, { description: "empty keys", values: map[string]any{"a": 1}, - path: "", expected: map[string]any{"a": 1}, }, { description: "blank keys", values: map[string]any{"a": 1}, - path: "", expected: map[string]any{"a": 1}, }, { description: "lower case keys", values: map[string]any{"a": 1, "b": 2}, - path: "a", + path: []string{"a"}, expected: 1, }, { description: "upper case keys", values: map[string]any{"A": 1}, - path: "A", + path: []string{"A"}, expected: 1, }, { description: "keyvalue", values: map[string]any{"a": maps.Pack("A", 1)}, - path: "a", + path: []string{"a"}, expected: 1, }, { description: "value not exist", values: map[string]any{"a": 1}, - path: "a.b", + path: []string{"a", "b"}, expected: nil, }, { description: "nest map", values: map[string]any{"a": map[string]any{"x": 1, "y": 2}}, - path: "a.y", + path: []string{"a", "y"}, expected: 2, }, { description: "non-map value", values: map[string]any{"a": map[string]any{"x": 1}}, - path: "x.y", + path: []string{"x", "y"}, expected: nil, }, } @@ -85,7 +83,7 @@ func TestSub(t *testing.T) { t.Run(testcase.description, func(t *testing.T) { t.Parallel() - actual := maps.Sub(testcase.values, testcase.path, ".") + actual := maps.Sub(testcase.values, testcase.path) assert.Equal(t, testcase.expected, actual) }) } diff --git a/provider.go b/provider.go index 862fc0fd..1cf62aaf 100644 --- a/provider.go +++ b/provider.go @@ -5,7 +5,6 @@ package konf import ( "context" - "strings" ) // Loader is the interface that wraps the Load method. @@ -41,10 +40,5 @@ func (c *Config) Exists(path []string) bool { } c.nocopy.Check() - value := c.providers.value() - if value == nil { - return false // To support zero Config - } - - return c.sub(value, strings.Join(path, c.delim())) != nil + return c.providers.sub(path) != nil } diff --git a/watch.go b/watch.go index ea891871..efaa3f99 100644 --- a/watch.go +++ b/watch.go @@ -11,6 +11,8 @@ import ( "reflect" "sync" "time" + + "github.com/nil-go/konf/internal/maps" ) // Watch watches and updates configuration when it changes. @@ -91,7 +93,9 @@ func (c *Config) Watch(ctx context.Context) error { //nolint:cyclop,funlen,gocog oldValues := *provider.values.Swap(&values) onChangesChannel <- c.onChanges.get( func(path string) bool { - return !reflect.DeepEqual(c.sub(oldValues, path), c.sub(values, path)) + paths := c.splitPath(path) + + return !reflect.DeepEqual(maps.Sub(oldValues, paths), maps.Sub(values, paths)) }, )