fix: config map cannot handle case-insensitive keys. (#2932)
* fix: #2922 * chore: rename const * feat: support anonymous map field * feat: support anonymous map field
This commit is contained in:
@@ -21,8 +21,8 @@ var loaders = map[string]func([]byte, interface{}) 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 interface{}, 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)
|
||||
}
|
||||
} else {
|
||||
info[lowerCaseName] = fieldInfo{
|
||||
name: name,
|
||||
info.mapField = fields.mapField
|
||||
case reflect.Map:
|
||||
elemField := buildFieldsInfo(mapping.Deref(ft.Elem()))
|
||||
info.children[lowerCaseName] = fieldInfo{
|
||||
mapField: &elemField,
|
||||
}
|
||||
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 interface{}, info map[string]fieldInfo) interface{} {
|
||||
func toLowerCaseInterface(v any, info fieldInfo) any {
|
||||
switch vv := v.(type) {
|
||||
case map[string]interface{}:
|
||||
return toLowerCaseKeyMap(vv, info)
|
||||
@@ -194,19 +200,21 @@ func toLowerCaseInterface(v interface{}, info map[string]fieldInfo) interface{}
|
||||
}
|
||||
}
|
||||
|
||||
func toLowerCaseKeyMap(m map[string]interface{}, info map[string]fieldInfo) map[string]interface{} {
|
||||
res := make(map[string]interface{})
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user