Repository: zeromicro/go-zero
Pull request #2951 (2023-03-02 09:32:26)

feat: conf add FillDefault func

codecov[bot]: # [Codecov](https://codecov.io/gh/zeromicro/go-zero/pull/2951?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#2951](https://codecov.io/gh/zeromicro/go-zero/pull/2951?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (da80062) into [master](https://codecov.io/gh/zeromicro/go-zero/commit/1cd1b17f70bc0fad39d28ca2b64b78cf811d8efc?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (1cd1b17) will **decrease** coverage by `0.04%`.
> The diff coverage is `30.00%`.


| [Impacted Files](https://codecov.io/gh/zeromicro/go-zero/pull/2951?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [core/mapping/unmarshaler.go](https://codecov.io/gh/zeromicro/go-zero/pull/2951?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-Y29yZS9tYXBwaW5nL3VubWFyc2hhbGVyLmdv) | `88.14% <12.50%> (-0.84%)` | :arrow_down: |
| [core/conf/config.go](https://codecov.io/gh/zeromicro/go-zero/pull/2951?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-Y29yZS9jb25mL2NvbmZpZy5nbw==) | `98.52% <100.00%> (+0.01%)` | :arrow_up: |


Patch:
diff --git a/core/conf/config.go b/core/conf/config.go
index ccc9e8ee..739aaf5d 100644
--- a/core/conf/config.go
+++ b/core/conf/config.go
@@ -13,12 +13,17 @@ import (
 	"github.com/zeromicro/go-zero/internal/encoding"
 )
 
-var loaders = map[string]func([]byte, any) error{
-	".json": LoadFromJsonBytes,
-	".toml": LoadFromTomlBytes,
-	".yaml": LoadFromYamlBytes,
-	".yml":  LoadFromYamlBytes,
-}
+const jsonTagKey = "json"
+
+var (
+	fillDefaultUnmarshaler = mapping.NewUnmarshaler(jsonTagKey, mapping.WithDefault())
+	loaders                = map[string]func([]byte, any) error{
+		".json": LoadFromJsonBytes,
+		".toml": LoadFromTomlBytes,
+		".yaml": LoadFromYamlBytes,
+		".yml":  LoadFromYamlBytes,
+	}
+)
 
 // children and mapField should not be both filled.
 // named fields and map cannot be bound to the same field name.
@@ -27,6 +32,12 @@ type fieldInfo struct {
 	mapField *fieldInfo
 }
 
+// FillDefault fills the default values for the given v,
+// and the premise is that the value of v must be guaranteed to be empty.
+func FillDefault(v any) error {
+	return fillDefaultUnmarshaler.Unmarshal(map[string]any{}, v)
+}
+
 // Load loads config into v from file, .json, .yaml and .yml are acceptable.
 func Load(file string, v any, opts ...Option) error {
 	content, err := os.ReadFile(file)
diff --git a/core/mapping/unmarshaler.go b/core/mapping/unmarshaler.go
index e4240cf2..f8674211 100644
--- a/core/mapping/unmarshaler.go
+++ b/core/mapping/unmarshaler.go
@@ -47,6 +47,7 @@ type (
 	UnmarshalOption func(*unmarshalOptions)
 
 	unmarshalOptions struct {
+		fillDefault  bool
 		fromString   bool
 		canonicalKey func(key string) string
 	}
@@ -710,7 +711,14 @@ func (u *Unmarshaler) processNamedField(field reflect.StructField, value reflect
 
 	valuer := createValuer(m, opts)
 	mapValue, hasValue := getValue(valuer, canonicalKey)
-	if !hasValue {
+
+	// When fillDefault is used, m is a null value, hasValue must be false, all priority judgments fillDefault.
+	if u.opts.fillDefault {
+		if !value.IsZero() {
+			return fmt.Errorf("set the default value, %s must be zero", fullName)
+		}
+		return u.processNamedFieldWithoutValue(field.Type, value, opts, fullName)
+	} else if !hasValue {
 		return u.processNamedFieldWithoutValue(field.Type, value, opts, fullName)
 	}
 
@@ -801,6 +809,10 @@ func (u *Unmarshaler) processNamedFieldWithoutValue(fieldType reflect.Type, valu
 		}
 	}
 
+	if u.opts.fillDefault {
+		return nil
+	}
+
 	switch fieldKind {
 	case reflect.Array, reflect.Map, reflect.Slice:
 		if !opts.optional() {
@@ -873,13 +885,20 @@ func WithStringValues() UnmarshalOption {
 	}
 }
 
-// WithCanonicalKeyFunc customizes an Unmarshaler with Canonical Key func
+// WithCanonicalKeyFunc customizes an Unmarshaler with Canonical Key func.
 func WithCanonicalKeyFunc(f func(string) string) UnmarshalOption {
 	return func(opt *unmarshalOptions) {
 		opt.canonicalKey = f
 	}
 }
 
+// WithDefault customizes an Unmarshaler with fill default values.
+func WithDefault() UnmarshalOption {
+	return func(opt *unmarshalOptions) {
+		opt.fillDefault = true
+	}
+}
+
 func createValuer(v valuerWithParent, opts *fieldOptionsWithContext) valuerWithParent {
 	if opts.inherit() {
 		return recursiveValuer{


Test patch:
diff --git a/core/conf/config_test.go b/core/conf/config_test.go
index 7ab3689f..8dba474f 100644
--- a/core/conf/config_test.go
+++ b/core/conf/config_test.go
@@ -1039,3 +1039,55 @@ func createTempFile(ext, text string) (string, error) {
 
 	return filename, nil
 }
+
+func TestFillDefaultUnmarshal(t *testing.T) {
+	t.Run("nil", func(t *testing.T) {
+		type St struct{}
+		err := FillDefault(St{})
+		assert.Error(t, err)
+	})
+
+	t.Run("not nil", func(t *testing.T) {
+		type St struct{}
+		err := FillDefault(&St{})
+		assert.NoError(t, err)
+	})
+
+	t.Run("default", func(t *testing.T) {
+		type St struct {
+			A string `json:",default=a"`
+			B string
+		}
+		var st St
+		err := FillDefault(&st)
+		assert.NoError(t, err)
+		assert.Equal(t, st.A, "a")
+	})
+
+	t.Run("env", func(t *testing.T) {
+		type St struct {
+			A string `json:",default=a"`
+			B string
+			C string `json:",env=TEST_C"`
+		}
+		t.Setenv("TEST_C", "c")
+
+		var st St
+		err := FillDefault(&st)
+		assert.NoError(t, err)
+		assert.Equal(t, st.A, "a")
+		assert.Equal(t, st.C, "c")
+	})
+
+	t.Run("has vaue", func(t *testing.T) {
+		type St struct {
+			A string `json:",default=a"`
+			B string
+		}
+		var st = St{
+			A: "b",
+		}
+		err := FillDefault(&st)
+		assert.Error(t, err)
+	})
+}
diff --git a/core/mapping/unmarshaler_test.go b/core/mapping/unmarshaler_test.go
index b756fba6..cdd7b98a 100644
--- a/core/mapping/unmarshaler_test.go
+++ b/core/mapping/unmarshaler_test.go
@@ -4384,3 +4384,56 @@ func BenchmarkUnmarshal(b *testing.B) {
 		UnmarshalKey(data, &an)
 	}
 }
+
+func TestFillDefaultUnmarshal(t *testing.T) {
+	fillDefaultUnmarshal := NewUnmarshaler(jsonTagKey, WithDefault())
+	t.Run("nil", func(t *testing.T) {
+		type St struct{}
+		err := fillDefaultUnmarshal.Unmarshal(map[string]any{}, St{})
+		assert.Error(t, err)
+	})
+
+	t.Run("not nil", func(t *testing.T) {
+		type St struct{}
+		err := fillDefaultUnmarshal.Unmarshal(map[string]any{}, &St{})
+		assert.NoError(t, err)
+	})
+
+	t.Run("default", func(t *testing.T) {
+		type St struct {
+			A string `json:",default=a"`
+			B string
+		}
+		var st St
+		err := fillDefaultUnmarshal.Unmarshal(map[string]any{}, &st)
+		assert.NoError(t, err)
+		assert.Equal(t, st.A, "a")
+	})
+
+	t.Run("env", func(t *testing.T) {
+		type St struct {
+			A string `json:",default=a"`
+			B string
+			C string `json:",env=TEST_C"`
+		}
+		t.Setenv("TEST_C", "c")
+
+		var st St
+		err := fillDefaultUnmarshal.Unmarshal(map[string]any{}, &st)
+		assert.NoError(t, err)
+		assert.Equal(t, st.A, "a")
+		assert.Equal(t, st.C, "c")
+	})
+
+	t.Run("has value", func(t *testing.T) {
+		type St struct {
+			A string `json:",default=a"`
+			B string
+		}
+		var st = St{
+			A: "b",
+		}
+		err := fillDefaultUnmarshal.Unmarshal(map[string]any{}, &st)
+		assert.Error(t, err)
+	})
+}