diff --git a/README.md b/README.md index f48ebb790..f77bc67ac 100644 --- a/README.md +++ b/README.md @@ -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("::")) diff --git a/viper.go b/viper.go index cbea96282..bb738bf98 100644 --- a/viper.go +++ b/viper.go @@ -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) }) } @@ -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: @@ -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]) diff --git a/viper_test.go b/viper_test.go index 8b0232aee..d7592bd3c 100644 --- a/viper_test.go +++ b/viper_test.go @@ -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::steve@hacker.com::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::steve@hacker.com::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: