3
0

Add support for decoding settings into arb. structure

This commit is contained in:
Denis Arh 2019-10-23 21:57:38 +02:00
parent 51db7cbb36
commit 90d5b13cbb
3 changed files with 249 additions and 0 deletions

136
pkg/settings/kv_decoder.go Normal file
View File

@ -0,0 +1,136 @@
package settings
import (
"errors"
"reflect"
"strings"
)
type (
// @todo support Decoder interface
// Decoder interface {
// Decode(kv KV, prefix string) error
// }
)
var (
// @todo support Decoder interface
// decoderTyEl = reflect.TypeOf((*Decoder)(nil)).Elem()
)
// Decode converts key-value (KV) into structs using tags & field names
//
// Supports decoding into all scalar types, can handle nested structures and simple maps (1 dim, string as key)
//
// Example:
// key-value pairs:
// string1: "string"
// number: 42
//
// struct{
// String1 string `kv:"string1`
// Number int
// }
//
func Decode(kv KV, dst interface{}, pp ...string) (err error) {
v := reflect.ValueOf(dst)
if v.Kind() != reflect.Ptr {
return errors.New("expecting a pointer, not a value")
}
if v.IsNil() {
return errors.New("nil pointer passed")
}
v = v.Elem()
var prefix string
if len(pp) > 0 {
// If called with prefix, join string slice + 1 empty string (to ensure tailing dot)
prefix = strings.Join(append(pp, ""), ".")
}
length := v.NumField()
for i := 0; i < length; i++ {
var (
f = v.Field(i)
t = v.Type().Field(i)
key = prefix + strings.ToLower(t.Name[:1]) + t.Name[1:]
tag = t.Tag.Get("kv")
tagFlags []string
)
if tag == "-" {
// Skip fields with kv tags equal to "-"
continue
}
// if !f.CanSet() {
// return errors.New("unexpected pointer for field " + t.Name)
// }
if tag != "" {
tagFlags = strings.Split(tag, ",")
if tagFlags[0] != "" {
// key explicitly set via tag, use that!
key = prefix + tagFlags[0]
}
for f := 1; f < len(tagFlags); f++ {
// @todo resolve i18n and other flags...
}
}
// @todo handle Decoder interface
// if f.Type().Implements(decoderTyEl) {
// result := reflect.ValueOf(&t).MethodByName("Decode").Call([]reflect.Value{
// reflect.ValueOf(kv.Filter(key)),
// reflect.ValueOf(prefix),
// })
//
// if len(result) != 1 {
// return errors.New("internal error, Decoder signature does not match")
// }
// }
// Handles structs
//
// It calls Decode recursively
if f.Kind() == reflect.Struct {
if err = Decode(kv.Filter(key), f.Addr().Interface(), key); err != nil {
return
}
continue
}
// Handles map values
if f.Kind() == reflect.Map {
if f.IsNil() {
// allocate new map
f.Set(reflect.MakeMap(f.Type()))
}
for k, val := range kv.CutPrefix(key + ".") {
mapValue := reflect.New(f.Type().Elem())
val.Unmarshal(mapValue.Interface())
f.SetMapIndex(reflect.ValueOf(k), mapValue.Elem())
}
continue
}
if val, ok := kv[key]; ok {
if err = val.Unmarshal(f.Addr().Interface()); err != nil {
return
}
}
}
return
}

View File

@ -0,0 +1,96 @@
package settings
import (
"testing"
"github.com/jmoiron/sqlx/types"
"github.com/stretchr/testify/require"
)
func TestDecode(t *testing.T) {
type (
subdst struct {
S string `kv:"s"`
B bool `kv:"b"`
Bar struct {
Foo string `kv:"foo"`
} `kv:"bar"`
}
withHandler struct{}
dst struct {
S string `kv:"s"`
B bool `kv:"b"`
N int `kv:"n"`
NoKV string
WH withHandler
Ptr *string
Sub subdst `kv:"sub"`
Map map[string]string `kv:"sub.map"`
S2I map[string]int `kv:"sub.s2i"`
}
)
var (
ptr = "point-me"
aux = dst{}
kv = KV{
"s": types.JSONText(`"string"`),
"b": types.JSONText("true"),
"n": types.JSONText("42"),
"sub.s": types.JSONText(`"string"`),
"sub.b": types.JSONText("true"),
"sub.bar": nil,
"sub.bar.foo": types.JSONText(`"foobar"`),
"noKV": types.JSONText(`"NO-KV-!"`),
"ptr": types.JSONText(`"point-me"`),
"sub.map.foo": types.JSONText(`"foo"`),
"sub.map.bar": types.JSONText(`"bar"`),
"sub.map.baz": types.JSONText(`"baz"`),
"sub.s2i.one": types.JSONText(`1`),
"sub.s2i.two": types.JSONText(`2`),
}
eq = dst{
S: "string",
B: true,
N: 42,
NoKV: "NO-KV-!",
Ptr: &ptr,
Sub: subdst{
S: "string",
B: true,
},
Map: map[string]string{
"foo": "foo",
"bar": "bar",
"baz": "baz",
},
S2I: map[string]int{
"one": 1,
"two": 2,
},
}
)
// setting this externaly (embedded structs)
eq.Sub.Bar.Foo = "foobar"
require.NoError(t, Decode(kv, &aux))
require.Equal(t, eq, aux)
}

View File

@ -121,6 +121,23 @@ func (kv KV) Filter(prefix string) KV {
return out
}
// CutPrefix returns values with matching prefix and removes the prefix from keys
func (kv KV) CutPrefix(prefix string) KV {
var out = KV{}
for k, v := range kv {
if strings.Index(k, prefix) == 0 {
out[k[len(prefix):]] = v
}
}
return out
}
// Decode is a helper function on KV that calls Decode() and passes on the dst
func (kv KV) Decode(dst interface{}) error {
return Decode(kv, dst)
}
// Replace finds and updates existing or appends new value
func (set *ValueSet) Replace(n *Value) {
for _, v := range *set {