Przeglądaj źródła

fix: config map cannot handle case-insensitive keys. (#2932)

* fix: #2922

* chore: rename const

* feat: support anonymous map field

* feat: support anonymous map field
Kevin Wan 2 lat temu
rodzic
commit
de4924a274
2 zmienionych plików z 99 dodań i 33 usunięć
  1. 38 30
      core/conf/config.go
  2. 61 3
      core/conf/config_test.go

+ 38 - 30
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)
+				}
+				info.mapField = fields.mapField
+			case reflect.Map:
+				elemField := buildFieldsInfo(mapping.Deref(ft.Elem()))
+				info.children[lowerCaseName] = fieldInfo{
+					mapField: &elemField,
 				}
-			} else {
-				info[lowerCaseName] = fieldInfo{
-					name:     name,
+			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
 		}

+ 61 - 3
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 {