feat(config): support Resolver for config variable placeholders (#1135)

* test: add yaml test case for reader

test: init test case for fillTemplate

* add env placeholder resolver

fix ci test fail

* fix ci test fail

* feat(config): add config resolver

* test(config): add test cases

* move defaultDecoder & defaultResolver to options.go
pull/1185/head
Kagaya 3 years ago committed by GitHub
parent 3089419e14
commit e19730e4b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 14
      config/config.go
  2. 141
      config/config_test.go
  3. 67
      config/options.go
  4. 3
      config/reader.go
  5. 45
      config/reader_test.go

@ -2,12 +2,10 @@ package config
import (
"errors"
"fmt"
"reflect"
"sync"
"time"
"github.com/go-kratos/kratos/v2/encoding"
"github.com/go-kratos/kratos/v2/log"
// init encoding
@ -51,16 +49,8 @@ type config struct {
func New(opts ...Option) Config {
options := options{
logger: log.DefaultLogger,
decoder: func(src *KeyValue, target map[string]interface{}) error {
if src.Format == "" {
target[src.Key] = src.Value
return nil
}
if codec := encoding.GetCodec(src.Format); codec != nil {
return codec.Unmarshal(src.Value, &target)
}
return fmt.Errorf("unsupported key: %s format: %s", src.Key, src.Format)
},
decoder: defaultDecoder,
resolver: defaultResolver,
}
for _, o := range opts {
o(&options)

@ -0,0 +1,141 @@
package config
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
)
func TestDefaultResolver(t *testing.T) {
var (
portString = "8080"
countInt = 10
enableBool = true
rateFloat = 0.9
)
data := map[string]interface{}{
"foo": map[string]interface{}{
"bar": map[string]interface{}{
"notexist": "${NOTEXIST:100}",
"port": "${PORT:8081}",
"count": "${COUNT:0}",
"enable": "${ENABLE:false}",
"rate": "${RATE}",
"empty": "${EMPTY:foobar}",
"array": []interface{}{"${PORT}", "${NOTEXIST:8081}"},
"value1": "${test.value}",
"value2": "$PORT",
"value3": "$PORT:default",
},
},
"test": map[string]interface{}{
"value": "foobar",
},
"PORT": "8080",
"COUNT": "10",
"ENABLE": "true",
"RATE": "0.9",
"EMPTY": "",
}
tests := []struct {
name string
path string
expect interface{}
}{
{
name: "test not exist int env with default",
path: "foo.bar.notexist",
expect: 100,
},
{
name: "test string with default",
path: "foo.bar.port",
expect: portString,
},
{
name: "test int with default",
path: "foo.bar.count",
expect: countInt,
},
{
name: "test bool with default",
path: "foo.bar.enable",
expect: enableBool,
},
{
name: "test float without default",
path: "foo.bar.rate",
expect: rateFloat,
},
{
name: "test empty value with default",
path: "foo.bar.empty",
expect: "",
},
{
name: "test array",
path: "foo.bar.array",
expect: []interface{}{portString, "8081"},
},
{
name: "test ${test.value}",
path: "foo.bar.value1",
expect: "foobar",
},
{
name: "test $value",
path: "foo.bar.value2",
expect: portString,
},
{
name: "test $value:default",
path: "foo.bar.value3",
expect: portString + ":default",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := defaultResolver(data)
assert.NoError(t, err)
rd := reader{
values: data,
}
if v, ok := rd.Value(test.path); ok {
var actual interface{}
switch test.expect.(type) {
case int:
if actual, err = v.Int(); err == nil {
assert.Equal(t, test.expect, int(actual.(int64)), "int value should be equal")
}
case string:
if actual, err = v.String(); err == nil {
assert.Equal(t, test.expect, actual, "string value should be equal")
}
case bool:
if actual, err = v.Bool(); err == nil {
assert.Equal(t, test.expect, actual, "bool value should be equal")
}
case float64:
if actual, err = v.Float(); err == nil {
assert.Equal(t, test.expect, actual, "float64 value should be equal")
}
default:
actual = v.Load()
if !reflect.DeepEqual(test.expect, actual) {
t.Logf("expect: %#v, actural: %#v", test.expect, actual)
t.Fail()
}
}
if err != nil {
t.Error(err)
}
} else {
t.Error("value path not found")
}
})
}
}

@ -1,18 +1,27 @@
package config
import (
"fmt"
"os"
"strings"
"github.com/go-kratos/kratos/v2/encoding"
"github.com/go-kratos/kratos/v2/log"
)
// Decoder is config decoder.
type Decoder func(*KeyValue, map[string]interface{}) error
// Resolver resolve placeholder in config.
type Resolver func(map[string]interface{}) error
// Option is config option.
type Option func(*options)
type options struct {
sources []Source
decoder Decoder
resolver Resolver
logger log.Logger
}
@ -30,9 +39,67 @@ func WithDecoder(d Decoder) Option {
}
}
// WithResolver with config resolver.
func WithResolver(r Resolver) Option {
return func(o *options) {
o.resolver = r
}
}
// WithLogger with config logger.
func WithLogger(l log.Logger) Option {
return func(o *options) {
o.logger = l
}
}
// defaultDecoder decode config from source KeyValue
// to target map[string]interface{} using src.Format codec.
func defaultDecoder(src *KeyValue, target map[string]interface{}) error {
if src.Format == "" {
target[src.Key] = src.Value
return nil
}
if codec := encoding.GetCodec(src.Format); codec != nil {
return codec.Unmarshal(src.Value, &target)
}
return fmt.Errorf("unsupported key: %s format: %s", src.Key, src.Format)
}
// defaultResolver resolve placeholder in map value,
// placeholder format in ${key:default} or $key.
func defaultResolver(input map[string]interface{}) error {
mapper := func(name string) string {
args := strings.Split(strings.TrimSpace(name), ":")
if v, has := readValue(input, args[0]); has {
s, _ := v.String()
return s
} else if len(args) > 1 { // default value
return args[1]
}
return ""
}
var resolve func(map[string]interface{}) error
resolve = func(sub map[string]interface{}) error {
for k, v := range sub {
switch vt := v.(type) {
case string:
sub[k] = os.Expand(vt, mapper)
case map[string]interface{}:
if err := resolve(vt); err != nil {
return err
}
case []interface{}:
for i, iface := range vt {
if s, ok := iface.(string); ok {
vt[i] = os.Expand(s, mapper)
}
}
sub[k] = vt
}
}
return nil
}
return resolve(input)
}

@ -43,6 +43,9 @@ func (r *reader) Merge(kvs ...*KeyValue) error {
return err
}
}
if err := r.opts.resolver(merged); err != nil {
return err
}
r.values = merged
return nil
}

