Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,7 @@ if err != nil {

If you want to unmarshal configuration where the keys themselves contain `.`
(the default key delimiter), you can change the delimiter.
The delimiter is case-insensitive when using alphabetic characters.

```go
v := viper.NewWithOptions(viper.KeyDelimiter("::"))
Expand Down
5 changes: 3 additions & 2 deletions viper.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ func (fn optionFunc) apply(v *Viper) {
// By default it's value is ".".
func KeyDelimiter(d string) Option {
return optionFunc(func(v *Viper) {
v.keyDelim = d
v.keyDelim = strings.ToLower(d)
})
}

Expand Down Expand Up @@ -486,7 +486,6 @@ func (v *Viper) searchIndexableWithPathPrefixes(source any, path []string) any {
// search for path prefixes, starting from the longest one
for i := len(path); i > 0; i-- {
prefixKey := strings.ToLower(strings.Join(path[0:i], v.keyDelim))

var val any
switch sourceIndexable := source.(type) {
case []any:
Expand Down Expand Up @@ -1464,10 +1463,12 @@ func Set(key string, value any) { v.Set(key, value) }

func (v *Viper) Set(key string, value any) {
// If alias passed in, then set the proper override

key = v.realKey(strings.ToLower(key))
value = toCaseInsensitiveValue(value)

path := strings.Split(key, v.keyDelim)

lastKey := strings.ToLower(path[len(path)-1])
deepestMap := deepSearch(v.override, path[0:len(path)-1])

Expand Down
151 changes: 118 additions & 33 deletions viper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2533,50 +2533,135 @@ func TestUnmarshal_DotSeparatorBackwardCompatibility(t *testing.T) {
// `)

func TestKeyDelimiter(t *testing.T) {
v := NewWithOptions(KeyDelimiter("::"))
v.SetConfigType("yaml")
r := strings.NewReader(string(yamlExampleWithDot))
t.Run("KeyDelimiterYAMLAndUnmarshal", func(t *testing.T) {
v := NewWithOptions(KeyDelimiter("::"))
v.SetConfigType("yaml")
r := strings.NewReader(string(yamlExampleWithDot))

err := v.unmarshalReader(r, v.config)
require.NoError(t, err)
err := v.unmarshalReader(r, v.config)
require.NoError(t, err)

values := map[string]any{
"image": map[string]any{
"repository": "someImage",
"tag": "1.0.0",
},
"ingress": map[string]any{
"annotations": map[string]any{
"traefik.frontend.rule.type": "PathPrefix",
"traefik.ingress.kubernetes.io/ssl-redirect": "true",
values := map[string]any{
"image": map[string]any{
"repository": "someImage",
"tag": "1.0.0",
},
},
}
"ingress": map[string]any{
"annotations": map[string]any{
"traefik.frontend.rule.type": "PathPrefix",
"traefik.ingress.kubernetes.io/ssl-redirect": "true",
},
},
}

v.SetDefault("charts::values", values)
v.SetDefault("charts::values", values)

assert.Equal(t, "leather", v.GetString("clothing::jacket"))
assert.Equal(t, "01/02/03", v.GetString("emails::[email protected]::created"))
// It retrieves existing values using the custom delimeter
assert.Equal(t, "leather", v.GetString("clothing::jacket"))
assert.Equal(t, "01/02/03", v.GetString("emails::[email protected]::created"))

type config struct {
Charts struct {
Values map[string]any
type config struct {
Charts struct {
Values map[string]any
}
}
}

expected := config{
Charts: struct {
Values map[string]any
}{
Values: values,
},
}
expected := config{
Charts: struct {
Values map[string]any
}{
Values: values,
},
}

var actual config
var actual config

require.NoError(t, v.Unmarshal(&actual))
require.NoError(t, v.Unmarshal(&actual))

assert.Equal(t, expected, actual)
// It sets and unmarshals new values with the custom delimiter
assert.Equal(t, expected, actual)
})

// Test the Set method with key delimiter for case insenitivty
t.Run("CaseInsensitiveDelimiter", func(t *testing.T) {
v := NewWithOptions(KeyDelimiter("Z"))
v.Set("fooZbar", "Foo Bar Baz")

got := v.Get("foo")
want := map[string]any{"bar": "Foo Bar Baz"}
assert.Equal(t, want, got)

v.Set("foozbar", "Foo Bar Baz")
got = v.Get("foo")
assert.Equal(t, want, got)

// Verify that setting a key ending with the delimiter does not create an empty key after the final character.
v.Set("baz", "Bazzz")
got = v.Get("baz")
want2 := "Bazzz"
assert.Equal(t, want2, got)
})

// Test the InConfig method with key delimiter for case insenitivty
t.Run("UpperCasedDelimInConfig", func(t *testing.T) {
v := NewWithOptions(KeyDelimiter("Z"))

v.config = map[string]any{
"foo": map[string]any{
"bar": "nestedValue",
},
}
assert.True(t, v.InConfig("fooZbar"))
assert.True(t, v.InConfig("foozbar"))
})

// Test the SetDefault method with key delimiter for case insenitivty
t.Run("UpperCasedDelimSetDefault", func(t *testing.T) {
v := NewWithOptions(KeyDelimiter("Z"))
v.SetDefault("fooZbar", "Foo Bar Baz")

got := v.Get("foo")
want := map[string]any{"bar": "Foo Bar Baz"}
assert.Equal(t, want, got)

v.SetDefault("foozbar", "Foo Bar Baz")
got = v.Get("foo")
assert.Equal(t, want, got)
})

// Test the flattenAndMergeMap private method with key delimiter for case insenitivty
t.Run("UpperCasedDelimFlattenAndMerge", func(t *testing.T) {
config := map[string]any{
"foo": map[string]any{
"bar": 123,
"baz": 456,
},
}

v := NewWithOptions(KeyDelimiter("Z"))
shadow := make(map[string]bool)
shadow = v.flattenAndMergeMap(shadow, config, "")

assert.True(t, shadow["fooZbar"])
assert.True(t, shadow["foozbaz"])
})

// Test the AllSettings method with key delimiter for case insenitivty
t.Run("UpperCasedDelimAllSettings", func(t *testing.T) {
v := NewWithOptions(KeyDelimiter("Z"))

v.Set("foozbar", 123)

got := v.AllSettings()

want := map[string]any{
"foo": map[string]any{
"bar": 123,
},
}

assert.Equal(t, want, got)
})
}

var yamlDeepNestedSlices = []byte(`TV:
Expand Down