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

* fix: #2922

* chore: rename const

* feat: support anonymous map field

* feat: support anonymous map field
master
Kevin Wan 2 years ago committed by GitHub
parent 2b08e0510c
commit de4924a274
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -21,8 +21,8 @@ var loaders = map[string]func([]byte, any) error{
} }
type fieldInfo struct { type fieldInfo struct {
name string
children map[string]fieldInfo children map[string]fieldInfo
mapField *fieldInfo
} }
// Load loads config into v from file, .json, .yaml and .yml are acceptable. // 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) { func addOrMergeFields(info fieldInfo, key string, child fieldInfo) {
if prev, ok := info[key]; ok { if prev, ok := info.children[key]; ok {
// merge fields // merge fields
for k, v := range fields { for k, v := range child.children {
prev.children[k] = v prev.children[k] = v
} }
prev.mapField = child.mapField
} else { } else {
info[key] = fieldInfo{ info.children[key] = child
name: name,
children: fields,
}
} }
} }
func buildFieldsInfo(tp reflect.Type) map[string]fieldInfo { func buildFieldsInfo(tp reflect.Type) fieldInfo {
tp = mapping.Deref(tp) tp = mapping.Deref(tp)
switch tp.Kind() { switch tp.Kind() {
@ -130,46 +128,54 @@ func buildFieldsInfo(tp reflect.Type) map[string]fieldInfo {
case reflect.Array, reflect.Slice: case reflect.Array, reflect.Slice:
return buildFieldsInfo(mapping.Deref(tp.Elem())) return buildFieldsInfo(mapping.Deref(tp.Elem()))
default: default:
return nil return fieldInfo{}
} }
} }
func buildStructFieldsInfo(tp reflect.Type) map[string]fieldInfo { func buildStructFieldsInfo(tp reflect.Type) fieldInfo {
info := make(map[string]fieldInfo) info := fieldInfo{
children: make(map[string]fieldInfo),
}
for i := 0; i < tp.NumField(); i++ { for i := 0; i < tp.NumField(); i++ {
field := tp.Field(i) field := tp.Field(i)
name := field.Name name := field.Name
lowerCaseName := toLowerCase(name) lowerCaseName := toLowerCase(name)
ft := mapping.Deref(field.Type) ft := mapping.Deref(field.Type)
// flatten anonymous fields // flatten anonymous fields
if field.Anonymous { if field.Anonymous {
if ft.Kind() == reflect.Struct { switch ft.Kind() {
case reflect.Struct:
fields := buildFieldsInfo(ft) fields := buildFieldsInfo(ft)
for k, v := range fields { for k, v := range fields.children {
addOrMergeFields(info, k, v.name, v.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 { default:
info[lowerCaseName] = fieldInfo{ info.children[lowerCaseName] = fieldInfo{
name: name,
children: make(map[string]fieldInfo), children: make(map[string]fieldInfo),
} }
} }
continue continue
} }
var fields map[string]fieldInfo var finfo fieldInfo
switch ft.Kind() { switch ft.Kind() {
case reflect.Struct: case reflect.Struct:
fields = buildFieldsInfo(ft) finfo = buildFieldsInfo(ft)
case reflect.Array, reflect.Slice: case reflect.Array, reflect.Slice:
fields = buildFieldsInfo(ft.Elem()) finfo = buildFieldsInfo(ft.Elem())
case reflect.Map: 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 return info
@ -179,7 +185,7 @@ func toLowerCase(s string) string {
return strings.ToLower(s) return strings.ToLower(s)
} }
func toLowerCaseInterface(v any, info map[string]fieldInfo) any { func toLowerCaseInterface(v any, info fieldInfo) any {
switch vv := v.(type) { switch vv := v.(type) {
case map[string]any: case map[string]any:
return toLowerCaseKeyMap(vv, info) 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) res := make(map[string]any)
for k, v := range m { for k, v := range m {
ti, ok := info[k] ti, ok := info.children[k]
if ok { if ok {
res[k] = toLowerCaseInterface(v, ti.children) res[k] = toLowerCaseInterface(v, ti)
continue continue
} }
lk := toLowerCase(k) lk := toLowerCase(k)
if ti, ok = info[lk]; ok { if ti, ok = info.children[lk]; ok {
res[lk] = toLowerCaseInterface(v, ti.children) res[lk] = toLowerCaseInterface(v, ti)
} else if info.mapField != nil {
res[k] = toLowerCaseInterface(v, *info.mapField)
} else { } else {
res[k] = v res[k] = v
} }

@ -17,7 +17,7 @@ func TestLoadConfig_notRecogFile(t *testing.T) {
filename, err := fs.TempFilenameWithText("hello") filename, err := fs.TempFilenameWithText("hello")
assert.Nil(t, err) assert.Nil(t, err)
defer os.Remove(filename) defer os.Remove(filename)
assert.NotNil(t, Load(filename, nil)) assert.NotNil(t, LoadConfig(filename, nil))
} }
func TestConfigJson(t *testing.T) { 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 var expect []string
for _, user := range val.Users { for _, user := range val.Users {
expect = append(expect, user.Name) expect = append(expect, user.Name)
@ -172,7 +172,7 @@ B: bar`)
A string A string
B string B string
} }
assert.NoError(t, LoadFromYamlBytes(text, &val1)) assert.NoError(t, LoadConfigFromYamlBytes(text, &val1))
assert.Equal(t, "foo", val1.A) assert.Equal(t, "foo", val1.A)
assert.Equal(t, "bar", val1.B) assert.Equal(t, "bar", val1.B)
assert.NoError(t, LoadFromYamlBytes(text, &val2)) assert.NoError(t, LoadFromYamlBytes(text, &val2))
@ -558,6 +558,64 @@ func TestUnmarshalJsonBytesWithAnonymousField(t *testing.T) {
assert.Equal(t, Int(3), c.Int) 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) { func createTempFile(ext, text string) (string, error) {
tmpfile, err := os.CreateTemp(os.TempDir(), hash.Md5Hex([]byte(text))+"*"+ext) tmpfile, err := os.CreateTemp(os.TempDir(), hash.Md5Hex([]byte(text))+"*"+ext)
if err != nil { if err != nil {

Loading…
Cancel
Save