diff --git a/common/structure/structure.go b/common/structure/structure.go new file mode 100644 index 0000000..ac824a0 --- /dev/null +++ b/common/structure/structure.go @@ -0,0 +1,160 @@ +package structure + +import ( + "fmt" + "reflect" + "strconv" + "strings" +) + +// Option is the configuration that is used to create a new decoder +type Option struct { + TagName string + WeaklyTypedInput bool +} + +// Decoder is the core of structure +type Decoder struct { + option *Option +} + +// NewDecoder return a Decoder by Option +func NewDecoder(option Option) *Decoder { + if option.TagName == "" { + option.TagName = "structure" + } + return &Decoder{option: &option} +} + +// Decode transform a map[string]interface{} to a struct +func (d *Decoder) Decode(src map[string]interface{}, dst interface{}) error { + if reflect.TypeOf(dst).Kind() != reflect.Ptr { + return fmt.Errorf("Decode must recive a ptr struct") + } + t := reflect.TypeOf(dst).Elem() + v := reflect.ValueOf(dst).Elem() + for idx := 0; idx < v.NumField(); idx++ { + field := t.Field(idx) + + tag := field.Tag.Get(d.option.TagName) + str := strings.SplitN(tag, ",", 2) + key := str[0] + omitempty := false + if len(str) > 1 { + omitempty = str[1] == "omitempty" + } + + value, ok := src[key] + if !ok { + if omitempty { + continue + } + return fmt.Errorf("key %s missing", key) + } + + err := d.decode(key, value, v.Field(idx)) + if err != nil { + return err + } + } + return nil +} + +func (d *Decoder) decode(name string, data interface{}, val reflect.Value) error { + switch val.Kind() { + case reflect.Int: + return d.decodeInt(name, data, val) + case reflect.String: + return d.decodeString(name, data, val) + case reflect.Bool: + return d.decodeBool(name, data, val) + case reflect.Slice: + return d.decodeSlice(name, data, val) + default: + return fmt.Errorf("type %s not support", val.Kind().String()) + } +} + +func (d *Decoder) decodeInt(name string, data interface{}, val reflect.Value) (err error) { + dataVal := reflect.ValueOf(data) + kind := dataVal.Kind() + switch { + case kind == reflect.Int: + val.SetInt(dataVal.Int()) + case kind == reflect.String && d.option.WeaklyTypedInput: + var i int64 + i, err = strconv.ParseInt(dataVal.String(), 0, val.Type().Bits()) + if err == nil { + val.SetInt(i) + } else { + err = fmt.Errorf("cannot parse '%s' as int: %s", name, err) + } + default: + err = fmt.Errorf( + "'%s' expected type '%s', got unconvertible type '%s'", + name, val.Type(), dataVal.Type(), + ) + } + return err +} + +func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value) (err error) { + dataVal := reflect.ValueOf(data) + kind := dataVal.Kind() + switch { + case kind == reflect.String: + val.SetString(dataVal.String()) + case kind == reflect.Int && d.option.WeaklyTypedInput: + val.SetString(strconv.FormatInt(dataVal.Int(), 10)) + default: + err = fmt.Errorf( + "'%s' expected type'%s', got unconvertible type '%s'", + name, val.Type(), dataVal.Type(), + ) + } + return err +} + +func (d *Decoder) decodeBool(name string, data interface{}, val reflect.Value) (err error) { + dataVal := reflect.ValueOf(data) + kind := dataVal.Kind() + switch { + case kind == reflect.Bool: + val.SetBool(dataVal.Bool()) + case kind == reflect.Int && d.option.WeaklyTypedInput: + val.SetBool(dataVal.Int() != 0) + default: + err = fmt.Errorf( + "'%s' expected type'%s', got unconvertible type '%s'", + name, val.Type(), dataVal.Type(), + ) + } + return err +} + +func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value) error { + dataVal := reflect.Indirect(reflect.ValueOf(data)) + valType := val.Type() + valElemType := valType.Elem() + + if dataVal.Kind() != reflect.Slice { + return fmt.Errorf("'%s' is not a slice", name) + } + + valSlice := val + for i := 0; i < dataVal.Len(); i++ { + currentData := dataVal.Index(i).Interface() + for valSlice.Len() <= i { + valSlice = reflect.Append(valSlice, reflect.Zero(valElemType)) + } + currentField := valSlice.Index(i) + + fieldName := fmt.Sprintf("%s[%d]", name, i) + if err := d.decode(fieldName, currentData, currentField); err != nil { + return err + } + } + + val.Set(valSlice) + return nil +} diff --git a/common/structure/structure_test.go b/common/structure/structure_test.go new file mode 100644 index 0000000..6e3e629 --- /dev/null +++ b/common/structure/structure_test.go @@ -0,0 +1,141 @@ +package structure + +import ( + "reflect" + "testing" +) + +var decoder = NewDecoder(Option{TagName: "test"}) +var weakTypeDecoder = NewDecoder(Option{TagName: "test", WeaklyTypedInput: true}) + +type Baz struct { + Foo int `test:"foo"` + Bar string `test:"bar"` +} + +type BazSlice struct { + Foo int `test:"foo"` + Bar []string `test:"bar"` +} + +type BazOptional struct { + Foo int `test:"foo,omitempty"` + Bar string `test:"bar,omitempty"` +} + +func TestStructure_Basic(t *testing.T) { + rawMap := map[string]interface{}{ + "foo": 1, + "bar": "test", + "extra": false, + } + + goal := &Baz{ + Foo: 1, + Bar: "test", + } + + s := &Baz{} + err := decoder.Decode(rawMap, s) + if err != nil { + t.Fatal(err.Error()) + } + if !reflect.DeepEqual(s, goal) { + t.Fatalf("bad: %#v", s) + } +} + +func TestStructure_Slice(t *testing.T) { + rawMap := map[string]interface{}{ + "foo": 1, + "bar": []string{"one", "two"}, + } + + goal := &BazSlice{ + Foo: 1, + Bar: []string{"one", "two"}, + } + + s := &BazSlice{} + err := decoder.Decode(rawMap, s) + if err != nil { + t.Fatal(err.Error()) + } + if !reflect.DeepEqual(s, goal) { + t.Fatalf("bad: %#v", s) + } +} + +func TestStructure_Optional(t *testing.T) { + rawMap := map[string]interface{}{ + "foo": 1, + } + + goal := &BazOptional{ + Foo: 1, + } + + s := &BazOptional{} + err := decoder.Decode(rawMap, s) + if err != nil { + t.Fatal(err.Error()) + } + if !reflect.DeepEqual(s, goal) { + t.Fatalf("bad: %#v", s) + } +} + +func TestStructure_MissingKey(t *testing.T) { + rawMap := map[string]interface{}{ + "foo": 1, + } + + s := &Baz{} + err := decoder.Decode(rawMap, s) + if err == nil { + t.Fatalf("should throw error: %#v", s) + } +} + +func TestStructure_ParamError(t *testing.T) { + rawMap := map[string]interface{}{} + s := Baz{} + err := decoder.Decode(rawMap, s) + if err == nil { + t.Fatalf("should throw error: %#v", s) + } +} + +func TestStructure_SliceTypeError(t *testing.T) { + rawMap := map[string]interface{}{ + "foo": 1, + "bar": []int{1, 2}, + } + + s := &BazSlice{} + err := decoder.Decode(rawMap, s) + if err == nil { + t.Fatalf("should throw error: %#v", s) + } +} + +func TestStructure_WeakType(t *testing.T) { + rawMap := map[string]interface{}{ + "foo": "1", + "bar": []int{1}, + } + + goal := &BazSlice{ + Foo: 1, + Bar: []string{"1"}, + } + + s := &BazSlice{} + err := weakTypeDecoder.Decode(rawMap, s) + if err != nil { + t.Fatal(err.Error()) + } + if !reflect.DeepEqual(s, goal) { + t.Fatalf("bad: %#v", s) + } +}