Repository: zeromicro/go-zero
Pull request #2974 (2023-03-06 12:27:40)

fix: config map with json tag

codecov[bot]: # [Codecov](https://codecov.io/gh/zeromicro/go-zero/pull/2974?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#2974](https://codecov.io/gh/zeromicro/go-zero/pull/2974?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (d9afa06) into [master](https://codecov.io/gh/zeromicro/go-zero/commit/7d5d7d908542b13eb2db0afe4f5e3a28aee2ebc0?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (7d5d7d9) will **increase** coverage by `0.01%`.
> The diff coverage is `100.00%`.


| [Impacted Files](https://codecov.io/gh/zeromicro/go-zero/pull/2974?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [core/conf/config.go](https://codecov.io/gh/zeromicro/go-zero/pull/2974?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-Y29yZS9jb25mL2NvbmZpZy5nbw==) | `98.60% <100.00%> (+0.07%)` | :arrow_up: |
| [core/mapping/utils.go](https://codecov.io/gh/zeromicro/go-zero/pull/2974?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-Y29yZS9tYXBwaW5nL3V0aWxzLmdv) | `92.61% <100.00%> (ø)` | |
| [core/stores/sqlx/sqlconn.go](https://codecov.io/gh/zeromicro/go-zero/pull/2974?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-Y29yZS9zdG9yZXMvc3FseC9zcWxjb25uLmdv) | `56.37% <0.00%> (-1.48%)` | :arrow_down: |
| [core/stores/sqlx/tx.go](https://codecov.io/gh/zeromicro/go-zero/pull/2974?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-Y29yZS9zdG9yZXMvc3FseC90eC5nbw==) | `23.30% <0.00%> (+3.88%)` | :arrow_up: |


Patch:
diff --git a/core/conf/config.go b/core/conf/config.go
index 5917ec11..7d2e76e6 100644
--- a/core/conf/config.go
+++ b/core/conf/config.go
@@ -13,7 +13,10 @@ import (
 	"github.com/zeromicro/go-zero/internal/encoding"
 )
 
-const jsonTagKey = "json"
+const (
+	jsonTagKey = "json"
+	jsonTagSep = ','
+)
 
 var (
 	fillDefaultUnmarshaler = mapping.NewUnmarshaler(jsonTagKey, mapping.WithDefault())
@@ -76,7 +79,7 @@ func LoadFromJsonBytes(content []byte, v any) error {
 	}
 
 	var m map[string]any
-	if err := jsonx.Unmarshal(content, &m); err != nil {
+	if err = jsonx.Unmarshal(content, &m); err != nil {
 		return err
 	}
 
@@ -239,7 +242,7 @@ func buildStructFieldsInfo(tp reflect.Type) (*fieldInfo, error) {
 
 	for i := 0; i < tp.NumField(); i++ {
 		field := tp.Field(i)
-		name := field.Name
+		name := getTagName(field)
 		lowerCaseName := toLowerCase(name)
 		ft := mapping.Deref(field.Type)
 		// flatten anonymous fields
@@ -255,6 +258,23 @@ func buildStructFieldsInfo(tp reflect.Type) (*fieldInfo, error) {
 	return info, nil
 }
 
+// getTagName get the tag name of the given field, if no tag name, use file.Name.
+// field.Name is returned on tags like `json:""` and `json:",optional"`.
+func getTagName(field reflect.StructField) string {
+	if tag, ok := field.Tag.Lookup(jsonTagKey); ok {
+		if pos := strings.IndexByte(tag, jsonTagSep); pos >= 0 {
+			tag = tag[:pos]
+		}
+
+		tag = strings.TrimSpace(tag)
+		if len(tag) > 0 {
+			return tag
+		}
+	}
+
+	return field.Name
+}
+
 func mergeFields(prev *fieldInfo, key string, children map[string]*fieldInfo) error {
 	if len(prev.children) == 0 || len(children) == 0 {
 		return newConflictKeyError(key)
diff --git a/core/mapping/utils.go b/core/mapping/utils.go
index 5ac706fe..88886228 100644
--- a/core/mapping/utils.go
+++ b/core/mapping/utils.go
@@ -255,7 +255,7 @@ func parseGroupedSegments(val string) []string {
 
 // don't modify returned fieldOptions, it's cached and shared among different calls.
 func parseKeyAndOptions(tagName string, field reflect.StructField) (string, *fieldOptions, error) {
-	value := field.Tag.Get(tagName)
+	value := strings.TrimSpace(field.Tag.Get(tagName))
 	if len(value) == 0 {
 		return field.Name, nil, nil
 	}


Test patch:
diff --git a/core/conf/config_test.go b/core/conf/config_test.go
index 64e9fd34..72392511 100644
--- a/core/conf/config_test.go
+++ b/core/conf/config_test.go
@@ -1022,24 +1022,6 @@ func TestLoadNamedFieldOverwritten(t *testing.T) {
 	})
 }
 
-func createTempFile(ext, text string) (string, error) {
-	tmpFile, err := os.CreateTemp(os.TempDir(), hash.Md5Hex([]byte(text))+"*"+ext)
-	if err != nil {
-		return "", err
-	}
-
-	if err := os.WriteFile(tmpFile.Name(), []byte(text), os.ModeTemporary); err != nil {
-		return "", err
-	}
-
-	filename := tmpFile.Name()
-	if err = tmpFile.Close(); err != nil {
-		return "", err
-	}
-
-	return filename, nil
-}
-
 func TestFillDefaultUnmarshal(t *testing.T) {
 	t.Run("nil", func(t *testing.T) {
 		type St struct{}
@@ -1079,7 +1061,7 @@ func TestFillDefaultUnmarshal(t *testing.T) {
 		assert.Equal(t, st.C, "c")
 	})
 
-	t.Run("has vaue", func(t *testing.T) {
+	t.Run("has value", func(t *testing.T) {
 		type St struct {
 			A string `json:",default=a"`
 			B string
@@ -1091,3 +1073,107 @@ func TestFillDefaultUnmarshal(t *testing.T) {
 		assert.Error(t, err)
 	})
 }
+
+func TestConfigWithJsonTag(t *testing.T) {
+	t.Run("map with value", func(t *testing.T) {
+		var input = []byte(`[Value]
+[Value.first]
+Email = "foo"
+[Value.second]
+Email = "bar"`)
+
+		type Value struct {
+			Email string
+		}
+
+		type Config struct {
+			ValueMap map[string]Value `json:"Value"`
+		}
+
+		var c Config
+		if assert.NoError(t, LoadFromTomlBytes(input, &c)) {
+			assert.Len(t, c.ValueMap, 2)
+		}
+	})
+
+	t.Run("map with ptr value", func(t *testing.T) {
+		var input = []byte(`[Value]
+[Value.first]
+Email = "foo"
+[Value.second]
+Email = "bar"`)
+
+		type Value struct {
+			Email string
+		}
+
+		type Config struct {
+			ValueMap map[string]*Value `json:"Value"`
+		}
+
+		var c Config
+		if assert.NoError(t, LoadFromTomlBytes(input, &c)) {
+			assert.Len(t, c.ValueMap, 2)
+		}
+	})
+
+	t.Run("map with optional", func(t *testing.T) {
+		var input = []byte(`[Value]
+[Value.first]
+Email = "foo"
+[Value.second]
+Email = "bar"`)
+
+		type Value struct {
+			Email string
+		}
+
+		type Config struct {
+			Value map[string]Value `json:",optional"`
+		}
+
+		var c Config
+		if assert.NoError(t, LoadFromTomlBytes(input, &c)) {
+			assert.Len(t, c.Value, 2)
+		}
+	})
+
+	t.Run("map with empty tag", func(t *testing.T) {
+		var input = []byte(`[Value]
+[Value.first]
+Email = "foo"
+[Value.second]
+Email = "bar"`)
+
+		type Value struct {
+			Email string
+		}
+
+		type Config struct {
+			Value map[string]Value `json:"  "`
+		}
+
+		var c Config
+		if assert.NoError(t, LoadFromTomlBytes(input, &c)) {
+			assert.Len(t, c.Value, 2)
+		}
+	})
+}
+
+func createTempFile(ext, text string) (string, error) {
+	tmpFile, err := os.CreateTemp(os.TempDir(), hash.Md5Hex([]byte(text))+"*"+ext)
+	if err != nil {
+		return "", err
+	}
+
+	if err := os.WriteFile(tmpFile.Name(), []byte(text), os.ModeTemporary); err != nil {
+		return "", err
+	}
+
+	filename := tmpFile.Name()
+	if err = tmpFile.Close(); err != nil {
+		return "", err
+	}
+
+	return filename, nil
+}
diff --git a/core/mapping/utils_test.go b/core/mapping/utils_test.go
index 854ffa46..84e0acf2 100644
--- a/core/mapping/utils_test.go
+++ b/core/mapping/utils_test.go
@@ -144,6 +144,10 @@ func TestParseSegments(t *testing.T) {
 			input:  "",
 			expect: []string{},
 		},
+		{
+			input:  "   ",
+			expect: []string{},
+		},
 		{
 			input:  ",",
 			expect: []string{""},