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 {
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
}

@ -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 {

Loading…
Cancel
Save