diff --git a/viper.go b/viper.go index 30ae4aa..3c5ab6d 100644 --- a/viper.go +++ b/viper.go @@ -34,6 +34,7 @@ import ( "sync" "time" + "github.com/deckarep/golang-set" "github.com/fsnotify/fsnotify" "github.com/go-viper/mapstructure/v2" "github.com/spf13/afero" @@ -1894,6 +1895,32 @@ func (v *Viper) AllKeys() []string { return a } +// Get the set of all top level keys. +func TopLevelKeys() []string { return v.TopLevelKeys() } +func (v *Viper) TopLevelKeys() []string { + s := mapset.NewSetFromSlice(v.getMapKeys(castMapStringToMapInterface(v.aliases))) + s = s.Union(mapset.NewSetFromSlice(v.getMapKeys(v.override))) + s = s.Union(mapset.NewSetFromSlice(v.getMapKeys(castMapFlagToMapInterface(v.pflags)))) + s = s.Union(mapset.NewSetFromSlice(v.getMapKeys(castMapStringToMapInterface(v.env)))) + s = s.Union(mapset.NewSetFromSlice(v.getMapKeys(v.config))) + s = s.Union(mapset.NewSetFromSlice(v.getMapKeys(v.defaults))) + + // convert interface{} to string + keys := []string{} + for _, k := range s.ToSlice() { + keys = append(keys, k.(string)) + } + return keys +} + +func (v *Viper) getMapKeys(m map[string]interface{}) []interface{} { + keys := []interface{}{} + for key := range m { + keys = append(keys, key) + } + return keys +} + // flattenAndMergeMap recursively flattens the given map into a map[string]bool // of key paths (used as a set, easier to manipulate than a []string): // - each path is merged into a single key string, delimited with v.keyDelim diff --git a/viper_test.go b/viper_test.go index 2edc83f..b5429fb 100644 --- a/viper_test.go +++ b/viper_test.go @@ -52,6 +52,23 @@ var yamlExampleWithExtras = []byte(`Existing: true Bogus: true `) +var yamlTopLevelKeys = []byte(` +restuarants: + types: + sushi: + name: sushi2go + rating: 10 + price: average + italian: + name: "pizza place" + rating: 7 + price: average + thai: + name: "thai garden" + rating: 9 + price: high +`) + type testUnmarshalExtra struct { Existing bool } @@ -971,6 +988,35 @@ func TestAllKeysWithEnv(t *testing.T) { assert.ElementsMatch(t, []string{"id", "foo.bar"}, v.AllKeys()) } +func TestTopLevelKeys(t *testing.T) { + v := New() + v.SetConfigType("yaml") + r := bytes.NewReader(yamlTopLevelKeys) + err := v.ReadConfig(r) + assert.NoError(t, err) + fmt.Printf("%#v\n", v.config) + s := v.Sub("restuarants.types") + for _, i := range s.TopLevelKeys() { + ss := s.Sub(i) + switch i { + case "sushi": + assert.Equal(t, ss.GetString("name"), "sushi2go") + assert.Equal(t, ss.GetString("price"), "average") + assert.Equal(t, ss.GetInt("rating"), 10) + case "italian": + assert.Equal(t, ss.GetString("name"), "pizza place") + assert.Equal(t, ss.GetString("price"), "average") + assert.Equal(t, ss.GetInt("rating"), 7) + case "thai": + assert.Equal(t, ss.GetString("name"), "thai garden") + assert.Equal(t, ss.GetString("price"), "high") + assert.Equal(t, ss.GetInt("rating"), 9) + default: + t.Fatalf("unexpected key, %q", i) + } + } +} + func TestAliasesOfAliases(t *testing.T) { v := New() v.Set("Title", "Checking Case")