fix: camel cased key of map item in config (#2715)

* fix: camel cased key of map item in config

* fix: mapping anonymous problem

* fix: mapping anonymous problem

* chore: refactor

* chore: add more tests

* chore: refactor
This commit is contained in:
Kevin Wan
2022-12-24 21:26:33 +08:00
committed by GitHub
parent f0d1722bbd
commit affbcb5698
5 changed files with 382 additions and 31 deletions

View File

@@ -5,6 +5,7 @@ import (
"log"
"os"
"path"
"reflect"
"strings"
"github.com/zeromicro/go-zero/core/jsonx"
@@ -21,6 +22,12 @@ var loaders = map[string]func([]byte, interface{}) error{
".yml": LoadFromYamlBytes,
}
type fieldInfo struct {
name string
kind reflect.Kind
children map[string]fieldInfo
}
// Load loads config into v from file, .json, .yaml and .yml are acceptable.
func Load(file string, v interface{}, opts ...Option) error {
content, err := os.ReadFile(file)
@@ -58,7 +65,10 @@ func LoadFromJsonBytes(content []byte, v interface{}) error {
return err
}
return mapping.UnmarshalJsonMap(toCamelCaseKeyMap(m), v, mapping.WithCanonicalKeyFunc(toCamelCase))
finfo := buildFieldsInfo(reflect.TypeOf(v))
camelCaseKeyMap := toCamelCaseKeyMap(m, finfo)
return mapping.UnmarshalJsonMap(camelCaseKeyMap, v, mapping.WithCanonicalKeyFunc(toCamelCase))
}
// LoadConfigFromJsonBytes loads config into v from content json bytes.
@@ -100,6 +110,64 @@ func MustLoad(path string, v interface{}, opts ...Option) {
}
}
func buildFieldsInfo(tp reflect.Type) map[string]fieldInfo {
tp = mapping.Deref(tp)
switch tp.Kind() {
case reflect.Struct:
return buildStructFieldsInfo(tp)
case reflect.Array, reflect.Slice:
return buildFieldsInfo(mapping.Deref(tp.Elem()))
default:
return nil
}
}
func buildStructFieldsInfo(tp reflect.Type) map[string]fieldInfo {
info := make(map[string]fieldInfo)
for i := 0; i < tp.NumField(); i++ {
field := tp.Field(i)
name := field.Name
ccName := toCamelCase(name)
ft := mapping.Deref(field.Type)
// flatten anonymous fields
if field.Anonymous {
if ft.Kind() == reflect.Struct {
fields := buildFieldsInfo(ft)
for k, v := range fields {
info[k] = v
}
} else {
info[ccName] = fieldInfo{
name: name,
kind: ft.Kind(),
}
}
continue
}
var fields map[string]fieldInfo
switch ft.Kind() {
case reflect.Struct:
fields = buildFieldsInfo(ft)
case reflect.Array, reflect.Slice:
fields = buildFieldsInfo(ft.Elem())
case reflect.Map:
fields = buildFieldsInfo(ft.Elem())
}
info[ccName] = fieldInfo{
name: name,
kind: ft.Kind(),
children: fields,
}
}
return info
}
func toCamelCase(s string) string {
var buf strings.Builder
buf.Grow(len(s))
@@ -123,14 +191,19 @@ func toCamelCase(s string) string {
if isCap || isLow {
buf.WriteRune(v)
capNext = false
} else if v == ' ' || v == '\t' {
continue
}
switch v {
// '.' is used for chained keys, e.g. "grand.parent.child"
case ' ', '.', '\t':
buf.WriteRune(v)
capNext = false
boundary = true
} else if v == '_' {
case '_':
capNext = true
boundary = true
} else {
default:
buf.WriteRune(v)
capNext = true
}
@@ -139,14 +212,14 @@ func toCamelCase(s string) string {
return buf.String()
}
func toCamelCaseInterface(v interface{}) interface{} {
func toCamelCaseInterface(v interface{}, info map[string]fieldInfo) interface{} {
switch vv := v.(type) {
case map[string]interface{}:
return toCamelCaseKeyMap(vv)
return toCamelCaseKeyMap(vv, info)
case []interface{}:
var arr []interface{}
for _, vvv := range vv {
arr = append(arr, toCamelCaseInterface(vvv))
arr = append(arr, toCamelCaseInterface(vvv, info))
}
return arr
default:
@@ -154,10 +227,22 @@ func toCamelCaseInterface(v interface{}) interface{} {
}
}
func toCamelCaseKeyMap(m map[string]interface{}) map[string]interface{} {
func toCamelCaseKeyMap(m map[string]interface{}, info map[string]fieldInfo) map[string]interface{} {
res := make(map[string]interface{})
for k, v := range m {
res[toCamelCase(k)] = toCamelCaseInterface(v)
ti, ok := info[k]
if ok {
res[k] = toCamelCaseInterface(v, ti.children)
continue
}
cck := toCamelCase(k)
if ti, ok = info[cck]; ok {
res[toCamelCase(k)] = toCamelCaseInterface(v, ti.children)
} else {
res[k] = v
}
}
return res