Repository: go-kratos/kratos
Pull request #3106 (2023-12-03 07:29:27)

feat(config): config support custom merge config
<!--
🎉 Thanks for sending a pull request to Kratos! Here are some tips for you:

1. If this is your first time contributing to Kratos, please read our contribution guide: https://go-kratos.dev/en/docs/community/contribution/
2. Ensure you have added or ran the appropriate tests and lint for your PR, please use `make lint` and `make test` before filing your PR, use `make clean` to tidy your go mod.
3. If the PR is unfinished, you may need to mark it as a WIP(Work In Progress) PR or Draft PR
4. Please use a semantic commits format title, such as `<type>[optional scope]: <description>`, see: https://go-kratos.dev/docs/community/contribution#type
5. at the same time, please note that similar work should be submitted in one PR as far as possible to reduce the workload of reviewers. Do not split a work into multiple PR unless it should.
-->

<!--
🎉 感谢您向 Kratos 发送 PR！以下是一些提示：
如果这是你第一次为 Kratos 贡献，请阅读我们的贡献指南：https://go-kratos.dev/en/docs/community/contribution/
2、确保您已经为您的 PR 添加或运行了适当的测试和lint，请在提交PR之前使用“make lint”和“make test”，使用“make clean”整理您的 go.mod。
3、如果 PR 未完成，您可能需要将其标记为 WIP（Work In Progress）PR 或 Draft PR
4、请使用语义提交格式标题，如“<类型>[可选范围]：<说明>`，请参阅：https://go-kratos.dev/docs/community/contribution#type
5. 同时请注意，同类的工作请尽量在一个PR中提交，以减轻 review 者的工作负担，不要把一项工作拆分成很多个PR，除非它应该这样做。
-->


#### Description (what this PR does / why we need it):
<!--
* The description should include the motivation for this PR or contrast this with previous behavior
-->


#### Which issue(s) this PR fixes (resolves / be part of):
<!--
* Automatically closes linked issue when PR is merged.
* If your PR is not fully resolved the issue, please use `part of #<issue number>` instead.

Usage: `fixes/resolves #<issue number>`, or `fixes/resolves (paste link of issue)`.
-->


#### Other special notes for the reviewers:
<!--
* Somethings that need extra attention for the reviewers
* Some additional notes, TODO list, etc.
-->


