Skip to content

Commit

Permalink
Add CaseSensitiveKeys option
Browse files Browse the repository at this point in the history
When reading configuration from sources with case-sensitive keys,
such as YAML, TOML, and JSON, a user may wish to preserve the case
of keys that appear in maps.  For example, consider when the value
of a setting is a map with string keys that are case-sensitive.
Ideally, if the value is not going to be indexed by a Viper lookup
key, then the map value should be treated as an opaque value by
Viper, and its keys should not be modified.  See spf13#1014

Viper's default behaviour is that keys are case-sensitive, and this
behavior is implemented by converting all keys to lower-case.  For
users that wish to preserve the case of keys, this commit introduces
an Option `CaseSensitiveKeys()` that can be used to configure Viper
to use case-sensitive keys.  When CaseSensitiveKeys is enabled, all
keys retain the original case, and lookups become case-sensitive
(except for lookups of values bound to environment variables).

The behavior of Viper could become hard to understand if a user
could change the CaseSensitiveKeys setting after values have been
stored.  For this reason, the setting may only be set when creating
a Viper instance, and it cannot be set on the "global" Viper.
  • Loading branch information
travisnewhouse committed Oct 20, 2023
1 parent b5daec6 commit 21336ce
Show file tree
Hide file tree
Showing 5 changed files with 556 additions and 82 deletions.
53 changes: 28 additions & 25 deletions util.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,39 +37,42 @@ func (pe ConfigParseError) Unwrap() error {
return pe.err
}

// toCaseInsensitiveValue checks if the value is a map;
// if so, create a copy and lower-case the keys recursively.
func toCaseInsensitiveValue(value any) any {
// CopyMap returns a deep copy of a map[any]any or map[string]any. If value is
// not one of those map types, then it is returned as-is. If preserveCase is
// false, then all keys will be converted to lower-case in the copy that is
// returned.
func CopyMap(value any, preserveCase bool) any {
var copyMap func(map[string]any, bool) map[string]any
copyMap = func(m map[string]any, preserveCase bool) map[string]any {
nm := make(map[string]any)

for key, val := range m {
if !preserveCase {
key = strings.ToLower(key)
}
switch v := val.(type) {
case map[any]any:
nm[key] = copyMap(cast.ToStringMap(v), preserveCase)
case map[string]any:
nm[key] = copyMap(v, preserveCase)
default:
nm[key] = v
}
}

return nm
}

switch v := value.(type) {
case map[any]any:
value = copyAndInsensitiviseMap(cast.ToStringMap(v))
value = copyMap(cast.ToStringMap(v), preserveCase)
case map[string]any:
value = copyAndInsensitiviseMap(v)
value = copyMap(v, preserveCase)
}

return value
}

// copyAndInsensitiviseMap behaves like insensitiviseMap, but creates a copy of
// any map it makes case insensitive.
func copyAndInsensitiviseMap(m map[string]any) map[string]any {
nm := make(map[string]any)

for key, val := range m {
lkey := strings.ToLower(key)
switch v := val.(type) {
case map[any]any:
nm[lkey] = copyAndInsensitiviseMap(cast.ToStringMap(v))
case map[string]any:
nm[lkey] = copyAndInsensitiviseMap(v)
default:
nm[lkey] = v
}
}

return nm
}

func insensitiviseVal(val any) any {
switch v := val.(type) {
case map[any]any:
Expand Down
55 changes: 45 additions & 10 deletions util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
"github.com/stretchr/testify/assert"
)

func TestCopyAndInsensitiviseMap(t *testing.T) {
func TestCopyMap(t *testing.T) {
var (
given = map[string]any{
"Foo": 32,
Expand All @@ -35,19 +35,54 @@ func TestCopyAndInsensitiviseMap(t *testing.T) {
"cde": "B",
},
}
expectedPreserveCase = map[string]any{
"Foo": 32,
"Bar": map[string]any{
"ABc": "A",
"cDE": "B",
},
}
)

got := copyAndInsensitiviseMap(given)
t.Run("convert to lower-case", func(t *testing.T) {
got := CopyMap(given, false)

assert.Equal(t, expected, got)
_, ok := given["foo"]
assert.False(t, ok)
_, ok = given["bar"]
assert.False(t, ok)

m := given["Bar"].(map[any]any)
_, ok = m["ABc"]
assert.True(t, ok)
})

t.Run("preserve case", func(t *testing.T) {
got := CopyMap(given, true)

assert.Equal(t, expectedPreserveCase, got)
_, ok := given["foo"]
assert.False(t, ok)
_, ok = given["bar"]
assert.False(t, ok)

m := given["Bar"].(map[any]any)
_, ok = m["ABc"]
assert.True(t, ok)
})

assert.Equal(t, expected, got)
_, ok := given["foo"]
assert.False(t, ok)
_, ok = given["bar"]
assert.False(t, ok)
t.Run("not a map", func(t *testing.T) {
var (
given = []any{42, "xyz"}
expected = []any{42, "xyz"}
)
got := CopyMap(given, false)
assert.Equal(t, expected, got)

m := given["Bar"].(map[any]any)
_, ok = m["ABc"]
assert.True(t, ok)
got = CopyMap(given, true)
assert.Equal(t, expected, got)
})
}

func TestAbsPathify(t *testing.T) {
Expand Down
Loading

0 comments on commit 21336ce

Please sign in to comment.