Add support for decoding settings into arb. structure
This commit is contained in:
parent
51db7cbb36
commit
90d5b13cbb
136
pkg/settings/kv_decoder.go
Normal file
136
pkg/settings/kv_decoder.go
Normal 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
|
||||
}
|
||||
96
pkg/settings/kv_decoder_test.go
Normal file
96
pkg/settings/kv_decoder_test.go
Normal 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)
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user