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:
@@ -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
|
||||
|
||||
@@ -283,6 +283,10 @@ func TestToCamelCase(t *testing.T) {
|
||||
input: "Hello World Foo_Bar",
|
||||
expect: "hello world fooBar",
|
||||
},
|
||||
{
|
||||
input: "Hello.World Foo_Bar",
|
||||
expect: "hello.world fooBar",
|
||||
},
|
||||
{
|
||||
input: "你好 World Foo_Bar",
|
||||
expect: "你好 world fooBar",
|
||||
@@ -328,6 +332,84 @@ func TestLoadFromYamlBytes(t *testing.T) {
|
||||
assert.Equal(t, "foo", val.Layer1.Layer2.Layer3)
|
||||
}
|
||||
|
||||
func TestLoadFromYamlBytesLayers(t *testing.T) {
|
||||
input := []byte(`layer1:
|
||||
layer2:
|
||||
layer3: foo`)
|
||||
var val struct {
|
||||
Value string `json:"Layer1.Layer2.Layer3"`
|
||||
}
|
||||
|
||||
assert.NoError(t, LoadFromYamlBytes(input, &val))
|
||||
assert.Equal(t, "foo", val.Value)
|
||||
}
|
||||
|
||||
func TestUnmarshalJsonBytesMap(t *testing.T) {
|
||||
input := []byte(`{"foo":{"/mtproto.RPCTos": "bff.bff","bar":"baz"}}`)
|
||||
|
||||
var val struct {
|
||||
Foo map[string]string
|
||||
}
|
||||
|
||||
assert.NoError(t, LoadFromJsonBytes(input, &val))
|
||||
assert.Equal(t, "bff.bff", val.Foo["/mtproto.RPCTos"])
|
||||
assert.Equal(t, "baz", val.Foo["bar"])
|
||||
}
|
||||
|
||||
func TestUnmarshalJsonBytesMapWithSliceElements(t *testing.T) {
|
||||
input := []byte(`{"foo":{"/mtproto.RPCTos": ["bff.bff", "any"],"bar":["baz", "qux"]}}`)
|
||||
|
||||
var val struct {
|
||||
Foo map[string][]string
|
||||
}
|
||||
|
||||
assert.NoError(t, LoadFromJsonBytes(input, &val))
|
||||
assert.EqualValues(t, []string{"bff.bff", "any"}, val.Foo["/mtproto.RPCTos"])
|
||||
assert.EqualValues(t, []string{"baz", "qux"}, val.Foo["bar"])
|
||||
}
|
||||
|
||||
func TestUnmarshalJsonBytesMapWithSliceOfStructs(t *testing.T) {
|
||||
input := []byte(`{"foo":{
|
||||
"/mtproto.RPCTos": [{"bar": "any"}],
|
||||
"bar":[{"bar": "qux"}, {"bar": "ever"}]}}`)
|
||||
|
||||
var val struct {
|
||||
Foo map[string][]struct {
|
||||
Bar string
|
||||
}
|
||||
}
|
||||
|
||||
assert.NoError(t, LoadFromJsonBytes(input, &val))
|
||||
assert.Equal(t, 1, len(val.Foo["/mtproto.RPCTos"]))
|
||||
assert.Equal(t, "any", val.Foo["/mtproto.RPCTos"][0].Bar)
|
||||
assert.Equal(t, 2, len(val.Foo["bar"]))
|
||||
assert.Equal(t, "qux", val.Foo["bar"][0].Bar)
|
||||
assert.Equal(t, "ever", val.Foo["bar"][1].Bar)
|
||||
}
|
||||
|
||||
func TestUnmarshalJsonBytesWithAnonymousField(t *testing.T) {
|
||||
type (
|
||||
Int int
|
||||
|
||||
InnerConf struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
Conf struct {
|
||||
Int
|
||||
InnerConf
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
input = []byte(`{"Name": "hello", "int": 3}`)
|
||||
c Conf
|
||||
)
|
||||
assert.NoError(t, LoadFromJsonBytes(input, &c))
|
||||
assert.Equal(t, "hello", c.Name)
|
||||
assert.Equal(t, Int(3), c.Int)
|
||||
}
|
||||
|
||||
func createTempFile(ext, text string) (string, error) {
|
||||
tmpfile, err := os.CreateTemp(os.TempDir(), hash.Md5Hex([]byte(text))+"*"+ext)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user