@ -20,6 +20,7 @@ func TestReader_Merge(t *testing.T) {
}
return fmt.Errorf("unsupported key: %s format: %s", kv.Key, kv.Format)
},
resolver: defaultResolver,
}
r := newReader(opts)
err = r.Merge(&KeyValue{
@ -55,10 +56,6 @@ func TestReader_Merge(t *testing.T) {
}
func TestReader_Value(t *testing.T) {
var (
err error
ok bool
)
opts := options{
decoder: func(kv *KeyValue, v map[string]interface{}) error {
if codec := encoding.GetCodec(kv.Format); codec != nil {
@ -66,13 +63,42 @@ func TestReader_Value(t *testing.T) {
}
return fmt.Errorf("unsupported key: %s format: %s", kv.Key, kv.Format)
},
resolver: defaultResolver,
}
r := newReader(opts)
err = r.Merge(&KeyValue{
Key: "b",
ymlval := `
a:
b:
X: 1
Y: "lol"
z: true
`
tests := []struct {
name string
kv KeyValue
}{
{
name: "json value",
kv: KeyValue{
Key: "config",
Value: []byte(`{"a": {"b": {"X": 1, "Y": "lol", "z": true}}}`),
Format: "json",
})
},
},
{
name: "yaml value",
kv: KeyValue{
Key: "config",
Value: []byte(ymlval),
Format: "yaml",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
r := newReader(opts)
err := r.Merge(&test.kv)
assert.NoError(t, err)
vv, ok := r.Value("a.b.X")
assert.True(t, ok)
@ -102,6 +128,8 @@ func TestReader_Value(t *testing.T) {
vv, ok = r.Value("a.b.Y.")
assert.False(t, ok)
})
}
}
func TestReader_Source(t *testing.T) {
@ -115,6 +143,7 @@ func TestReader_Source(t *testing.T) {
}
return fmt.Errorf("unsupported key: %s format: %s", kv.Key, kv.Format)
},
resolver: defaultResolver,
}
r := newReader(opts)
err = r.Merge(&KeyValue{

Loading…
Cancel
Save