From de4924a2746cf50934a906e136475a4b80bcccd0 Mon Sep 17 00:00:00 2001 From: Kevin Wan Date: Tue, 28 Feb 2023 23:32:08 +0800 Subject: [PATCH] fix: config map cannot handle case-insensitive keys. (#2932) * fix: #2922 * chore: rename const * feat: support anonymous map field * feat: support anonymous map field --- core/conf/config.go | 68 ++++++++++++++++++++++------------------ core/conf/config_test.go | 64 +++++++++++++++++++++++++++++++++++-- 2 files changed, 99 insertions(+), 33 deletions(-) diff --git a/core/conf/config.go b/core/conf/config.go index 729accdb..002a4d91 100644 --- a/core/conf/config.go +++ b/core/conf/config.go @@ -21,8 +21,8 @@ var loaders = map[string]func([]byte, any) error{ } type fieldInfo struct { - name string children map[string]fieldInfo + mapField *fieldInfo } // Load loads config into v from file, .json, .yaml and .yml are acceptable. @@ -107,21 +107,19 @@ func MustLoad(path string, v any, opts ...Option) { } } -func addOrMergeFields(info map[string]fieldInfo, key, name string, fields map[string]fieldInfo) { - if prev, ok := info[key]; ok { +func addOrMergeFields(info fieldInfo, key string, child fieldInfo) { + if prev, ok := info.children[key]; ok { // merge fields - for k, v := range fields { + for k, v := range child.children { prev.children[k] = v } + prev.mapField = child.mapField } else { - info[key] = fieldInfo{ - name: name, - children: fields, - } + info.children[key] = child } } -func buildFieldsInfo(tp reflect.Type) map[string]fieldInfo { +func buildFieldsInfo(tp reflect.Type) fieldInfo { tp = mapping.Deref(tp) switch tp.Kind() { @@ -130,46 +128,54 @@ func buildFieldsInfo(tp reflect.Type) map[string]fieldInfo { case reflect.Array, reflect.Slice: return buildFieldsInfo(mapping.Deref(tp.Elem())) default: - return nil + return fieldInfo{} } } -func buildStructFieldsInfo(tp reflect.Type) map[string]fieldInfo { - info := make(map[string]fieldInfo) +func buildStructFieldsInfo(tp reflect.Type) fieldInfo { + info := fieldInfo{ + children: make(map[string]fieldInfo), + } for i := 0; i < tp.NumField(); i++ { field := tp.Field(i) name := field.Name lowerCaseName := toLowerCase(name) ft := mapping.Deref(field.Type) - // flatten anonymous fields if field.Anonymous { - if ft.Kind() == reflect.Struct { + switch ft.Kind() { + case reflect.Struct: fields := buildFieldsInfo(ft) - for k, v := range fields { - addOrMergeFields(info, k, v.name, v.children) + for k, v := range fields.children { + addOrMergeFields(info, k, v) } - } else { - info[lowerCaseName] = fieldInfo{ - name: name, + info.mapField = fields.mapField + case reflect.Map: + elemField := buildFieldsInfo(mapping.Deref(ft.Elem())) + info.children[lowerCaseName] = fieldInfo{ + mapField: &elemField, + } + default: + info.children[lowerCaseName] = fieldInfo{ children: make(map[string]fieldInfo), } } continue } - var fields map[string]fieldInfo + var finfo fieldInfo switch ft.Kind() { case reflect.Struct: - fields = buildFieldsInfo(ft) + finfo = buildFieldsInfo(ft) case reflect.Array, reflect.Slice: - fields = buildFieldsInfo(ft.Elem()) + finfo = buildFieldsInfo(ft.Elem()) case reflect.Map: - fields = buildFieldsInfo(ft.Elem()) + elemInfo := buildFieldsInfo(mapping.Deref(ft.Elem())) + finfo.mapField = &elemInfo } - addOrMergeFields(info, lowerCaseName, name, fields) + addOrMergeFields(info, lowerCaseName, finfo) } return info @@ -179,7 +185,7 @@ func toLowerCase(s string) string { return strings.ToLower(s) } -func toLowerCaseInterface(v any, info map[string]fieldInfo) any { +func toLowerCaseInterface(v any, info fieldInfo) any { switch vv := v.(type) { case map[string]any: return toLowerCaseKeyMap(vv, info) @@ -194,19 +200,21 @@ func toLowerCaseInterface(v any, info map[string]fieldInfo) any { } } -func toLowerCaseKeyMap(m map[string]any, info map[string]fieldInfo) map[string]any { +func toLowerCaseKeyMap(m map[string]any, info fieldInfo) map[string]any { res := make(map[string]any) for k, v := range m { - ti, ok := info[k] + ti, ok := info.children[k] if ok { - res[k] = toLowerCaseInterface(v, ti.children) + res[k] = toLowerCaseInterface(v, ti) continue } lk := toLowerCase(k) - if ti, ok = info[lk]; ok { - res[lk] = toLowerCaseInterface(v, ti.children) + if ti, ok = info.children[lk]; ok { + res[lk] = toLowerCaseInterface(v, ti) + } else if info.mapField != nil { + res[k] = toLowerCaseInterface(v, *info.mapField) } else { res[k] = v } diff --git a/core/conf/config_test.go b/core/conf/config_test.go index c78c25de..e92152e7 100644 --- a/core/conf/config_test.go +++ b/core/conf/config_test.go @@ -17,7 +17,7 @@ func TestLoadConfig_notRecogFile(t *testing.T) { filename, err := fs.TempFilenameWithText("hello") assert.Nil(t, err) defer os.Remove(filename) - assert.NotNil(t, Load(filename, nil)) + assert.NotNil(t, LoadConfig(filename, nil)) } func TestConfigJson(t *testing.T) { @@ -64,7 +64,7 @@ func TestLoadFromJsonBytesArray(t *testing.T) { } } - assert.NoError(t, LoadFromJsonBytes(input, &val)) + assert.NoError(t, LoadConfigFromJsonBytes(input, &val)) var expect []string for _, user := range val.Users { expect = append(expect, user.Name) @@ -172,7 +172,7 @@ B: bar`) A string B string } - assert.NoError(t, LoadFromYamlBytes(text, &val1)) + assert.NoError(t, LoadConfigFromYamlBytes(text, &val1)) assert.Equal(t, "foo", val1.A) assert.Equal(t, "bar", val1.B) assert.NoError(t, LoadFromYamlBytes(text, &val2)) @@ -558,6 +558,64 @@ func TestUnmarshalJsonBytesWithAnonymousField(t *testing.T) { assert.Equal(t, Int(3), c.Int) } +func TestUnmarshalJsonBytesWithMapValueOfStruct(t *testing.T) { + type ( + Value struct { + Name string + } + + Config struct { + Items map[string]Value + } + ) + + var inputs = [][]byte{ + []byte(`{"Items": {"Key":{"Name": "foo"}}}`), + []byte(`{"Items": {"Key":{"Name": "foo"}}}`), + []byte(`{"items": {"key":{"name": "foo"}}}`), + []byte(`{"items": {"key":{"name": "foo"}}}`), + } + for _, input := range inputs { + var c Config + if assert.NoError(t, LoadFromJsonBytes(input, &c)) { + assert.Equal(t, 1, len(c.Items)) + for _, v := range c.Items { + assert.Equal(t, "foo", v.Name) + } + } + } +} + +func TestUnmarshalJsonBytesWithMapTypeValueOfStruct(t *testing.T) { + type ( + Value struct { + Name string + } + + Map map[string]Value + + Config struct { + Map + } + ) + + var inputs = [][]byte{ + []byte(`{"Map": {"Key":{"Name": "foo"}}}`), + []byte(`{"Map": {"Key":{"Name": "foo"}}}`), + []byte(`{"map": {"key":{"name": "foo"}}}`), + []byte(`{"map": {"key":{"name": "foo"}}}`), + } + for _, input := range inputs { + var c Config + if assert.NoError(t, LoadFromJsonBytes(input, &c)) { + assert.Equal(t, 1, len(c.Map)) + for _, v := range c.Map { + assert.Equal(t, "foo", v.Name) + } + } + } +} + func createTempFile(ext, text string) (string, error) { tmpfile, err := os.CreateTemp(os.TempDir(), hash.Md5Hex([]byte(text))+"*"+ext) if err != nil {