Repository: zeromicro/go-zero
Pull request #2807 (2023-01-20 15:09:42)

fix: mapping optional dep not canonicaled


Patch:
diff --git a/core/mapping/unmarshaler.go b/core/mapping/unmarshaler.go
index e4293714..fee1457a 100644
--- a/core/mapping/unmarshaler.go
+++ b/core/mapping/unmarshaler.go
@@ -372,6 +372,26 @@ func (u *Unmarshaler) parseOptionsWithContext(field reflect.StructField, m Value
 		return key, nil, nil
 	}
 
+	if u.opts.canonicalKey != nil {
+		key = u.opts.canonicalKey(key)
+
+		if len(options.OptionalDep) > 0 {
+			// need to create a new fieldOption, because the original one is shared through cache.
+			options = &fieldOptions{
+				fieldOptionsWithContext: fieldOptionsWithContext{
+					Inherit:    options.Inherit,
+					FromString: options.FromString,
+					Optional:   options.Optional,
+					Options:    options.Options,
+					Default:    options.Default,
+					EnvVar:     options.EnvVar,
+					Range:      options.Range,
+				},
+				OptionalDep: u.opts.canonicalKey(options.OptionalDep),
+			}
+		}
+	}
+
 	optsWithContext, err := options.toOptionsWithContext(key, m, fullName)
 	if err != nil {
 		return "", nil, err


Test patch:
diff --git a/core/conf/config_test.go b/core/conf/config_test.go
index 82dc5545..7be4f540 100644
--- a/core/conf/config_test.go
+++ b/core/conf/config_test.go
@@ -97,6 +97,30 @@ d = "abcd!@#$112"
 	assert.Equal(t, "abcd!@#$112", val.D)
 }
 
+func TestConfigOptional(t *testing.T) {
+	text := `a = "foo"
+b = 1
+c = "FOO"
+d = "abcd"
+`
+	tmpfile, err := createTempFile(".toml", text)
+	assert.Nil(t, err)
+	defer os.Remove(tmpfile)
+
+	var val struct {
+		A string `json:"a"`
+		B int    `json:"b,optional"`
+		C string `json:"c,optional=B"`
+		D string `json:"d,optional=b"`
+	}
+	if assert.NoError(t, Load(tmpfile, &val)) {
+		assert.Equal(t, "foo", val.A)
+		assert.Equal(t, 1, val.B)
+		assert.Equal(t, "FOO", val.C)
+		assert.Equal(t, "abcd", val.D)
+	}
+}
+
 func TestConfigJsonCanonical(t *testing.T) {
 	text := []byte(`{"a": "foo", "B": "bar"}`)
 
diff --git a/core/mapping/jsonunmarshaler_test.go b/core/mapping/jsonunmarshaler_test.go
index 05af99c9..832ed24e 100644
--- a/core/mapping/jsonunmarshaler_test.go
+++ b/core/mapping/jsonunmarshaler_test.go
@@ -930,3 +930,13 @@ func TestUnmarshalJsonArray(t *testing.T) {
 	assert.Equal(t, "kevin", v[0].Name)
 	assert.Equal(t, 18, v[0].Age)
 }
+
+func TestUnmarshalJsonBytesError(t *testing.T) {
+	var v []struct {
+		Name string `json:"name"`
+		Age  int    `json:"age"`
+	}
+
+	assert.Error(t, UnmarshalJsonBytes([]byte((``)), &v))
+	assert.Error(t, UnmarshalJsonReader(strings.NewReader(``), &v))
+}
diff --git a/core/mapping/unmarshaler_test.go b/core/mapping/unmarshaler_test.go
index fc4d2cd4..ca57cac5 100644
--- a/core/mapping/unmarshaler_test.go
+++ b/core/mapping/unmarshaler_test.go
@@ -77,6 +77,26 @@ func TestUnmarshalWithoutTagNameWithCanonicalKey(t *testing.T) {
 	}
 }
 
+func TestUnmarshalWithoutTagNameWithCanonicalKeyOptionalDep(t *testing.T) {
+	type inner struct {
+		FirstName string `key:",optional"`
+		LastName  string `key:",optional=FirstName"`
+	}
+	m := map[string]interface{}{
+		"firstname": "go",
+		"lastname":  "zero",
+	}
+
+	var in inner
+	unmarshaler := NewUnmarshaler(defaultKeyName, WithCanonicalKeyFunc(func(s string) string {
+		return strings.ToLower(s)
+	}))
+	if assert.NoError(t, unmarshaler.Unmarshal(m, &in)) {
+		assert.Equal(t, "go", in.FirstName)
+		assert.Equal(t, "zero", in.LastName)
+	}
+}
+
 func TestUnmarshalBool(t *testing.T) {
 	type inner struct {
 		True           bool `key:"yes"`
@@ -1099,6 +1119,66 @@ func TestUnmarshalStructOptionalDependsNotEnoughValue(t *testing.T) {
 	assert.Error(t, UnmarshalKey(m, &in))
 }
 
+func TestUnmarshalStructOptionalDependsMoreValues(t *testing.T) {
+	type address struct {
+		Optional        string `key:",optional"`
+		OptionalDepends string `key:",optional=a=b"`
+	}
+	type inner struct {
+		Name    string  `key:"name"`
+		Address address `key:"address"`
+	}
+
+	m := map[string]interface{}{
+		"name":    "kevin",
+		"address": map[string]interface{}{},
+	}
+
+	var in inner
+	assert.Error(t, UnmarshalKey(m, &in))
+}
+
+func TestUnmarshalStructMissing(t *testing.T) {
+	type address struct {
+		Optional        string `key:",optional"`
+		OptionalDepends string `key:",optional=a=b"`
+	}
+	type inner struct {
+		Name    string  `key:"name"`
+		Address address `key:"address"`
+	}
+
+	m := map[string]interface{}{
+		"name": "kevin",
+	}
+
+	var in inner
+	assert.Error(t, UnmarshalKey(m, &in))
+}
+
+func TestUnmarshalNestedStructMissing(t *testing.T) {
+	type mostInner struct {
+		Name string `key:"name"`
+	}
+	type address struct {
+		Optional        string `key:",optional"`
+		OptionalDepends string `key:",optional=a=b"`
+		MostInner       mostInner
+	}
+	type inner struct {
+		Name    string  `key:"name"`
+		Address address `key:"address"`
+	}
+
+	m := map[string]interface{}{
+		"name":    "kevin",
+		"address": map[string]interface{}{},
+	}
+
+	var in inner
+	assert.Error(t, UnmarshalKey(m, &in))
+}
+
 func TestUnmarshalAnonymousStructOptionalDepends(t *testing.T) {
 	type AnonAddress struct {
 		City            string `key:"city"`
@@ -1422,6 +1502,18 @@ func TestUnmarshalOptionsOptionalWrongValue(t *testing.T) {
 	assert.Error(t, UnmarshalKey(m, &in))
 }
 
+func TestUnmarshalOptionsMissingValues(t *testing.T) {
+	type inner struct {
+		Value string `key:"value,options"`
+	}
+	m := map[string]interface{}{
+		"value": "first",
+	}
+
+	var in inner
+	assert.Error(t, UnmarshalKey(m, &in))
+}
+
 func TestUnmarshalStringOptionsWithStringOptionsNotString(t *testing.T) {
 	type inner struct {
 		Value   string `key:"value,options=first|second"`