Skip to content

Commit

Permalink
preserve the case for key of map (#318)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ktong authored May 2, 2024
1 parent 5859f23 commit 5c55b40
Show file tree
Hide file tree
Showing 11 changed files with 62 additions and 20 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
11 changes: 3 additions & 8 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"sync"
"sync/atomic"
"time"
"unicode"

"github.com/nil-go/konf/internal"
"github.com/nil-go/konf/internal/convert"
Expand Down Expand Up @@ -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())
Expand All @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down
9 changes: 9 additions & 0 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"}},
Expand Down
19 changes: 13 additions & 6 deletions internal/convert/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"strings"

"github.com/nil-go/konf/internal"
"github.com/nil-go/konf/internal/maps"
)

type Converter struct {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/convert/converter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
Expand Down
5 changes: 3 additions & 2 deletions internal/maps/sub.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
6 changes: 6 additions & 0 deletions internal/maps/sub_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
2 changes: 1 addition & 1 deletion internal/maps/transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
2 changes: 1 addition & 1 deletion internal/maps/transform_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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})},
},
}

Expand Down
21 changes: 21 additions & 0 deletions internal/maps/value.go
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 2 additions & 1 deletion watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"log/slog"
"reflect"
"slices"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -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)
}
Expand Down

0 comments on commit 5c55b40

Please sign in to comment.