diff --git a/koanf.go b/koanf.go index b4bae13..fb57a50 100644 --- a/koanf.go +++ b/koanf.go @@ -368,26 +368,29 @@ func (ko *Koanf) Get(path string) any { return out } -// Slices returns a list of Koanf instances constructed from the value -// at path. It accepts []map[string]any or []any containing map[string]any -// elements. Non-slice elements are skipped. +// Slices returns a list of Koanf instances constructed out of a +// []map[string]any interface at the given path. func (ko *Koanf) Slices(path string) []*Koanf { out := []*Koanf{} if path == "" { return out } - switch v := ko.Get(path).(type) { - case []map[string]any: - for _, mp := range v { - out = ko.appendMap(mp, out) - } - case []any: - for _, item := range v { - if mp, ok := item.(map[string]any); ok { - out = ko.appendMap(mp, out) - } + // Does the path exist? + sl, ok := ko.Get(path).([]any) + if !ok { + return out + } + + for _, s := range sl { + mp, ok := s.(map[string]any) + if !ok { + continue } + + k := New(ko.conf.Delim) + _ = k.merge(mp, new(options)) + out = append(out, k) } return out @@ -413,16 +416,13 @@ func (ko *Koanf) MapKeys(path string) []string { return out } - // A value set with Set() or by a Provider such as structs keeps its native - // map type, so the keys have to be read through reflection too. - rv := reflect.ValueOf(o) - if rv.Kind() != reflect.Map || rv.Type().Key().Kind() != reflect.String { + mp, ok := o.(map[string]any) + if !ok { return out } - - out = make([]string, 0, rv.Len()) - for _, k := range rv.MapKeys() { - out = append(out, k.String()) + out = make([]string, 0, len(mp)) + for k := range mp { + out = append(out, k) } sort.Strings(out) return out @@ -656,10 +656,3 @@ func textUnmarshalerHookFunc() mapstructure.DecodeHookFuncType { return result, nil } } - -// appendMap creates new Koanf instances from a map returns a slice of Koanf instances. -func (ko *Koanf) appendMap(mp map[string]any, out []*Koanf) []*Koanf { - k := New(ko.conf.Delim) - _ = k.merge(mp, new(options)) - return append(out, k) -} diff --git a/tests/koanf_test.go b/tests/koanf_test.go index a8d772e..39cbe92 100644 --- a/tests/koanf_test.go +++ b/tests/koanf_test.go @@ -1307,90 +1307,6 @@ func TestSlices(t *testing.T) { } } -func TestSlicesNativeMapSlice(t *testing.T) { - assert := assert.New(t) - - // A natively-typed []map[string]any (as produced by the confmap and - // structs providers, and named in Slices' own doc) must be handled too, - // not just a []any of maps decoded by a parser. It was silently dropped. - k := koanf.New(delim) - require.NoError(t, k.Load(confmap.Provider(map[string]any{ - "servers": []map[string]any{ - {"host": "a", "port": 1}, - {"host": "b", "port": 2}, - }, - }, "."), nil)) - - slices := k.Slices("servers") - require.Len(t, slices, 2, "Slices dropped a []map[string]any value") - assert.Equal("a", slices[0].String("host")) - assert.Equal(1, slices[0].Int("port")) - assert.Equal("b", slices[1].String("host")) - assert.Equal(2, slices[1].Int("port")) -} - -func TestSlicesEdgeCases(t *testing.T) { - for _, tt := range []struct { - name string - value any - want []map[string]any - }{ - {"nil_native_slice", []map[string]any(nil), []map[string]any{}}, - {"empty_native_slice", []map[string]any{}, []map[string]any{}}, - {"nil_interface_slice", []any(nil), []map[string]any{}}, - {"empty_interface_slice", []any{}, []map[string]any{}}, - {"nil_and_empty_native_maps", []map[string]any{nil, {}}, []map[string]any{{}, {}}}, - {"nil_and_empty_interface_maps", []any{map[string]any(nil), map[string]any{}}, []map[string]any{{}, {}}}, - { - "mixed_interface_slice", - []any{nil, 42, map[string]any{"host": "a"}, "invalid", map[string]any{"host": "b"}}, - []map[string]any{{"host": "a"}, {"host": "b"}}, - }, - } { - t.Run(tt.name, func(t *testing.T) { - k := koanf.New(delim) - require.NoError(t, k.Load(confmap.Provider(map[string]any{"servers": tt.value}, ""), nil)) - - slices := k.Slices("servers") - require.NotNil(t, slices) - require.Len(t, slices, len(tt.want)) - for i, child := range slices { - assert.Equal(t, tt.want[i], child.Raw()) - } - }) - } -} - -func TestSlicesCopyIsolation(t *testing.T) { - for _, native := range []bool{false, true} { - name := "interface_slice" - if native { - name = "native_slice" - } - t.Run(name, func(t *testing.T) { - // Reuse the same map to check that siblings are independent too. - mp := map[string]any{"nested": map[string]any{"host": "original"}} - var value any = []any{mp, mp} - if native { - value = []map[string]any{mp, mp} - } - - k := koanf.New("/") - require.NoError(t, k.Load(confmap.Provider(map[string]any{"servers": value}, ""), nil)) - before := k.Get("servers") - slices := k.Slices("servers") - require.Len(t, slices, 2) - assert.Equal(t, "/", slices[0].Delim()) - assert.Equal(t, "original", slices[0].String("nested/host")) - - require.NoError(t, slices[0].Set("nested/host", "changed")) - assert.Equal(t, "changed", slices[0].String("nested/host")) - assert.Equal(t, "original", slices[1].String("nested/host")) - assert.Equal(t, before, k.Get("servers")) - }) - } -} - func TestGetTypes(t *testing.T) { assert := assert.New(t) for _, c := range cases { @@ -1606,38 +1522,6 @@ func TestGetStringsMap(t *testing.T) { assert.Equal(map[string][]string{"k2": {"value"}}, k.StringsMap("ifaces3"), "types don't match") } -func TestGetStringsMapEmptySlices(t *testing.T) { - for _, tt := range []struct { - name string - value any - }{ - {"string_slices", map[string][]string{"empty": {}, "nil": nil}}, - {"interface_slices", map[string][]any{"empty": {}, "nil": nil}}, - {"interface_map", map[string]any{"empty": []any{}, "nil": []any(nil)}}, - } { - t.Run(tt.name, func(t *testing.T) { - k := koanf.New(delim) - require.NoError(t, k.Set("lists", tt.value)) - want := map[string][]string{"empty": nil, "nil": nil} - require.Equal(t, want, k.StringsMap("lists")) - require.Equal(t, want, k.MustStringsMap("lists")) - }) - } - - t.Run("json", func(t *testing.T) { - k := koanf.New(delim) - require.NoError(t, k.Load(rawbytes.Provider([]byte(`{"lists":{"empty":[],"populated":["value"]}}`)), json.Parser())) - require.Equal(t, map[string][]string{"empty": nil, "populated": {"value"}}, k.StringsMap("lists")) - }) - - t.Run("invalid_element", func(t *testing.T) { - k := koanf.New(delim) - require.NoError(t, k.Set("lists", map[string][]any{"empty": {}, "invalid": {42}})) - require.Empty(t, k.StringsMap("lists")) - require.Panics(t, func() { k.MustStringsMap("lists") }) - }) -} - func TestBoolsNativeSlice(t *testing.T) { assert := assert.New(t) @@ -1649,59 +1533,6 @@ func TestBoolsNativeSlice(t *testing.T) { assert.Equal([]bool{true, false, true}, k.Bools("bools")) } -func TestTypedGettersNativeMaps(t *testing.T) { - for _, tt := range []struct { - name string - value any - get func(*koanf.Koanf, string) any - }{ - {"int", map[string]int{"a": 1, "b": 2}, func(k *koanf.Koanf, path string) any { return k.IntMap(path) }}, - {"int64", map[string]int64{"a": 1 << 54, "b": -2}, func(k *koanf.Koanf, path string) any { return k.Int64Map(path) }}, - {"float64", map[string]float64{"a": 1.5, "b": -2.5}, func(k *koanf.Koanf, path string) any { return k.Float64Map(path) }}, - {"bool", map[string]bool{"a": true, "b": false}, func(k *koanf.Koanf, path string) any { return k.BoolMap(path) }}, - {"nil_int", map[string]int(nil), func(k *koanf.Koanf, path string) any { return k.IntMap(path) }}, - {"nil_int64", map[string]int64(nil), func(k *koanf.Koanf, path string) any { return k.Int64Map(path) }}, - {"nil_float64", map[string]float64(nil), func(k *koanf.Koanf, path string) any { return k.Float64Map(path) }}, - {"nil_bool", map[string]bool(nil), func(k *koanf.Koanf, path string) any { return k.BoolMap(path) }}, - } { - t.Run(tt.name, func(t *testing.T) { - k := koanf.New(delim) - require.NoError(t, k.Load(confmap.Provider(map[string]any{"value": tt.value}, ""), nil)) - got := tt.get(k, "value") - require.NotNil(t, got) - if strings.HasPrefix(tt.name, "nil_") { - assert.Empty(t, got) - return - } - assert.Equal(t, tt.value, got) - - // Changing the returned map must not change the stored configuration. - switch mp := got.(type) { - case map[string]int: - mp["a"] = 0 - case map[string]int64: - mp["a"] = 0 - case map[string]float64: - mp["a"] = 0 - case map[string]bool: - mp["a"] = false - } - assert.Equal(t, tt.value, tt.get(k, "value")) - }) - } -} - -func TestMapKeysNativeMap(t *testing.T) { - assert := assert.New(t) - - k := koanf.New(delim) - assert.NoError(k.Load(confmap.Provider(map[string]any{ - "ints": map[string]int64{"a": 1, "b": 2}, - }, "."), nil)) - - assert.Equal([]string{"a", "b"}, k.MapKeys("ints"), "map keys mismatch for a natively-typed map") -} - // waitTimeout waits for the waitgroup for the specified max timeout. // Returns true if waiting timed out. func waitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {