diff --git a/structtags/README.md b/structtags/README.md new file mode 100644 index 0000000..430e95a --- /dev/null +++ b/structtags/README.md @@ -0,0 +1,13 @@ +# structtags + +Ускорьте функцию `Unpack()`, про которую рассказывали на лекции (https://p.go.manytask.org/08-reflect/lecture.slide#19). + +Ваша функция должна работать быстрее, чем бейзлайн + 20%. +``` +goos: linux +goarch: amd64 +pkg: gitlab.com/slon/shad-go/structtags +BenchmarkUnpacker/user-4 3273 329346 ns/op +BenchmarkUnpacker/user+good+order-4 648 1721068 ns/op +PASS +``` diff --git a/structtags/structtags.go b/structtags/structtags.go new file mode 100644 index 0000000..96b0d14 --- /dev/null +++ b/structtags/structtags.go @@ -0,0 +1,80 @@ +// +build !solution + +package structtags + +import ( + "fmt" + "net/http" + "reflect" + "strconv" + "strings" +) + +type Unpacker struct { +} + +func NewUnpacker() *Unpacker { + return &Unpacker{} +} + +func (u *Unpacker) Unpack(req *http.Request, ptr interface{}) error { + if err := req.ParseForm(); err != nil { + return err + } + fields := make(map[string]reflect.Value) + v := reflect.ValueOf(ptr).Elem() + for i := 0; i < v.NumField(); i++ { + fieldInfo := v.Type().Field(i) + tag := fieldInfo.Tag + name := tag.Get("http") + if name == "" { + name = strings.ToLower(fieldInfo.Name) + } + fields[name] = v.Field(i) + } + for name, values := range req.Form { + f := fields[name] + if !f.IsValid() { + continue + } + for _, value := range values { + if f.Kind() == reflect.Slice { + elem := reflect.New(f.Type().Elem()).Elem() + if err := u.populate(elem, value); err != nil { + return fmt.Errorf("%s: %v", name, err) + } + f.Set(reflect.Append(f, elem)) + } else { + if err := u.populate(f, value); err != nil { + return fmt.Errorf("%s: %v", name, err) + } + } + } + } + return nil +} + +func (u *Unpacker) populate(v reflect.Value, value string) error { + switch v.Kind() { + case reflect.String: + v.SetString(value) + + case reflect.Int: + i, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return err + } + v.SetInt(i) + + case reflect.Bool: + b, err := strconv.ParseBool(value) + if err != nil { + return err + } + v.SetBool(b) + + default: + return fmt.Errorf("unsupported kind %s", v.Type()) + } + return nil +} diff --git a/structtags/structtags_test.go b/structtags/structtags_test.go new file mode 100644 index 0000000..2d97931 --- /dev/null +++ b/structtags/structtags_test.go @@ -0,0 +1,166 @@ +package structtags + +import ( + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/require" +) + +var ( + expectedUser = User{ + ID: 1, + Name: "John", + Surname: "Doe", + Phone: "88005551234", + HasSubscription: true, + } + userURL = fmt.Sprintf( + "localhost/user?id=%d&name=%s&surname=%s&phone=%s&has_subscription=%t", + expectedUser.ID, + expectedUser.Name, + expectedUser.Surname, + expectedUser.Phone, + expectedUser.HasSubscription, + ) + + expectedGood = Good{ + ID: 45, + Name: "pizza", + } + goodURL = fmt.Sprintf( + "localhost/good?id=%d&name=%s", + expectedGood.ID, + expectedGood.Name, + ) + + expectedOrder = Order{ + ID: 37, + UserID: 73, + GoodIds: []int{1, 2, 3}, + Date: "01.01.1970", + } + orderURL = fmt.Sprintf( + "localhost/order?id=%d&user_id=%d&good_ids=%d&good_ids=%d&good_ids=%d&date=%s", + expectedOrder.ID, + expectedOrder.UserID, + expectedOrder.GoodIds[0], + expectedOrder.GoodIds[1], + expectedOrder.GoodIds[2], + expectedOrder.Date, + ) +) + +type User struct { + ID int + Name string + Surname string + Phone string + HasSubscription bool `http:"has_subscription"` +} + +type Good struct { + ID int + Name string +} + +type Order struct { + ID int + UserID int `http:"user_id"` + GoodIds []int `http:"good_ids"` + Date string +} + +func TestUnpack_User(t *testing.T) { + r, _ := http.NewRequest("GET", userURL, nil) + user := &User{} + u := NewUnpacker() + err := u.Unpack(r, user) + require.NoError(t, err) + require.Equal(t, expectedUser.ID, user.ID) + require.Equal(t, expectedUser.Name, user.Name) + require.Equal(t, expectedUser.Surname, user.Surname) + require.Equal(t, expectedUser.Phone, user.Phone) + require.Equal(t, expectedUser.HasSubscription, user.HasSubscription) +} + +func TestUnpack_Good(t *testing.T) { + r, _ := http.NewRequest("GET", goodURL, nil) + good := &Good{} + u := NewUnpacker() + err := u.Unpack(r, good) + require.NoError(t, err) + require.Equal(t, expectedGood.ID, good.ID) + require.Equal(t, expectedGood.Name, good.Name) +} + +func TestUnpack_Order(t *testing.T) { + r, _ := http.NewRequest("GET", orderURL, nil) + order := &Order{} + u := NewUnpacker() + err := u.Unpack(r, order) + fmt.Println(orderURL) + require.NoError(t, err) + require.Equal(t, expectedOrder.ID, order.ID) + require.Equal(t, expectedOrder.UserID, order.UserID) + require.Equal(t, expectedOrder.GoodIds, order.GoodIds) + require.Equal(t, expectedOrder.Date, order.Date) +} + +func TestUnpack_ParseFormError(t *testing.T) { + r, _ := http.NewRequest("POST", "localhost", nil) + user := &User{} + u := NewUnpacker() + err := u.Unpack(r, user) + require.Error(t, err) +} + +func TestUnpack_IncorrectBoolData(t *testing.T) { + url := "localhost/user?id=1&has_subscription=7" + r, _ := http.NewRequest("GET", url, nil) + user := &User{} + u := NewUnpacker() + err := u.Unpack(r, user) + require.Error(t, err) +} + +func TestUnpack_IncorrectIntData(t *testing.T) { + url := "localhost/user?id=abc" + r, _ := http.NewRequest("GET", url, nil) + user := &User{} + u := NewUnpacker() + err := u.Unpack(r, user) + require.Error(t, err) +} + +func BenchmarkUnpacker(b *testing.B) { + userRequest, _ := http.NewRequest("GET", userURL, nil) + user := &User{} + + goodRequest, _ := http.NewRequest("GET", goodURL, nil) + good := &Good{} + + orderRequest, _ := http.NewRequest("GET", orderURL, nil) + order := &Order{} + + b.Run("user", func(b *testing.B) { + u := NewUnpacker() + for i := 0; i < b.N; i++ { + for j := 0; j < 1000; j++ { + _ = u.Unpack(userRequest, user) + } + } + }) + + b.Run("user+good+order", func(b *testing.B) { + u := NewUnpacker() + for i := 0; i < b.N; i++ { + for j := 0; j < 1000; j++ { + _ = u.Unpack(userRequest, user) + _ = u.Unpack(goodRequest, good) + _ = u.Unpack(orderRequest, order) + } + } + }) +}