From 5c55b403d02df83ad57b307f96b30067d67ad5bd Mon Sep 17 00:00:00 2001 From: Kuisong Tong Date: Thu, 2 May 2024 14:32:00 -0700 Subject: [PATCH] preserve the case for key of map (#318) currently if the target has map, all keys in map are lower case by default due to case-insensitive. This PR fixes it by preserving the original case for keys in map --- CHANGELOG.md | 2 ++ config.go | 11 +++-------- config_test.go | 9 +++++++++ internal/convert/converter.go | 19 +++++++++++++------ internal/convert/converter_test.go | 2 +- internal/maps/sub.go | 5 +++-- internal/maps/sub_test.go | 6 ++++++ internal/maps/transform.go | 2 +- internal/maps/transform_test.go | 2 +- internal/maps/value.go | 21 +++++++++++++++++++++ watch.go | 3 ++- 11 files changed, 62 insertions(+), 20 deletions(-) create mode 100644 internal/maps/value.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 15a7772e..b51a7888 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixed - Explain supports empty string as path (#314). +- Reserve the case for key of map when unmarshalling. All keys in map used to be lower case, + now it matches the case in the configuration (#318). ## [1.1.0] - 2024-04-24 diff --git a/config.go b/config.go index f850c347..2cc0ec6a 100644 --- a/config.go +++ b/config.go @@ -14,7 +14,6 @@ import ( "sync" "sync/atomic" "time" - "unicode" "github.com/nil-go/konf/internal" "github.com/nil-go/konf/internal/convert" @@ -147,7 +146,7 @@ func (c *Config) log(ctx context.Context, level slog.Level, message string, attr func (c *Config) sub(values map[string]any, path string) any { if !c.caseSensitive { - path = toLower(path) + path = strings.ToLower(path) } return maps.Sub(values, path, c.delim()) @@ -163,14 +162,10 @@ func (c *Config) delim() string { func (c *Config) transformKeys(m map[string]any) { if !c.caseSensitive { - maps.TransformKeys(m, toLower) + maps.TransformKeys(m, strings.ToLower) } } -func toLower(s string) string { - return strings.Map(unicode.ToLower, s) -} - // Explain provides information about how Config resolve each value // from loaders for the given path. It blur sensitive information. // The path is case-insensitive unless konf.WithCaseSensitive is set. @@ -246,7 +241,7 @@ type provider struct { //nolint:gochecknoglobals var ( defaultTagName = convert.WithTagName("konf") - defaultKeyMap = convert.WithKeyMapper(toLower) + defaultKeyMap = convert.WithKeyMapper(strings.ToLower) defaultHooks = []convert.Option{ convert.WithHook[string, time.Duration](time.ParseDuration), convert.WithHook[string, []string](func(f string) ([]string, error) { diff --git a/config_test.go b/config_test.go index ad6a4b34..39592209 100644 --- a/config_test.go +++ b/config_test.go @@ -83,6 +83,15 @@ func TestConfig_Unmarshal(t *testing.T) { assert.Equal(t, "string", value) }, }, + { + description: "config for map", + loaders: []konf.Loader{mapLoader{"Config": "struct"}}, + assert: func(config *konf.Config) { + var value map[string]string + assert.NoError(t, config.Unmarshal("", &value)) + assert.Equal(t, "struct", value["Config"]) + }, + }, { description: "config for struct", loaders: []konf.Loader{mapLoader{"config": "struct"}}, diff --git a/internal/convert/converter.go b/internal/convert/converter.go index b76bd0cf..1c0da7a1 100644 --- a/internal/convert/converter.go +++ b/internal/convert/converter.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/nil-go/konf/internal" + "github.com/nil-go/konf/internal/maps" ) type Converter struct { @@ -348,16 +349,21 @@ func (c Converter) convertMap(name string, fromVal, toVal reflect.Value) error { errs := make([]error, 0, toVal.Len()) for _, fromKeyVal := range fromVal.MapKeys() { fieldName := name + "[" + fromKeyVal.String() + "]" - toKeyVal := reflect.New(toKeyType) - if err := c.convert(fieldName, fromKeyVal.Interface(), pointer(toKeyVal)); err != nil { + + fromValueVal := fromVal.MapIndex(fromKeyVal) + toValueVal := reflect.New(toValueType) + key, value := maps.Unpack(fromValueVal.Interface()) + if err := c.convert(fieldName, value, pointer(toValueVal)); err != nil { errs = append(errs, err) continue } - fromValueVal := fromVal.MapIndex(fromKeyVal) - toValueVal := reflect.New(toValueType) - if err := c.convert(fieldName, fromValueVal.Interface(), pointer(toValueVal)); err != nil { + if key == "" { + key = fromKeyVal.String() + } + toKeyVal := reflect.New(toKeyType) + if err := c.convert(fieldName, key, pointer(toKeyVal)); err != nil { errs = append(errs, err) continue @@ -531,7 +537,8 @@ func (c Converter) convertStruct(name string, fromVal, toVal reflect.Value) erro if name != "" { fieldName = name + "." + fieldName } - if err := c.convert(fieldName, elemVal.Interface(), pointer(fieldVal)); err != nil { + _, value := maps.Unpack(elemVal.Interface()) + if err := c.convert(fieldName, value, pointer(fieldVal)); err != nil { errs = append(errs, err) } } diff --git a/internal/convert/converter_test.go b/internal/convert/converter_test.go index 640cf29a..2a5e570c 100644 --- a/internal/convert/converter_test.go +++ b/internal/convert/converter_test.go @@ -650,7 +650,7 @@ func TestConverter(t *testing.T) { //nolint:maintidx }, { description: "map to map (key convert error)", - from: map[string]int{"-2": -42}, + from: map[string]int{"-2": 42}, to: pointer(map[uint]uint(nil)), err: "cannot parse '[-2]' as uint: strconv.ParseUint: parsing \"-2\": invalid syntax", }, diff --git a/internal/maps/sub.go b/internal/maps/sub.go index 20607e24..808a5a4e 100644 --- a/internal/maps/sub.go +++ b/internal/maps/sub.go @@ -11,11 +11,12 @@ func Sub(values map[string]any, path string, delimiter string) any { } key, path, _ := strings.Cut(path, delimiter) + _, value := Unpack(values[key]) if path == "" { - return values[key] + return value } - if mp, ok := values[key].(map[string]any); ok { + if mp, ok := value.(map[string]any); ok { return Sub(mp, path, delimiter) } diff --git a/internal/maps/sub_test.go b/internal/maps/sub_test.go index 5b9aeccc..1e13a855 100644 --- a/internal/maps/sub_test.go +++ b/internal/maps/sub_test.go @@ -55,6 +55,12 @@ func TestSub(t *testing.T) { path: "A", expected: 1, }, + { + description: "keyvalue", + values: map[string]any{"a": maps.Pack("A", 1)}, + path: "a", + expected: 1, + }, { description: "value not exist", values: map[string]any{"a": 1}, diff --git a/internal/maps/transform.go b/internal/maps/transform.go index 7e6b33c4..711cc652 100644 --- a/internal/maps/transform.go +++ b/internal/maps/transform.go @@ -14,7 +14,7 @@ func TransformKeys(src map[string]interface{}, keyMap func(string) string) { newKey := keyMap(key) if newKey != key { delete(src, key) - src[newKey] = value + src[newKey] = Pack(key, value) } } } diff --git a/internal/maps/transform_test.go b/internal/maps/transform_test.go index ed78ce17..e3120f3a 100644 --- a/internal/maps/transform_test.go +++ b/internal/maps/transform_test.go @@ -33,7 +33,7 @@ func TestTransformKeys(t *testing.T) { description: "transform keys", src: map[string]any{"A": map[string]any{"X": 1, "y": 2}}, keyMap: strings.ToLower, - expected: map[string]any{"a": map[string]any{"x": 1, "y": 2}}, + expected: map[string]any{"a": maps.Pack("A", map[string]any{"x": maps.Pack("X", 1), "y": 2})}, }, } diff --git a/internal/maps/value.go b/internal/maps/value.go new file mode 100644 index 00000000..50751452 --- /dev/null +++ b/internal/maps/value.go @@ -0,0 +1,21 @@ +// Copyright (c) 2024 The konf authors +// Use of this source code is governed by a MIT license found in the LICENSE file. + +package maps + +type KeyValue struct { + Key string + Value any +} + +func Pack(key string, value any) KeyValue { + return KeyValue{Key: key, Value: value} +} + +func Unpack(value any) (string, any) { + if v, ok := value.(KeyValue); ok { + return v.Key, v.Value + } + + return "", value +} diff --git a/watch.go b/watch.go index f108ead5..ecee1602 100644 --- a/watch.go +++ b/watch.go @@ -10,6 +10,7 @@ import ( "log/slog" "reflect" "slices" + "strings" "sync" "time" @@ -175,7 +176,7 @@ func (c *Config) OnChange(onChange func(*Config), paths ...string) { } for _, path := range paths { if !c.caseSensitive { - path = toLower(path) + path = strings.ToLower(path) } c.onChanges[path] = append(c.onChanges[path], onChange) }