codecov-commenter: ## [Codecov](https://app.codecov.io/gh/go-kratos/kratos/pull/3106?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) Report
Attention: `1 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`d13071e`)](https://app.codecov.io/gh/go-kratos/kratos/commit/d13071e45f8d6546fd30fdbcb6611fb338d26394?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) 84.28% compared to head [(`f3f2c9d`)](https://app.codecov.io/gh/go-kratos/kratos/pull/3106?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) 84.30%.

> :exclamation: Current head f3f2c9d differs from pull request most recent head d2d0b18. Consider uploading reports for the commit d2d0b18 to get more accurate results

| [Files](https://app.codecov.io/gh/go-kratos/kratos/pull/3106?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) | Patch % | Lines |
|---|---|---|
| [config/reader.go](https://app.codecov.io/gh/go-kratos/kratos/pull/3106?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos#diff-Y29uZmlnL3JlYWRlci5nbw==) | 0.00% | [0 Missing and 1 partial :warning: ](https://app.codecov.io/gh/go-kratos/kratos/pull/3106?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) |

:exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #3106      +/-   ##
==========================================
+ Coverage   84.28%   84.30%   +0.01%     
==========================================
  Files          88       88              
  Lines        3984     3988       +4     
==========================================
+ Hits         3358     3362       +4     
  Misses        449      449              
  Partials      177      177              
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/go-kratos/kratos/pull/3106?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos).


Patch:
diff --git a/config/config.go b/config/config.go
index 9efc9024..a7957d8d 100644
--- a/config/config.go
+++ b/config/config.go
@@ -7,6 +7,8 @@ import (
 	"sync"
 	"time"
 
+	"github.com/imdario/mergo"
+
 	// init encoding
 	_ "github.com/go-kratos/kratos/v2/encoding/json"
 	_ "github.com/go-kratos/kratos/v2/encoding/proto"
@@ -17,12 +19,7 @@ import (
 
 var _ Config = (*config)(nil)
 
-var (
-	// ErrNotFound is key not found.
-	ErrNotFound = errors.New("key not found")
-	// ErrTypeAssert is type assert error.
-	ErrTypeAssert = errors.New("type assert error")
-)
+var ErrNotFound = errors.New("key not found") // ErrNotFound is key not found.
 
 // Observer is config observer.
 type Observer func(string, Value)
@@ -49,6 +46,9 @@ func New(opts ...Option) Config {
 	o := options{
 		decoder:  defaultDecoder,
 		resolver: defaultResolver,
+		merge: func(dst, src interface{}) error {
+			return mergo.Map(dst, src, mergo.WithOverride)
+		},
 	}
 	for _, opt := range opts {
 		opt(&o)
diff --git a/config/options.go b/config/options.go
index 443ddaba..fa833356 100644
--- a/config/options.go
+++ b/config/options.go
@@ -6,7 +6,6 @@ import (
 	"strings"
 
 	"github.com/go-kratos/kratos/v2/encoding"
-	"github.com/go-kratos/kratos/v2/log"
 )
 
 // Decoder is config decoder.
@@ -15,6 +14,9 @@ type Decoder func(*KeyValue, map[string]interface{}) error
 // Resolver resolve placeholder in config.
 type Resolver func(map[string]interface{}) error
 
+// Merge is config merge func.
+type Merge func(dst, src interface{}) error
+
 // Option is config option.
 type Option func(*options)
 
@@ -22,6 +24,7 @@ type options struct {
 	sources  []Source
 	decoder  Decoder
 	resolver Resolver
+	merge    Merge
 }
 
 // WithSource with config source.
@@ -49,10 +52,11 @@ func WithResolver(r Resolver) Option {
 	}
 }
 
-// WithLogger with config logger.
-// Deprecated: use global logger instead.
-func WithLogger(_ log.Logger) Option {
-	return func(o *options) {}
+// WithMergeFunc with config merge func.
+func WithMergeFunc(m Merge) Option {
+	return func(o *options) {
+		o.merge = m
+	}
 }
 
 // defaultDecoder decode config from source KeyValue
diff --git a/config/reader.go b/config/reader.go
index 2202de6b..12970505 100644
--- a/config/reader.go
+++ b/config/reader.go
@@ -8,7 +8,6 @@ import (
 	"strings"
 	"sync"
 
-	"github.com/imdario/mergo"
 	"google.golang.org/protobuf/encoding/protojson"
 	"google.golang.org/protobuf/proto"
 
@@ -48,7 +47,7 @@ func (r *reader) Merge(kvs ...*KeyValue) error {
 			log.Errorf("Failed to config decode error: %v key: %s value: %s", err, kv.Key, string(kv.Value))
 			return err
 		}
-		if err := mergo.Map(&merged, convertMap(next), mergo.WithOverride); err != nil {
+		if err := r.opts.merge(&merged, convertMap(next)); err != nil {
 			log.Errorf("Failed to config merge error: %v key: %s value: %s", err, kv.Key, string(kv.Value))
 			return err
 		}


Test patch:
diff --git a/config/config_test.go b/config/config_test.go
index 9a166b00..9af90a0e 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -3,6 +3,8 @@ package config
 import (
 	"errors"
 	"testing"
+
+	"github.com/imdario/mergo"
 )
 
 const (
@@ -131,6 +133,9 @@ func TestConfig(t *testing.T) {
 		sources:  []Source{jSource},
 		decoder:  defaultDecoder,
 		resolver: defaultResolver,
+		merge: func(dst, src interface{}) error {
+			return mergo.Map(dst, src, mergo.WithOverride)
+		},
 	}
 	cf := &config{}
 	cf.opts = opts
diff --git a/config/options_test.go b/config/options_test.go
index c8cd2bf8..5b2c5aca 100644
--- a/config/options_test.go
+++ b/config/options_test.go
@@ -226,3 +226,14 @@ func TestExpand(t *testing.T) {
 		}
 	}
 }
+
+func TestWithMergeFunc(t *testing.T) {
+	c := &options{}
+	a := func(dst, src interface{}) error {
+		return nil
+	}
+	WithMergeFunc(a)(c)
+	if c.merge == nil {
+		t.Fatal("c.merge is nil")
+	}
+}
diff --git a/config/reader_test.go b/config/reader_test.go
index 11b2d990..cfd49f77 100644
--- a/config/reader_test.go
+++ b/config/reader_test.go
@@ -6,6 +6,8 @@ import (
 	"testing"
 
 	"github.com/go-kratos/kratos/v2/encoding"
+
+	"github.com/imdario/mergo"
 )
 
 func TestReader_Merge(t *testing.T) {
@@ -21,6 +23,9 @@ func TestReader_Merge(t *testing.T) {
 			return fmt.Errorf("unsupported key: %s format: %s", kv.Key, kv.Format)
 		},
 		resolver: defaultResolver,
+		merge: func(dst, src interface{}) error {
+			return mergo.Map(dst, src, mergo.WithOverride)
+		},
 	}
 	r := newReader(opts)
 	err = r.Merge(&KeyValue{
@@ -82,6 +87,9 @@ func TestReader_Value(t *testing.T) {
 			return fmt.Errorf("unsupported key: %s format: %s", kv.Key, kv.Format)
 		},
 		resolver: defaultResolver,
+		merge: func(dst, src interface{}) error {
+			return mergo.Map(dst, src, mergo.WithOverride)
+		},
 	}
 
 	ymlval := `
@@ -184,6 +192,9 @@ func TestReader_Source(t *testing.T) {
 			return fmt.Errorf("unsupported key: %s format: %s", kv.Key, kv.Format)
 		},
 		resolver: defaultResolver,
+		merge: func(dst, src interface{}) error {
+			return mergo.Map(dst, src, mergo.WithOverride)
+		},
 	}
 	r := newReader(opts)
 	err = r.Merge(&KeyValue{