From 90d5b13cbb08f42e54d8c11ef25532e2b40b0f3a Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Wed, 23 Oct 2019 21:57:38 +0200 Subject: [PATCH] Add support for decoding settings into arb. structure --- pkg/settings/kv_decoder.go | 136 ++++++++++++++++++++++++++++++++ pkg/settings/kv_decoder_test.go | 96 ++++++++++++++++++++++ pkg/settings/types.go | 17 ++++ 3 files changed, 249 insertions(+) create mode 100644 pkg/settings/kv_decoder.go create mode 100644 pkg/settings/kv_decoder_test.go diff --git a/pkg/settings/kv_decoder.go b/pkg/settings/kv_decoder.go new file mode 100644 index 000000000..3901f0181 --- /dev/null +++ b/pkg/settings/kv_decoder.go @@ -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 +} diff --git a/pkg/settings/kv_decoder_test.go b/pkg/settings/kv_decoder_test.go new file mode 100644 index 000000000..b38cf09b8 --- /dev/null +++ b/pkg/settings/kv_decoder_test.go @@ -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) +} diff --git a/pkg/settings/types.go b/pkg/settings/types.go index c22f2592f..d139f5547 100644 --- a/pkg/settings/types.go +++ b/pkg/settings/types.go @@ -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 {