commit 680002c5b37b659b6dfd4ebdc01f5939f59c38f6 Author: Maxim Lebedev Date: Thu Oct 3 14:51:00 2019 +0000 :truck: Moved code from GitHub diff --git a/README.md b/README.md new file mode 100644 index 0000000..6fd2ccf --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# Тестовое задание +Реализовать сервис корзины товаров: + +* Метод добавления/обновления товара в корзину с указанием количества товара +* Метод удаление товара из корзины +* Метод получения списка товаров, их количества и суммы + +Протокол: gRPC +Язык: Golang +БД: postgres + +Приложение должно быть покрыто тестами, код разместить в виде публичного репозитория на github. + +Для поднятия тестового окружения можно использовать docker & docker-compose, для CI - travis. diff --git a/cmd/client/main.go b/cmd/client/main.go new file mode 100644 index 0000000..e120ef4 --- /dev/null +++ b/cmd/client/main.go @@ -0,0 +1,50 @@ +//go:generate mockgen -package=model -source=./../../internal/model/model.pb.go -destination=./../../internal/model/model_mock.go CartShopClient +package main + +import ( + "flag" + "log" + + "gitlab.com/toby3d/test/internal/client" + "gitlab.com/toby3d/test/internal/model" + "golang.org/x/net/context" +) + +var flagAddr = flag.String("addr", ":2368", "set specific address and port for client instance") + +func main() { + flag.Parse() + + c, err := client.NewClient(*flagAddr) + if err != nil { + log.Fatalln(err.Error()) + } + defer c.Close() + + resp, err := c.Add(context.TODO(), &model.AddRequest{ProductId: 5, Quanity: 42}) + if err != nil { + log.Fatalln(err.Error()) + } + if !resp.GetOk() { + log.Printf("Get error on adding product: %s", resp.GetDescription()) + return + } + log.Printf( + "Product %d has been added to cart, current quanity of this product is %d", + resp.GetItem().GetProductId(), resp.GetItem().GetQuanity(), + ) + + if resp, err = c.Get(context.TODO(), &model.GetRequest{}); err != nil { + log.Fatalln(err.Error()) + } + if !resp.GetOk() { + log.Printf("Get error on getting cart: %s", resp.GetDescription()) + return + } + log.Printf( + "Cart contains %d unique products (in %d quanity) with total price %g", + resp.GetCart().GetItemsCount(), + resp.GetCart().GetQuanityCount(), + resp.GetCart().GetTotalPrice(), + ) +} diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..4086948 --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,44 @@ +//go:generate protoc -I=./../../internal/model/ --go_out=plugins=grpc:./../../internal/model/ ./../../internal/model/model.proto +package main + +import ( + "flag" + "log" + + "gitlab.com/toby3d/test/internal/db" + "gitlab.com/toby3d/test/internal/handler" + "gitlab.com/toby3d/test/internal/server" + "gitlab.com/toby3d/test/internal/store" +) + +var ( + flagAddr = flag.String("addr", ":2368", "set specific address and port for server instance") + flagDB = flag.String( + "db", `host=/var/run/postgresql dbname=testing sslmode=disable`, + "set specific parameters for connecting to database", + ) +) + +func main() { + flag.Parse() + + dataBase, err := db.Open(*flagDB) + if err != nil { + log.Fatalln(err.Error()) + } + defer dataBase.Close() + + if err = db.AutoMigrate(dataBase); err != nil { + log.Fatalln(err.Error()) + } + + srv, err := server.NewServer( + *flagAddr, handler.NewHandler(store.NewCartStore(dataBase), store.NewProductStore(dataBase)), + ) + if err != nil { + log.Fatalln(err.Error()) + } + if err = srv.Start(); err != nil { + log.Fatalln(err.Error()) + } +} diff --git a/internal/client/client.go b/internal/client/client.go new file mode 100644 index 0000000..d1d39e4 --- /dev/null +++ b/internal/client/client.go @@ -0,0 +1,37 @@ +package client + +import ( + "gitlab.com/toby3d/test/internal/model" + "golang.org/x/xerrors" + "google.golang.org/grpc" +) + +// Client представляет собой простой gRPC клиент +type Client struct { + model.ShopCartClient + listener *grpc.ClientConn +} + +// ErrClientNotInitialized описывает ошибку инициализации сервера +var ErrClientNotInitialized = xerrors.New("client is not initialized") + +// NewClient создаёт новый клиент +func NewClient(addr string) (*Client, error) { + var c Client + + var err error + if c.listener, err = grpc.Dial(addr, grpc.WithInsecure()); err != nil { + return nil, err + } + + c.ShopCartClient = model.NewShopCartClient(c.listener) + return &c, nil +} + +// Close закрывает все активные соединения с клиентом +func (c *Client) Close() error { + if c == nil || c.listener == nil { + return ErrClientNotInitialized + } + return c.listener.Close() +} diff --git a/internal/client/client_test.go b/internal/client/client_test.go new file mode 100644 index 0000000..3d8f4a1 --- /dev/null +++ b/internal/client/client_test.go @@ -0,0 +1,27 @@ +package client + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewClient(t *testing.T) { + /* TODO(toby3d): Почему-то невалидный адрес не вызывает никаких ошибок (упреждающее прослушивание?) + t.Run("invalid", func(t *testing.T) { + c, err := NewClient("wtf") + assert.Error(t, err) + t.Run("close", func(t *testing.T) { + assert.Error(t, c.Close()) + }) + }) + */ + t.Run("valid", func(t *testing.T) { + c, err := NewClient(":2368") + assert.NoError(t, err) + assert.NotNil(t, c) + t.Run("close", func(t *testing.T) { + assert.NoError(t, c.Close()) + }) + }) +} diff --git a/internal/db/db.go b/internal/db/db.go new file mode 100644 index 0000000..4e6e9a0 --- /dev/null +++ b/internal/db/db.go @@ -0,0 +1,66 @@ +package db + +import ( + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" + "gitlab.com/toby3d/test/internal/model" + "golang.org/x/xerrors" +) + +// demoProducts представляют собой демо-набор продуктов, добавляемые через AutoMigrate для дальнейшего чтения сторами +var demoProducts = []*model.Product{ + &model.Product{Id: 24, Name: "Banana", Price: 2.49}, + &model.Product{Id: 42, Name: "Apple", Price: 4.99}, + &model.Product{Id: 420, Name: "Bottle of Soda", Price: 10}, +} + +var ErrDataBaseNotInitialized = xerrors.New("database is not initialized") + +// Open открывает соединение с PostgreSQL по указанному адресу с параметрами +func Open(addr string) (*sqlx.DB, error) { + client, err := sqlx.Connect("postgres", addr) + if err != nil { + return nil, err + } + if err = client.Ping(); err != nil { + _ = client.Close() + return nil, err + } + return client, nil +} + +// AutoMigrate создаёт таблицы, если они не существуют, для адекватной работы сторов +func AutoMigrate(db *sqlx.DB) (err error) { + if db == nil || db.DB == nil { + return ErrDataBaseNotInitialized + } + + if _, err = db.Exec("CREATE TABLE IF NOT EXISTS products (id SERIAL PRIMARY KEY, name TEXT, price FLOAT)"); err != nil { + return + } + for _, p := range demoProducts { + if _, err = db.Exec( + "INSERT INTO products (id, name, price) VALUES ($1, $2, $3)", + p.GetId(), p.GetName(), p.GetPrice(), + ); err != nil { + return + } + } + if _, err = db.Exec("CREATE TABLE IF NOT EXISTS cart (product_id SERIAL PRIMARY KEY, quanity INT)"); err != nil { + return + } + return nil +} + +// AutoClean удаляет таблицы созданные AutoMigrate +func AutoClean(db *sqlx.DB) (err error) { + if db == nil || db.DB == nil { + return ErrDataBaseNotInitialized + } + + if _, err = db.Exec("DROP TABLE IF EXISTS cart"); err != nil { + return + } + _, err = db.Exec("DROP TABLE IF EXISTS products") + return +} diff --git a/internal/db/db_test.go b/internal/db/db_test.go new file mode 100644 index 0000000..e3cfcbe --- /dev/null +++ b/internal/db/db_test.go @@ -0,0 +1,28 @@ +package db + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOpen(t *testing.T) { + t.Run("invalid", func(t *testing.T) { + db, err := Open(`wtf`) + assert.Error(t, err) + assert.Nil(t, db) + t.Run("auto migrate/clean", func(t *testing.T) { + assert.Error(t, AutoMigrate(db)) + assert.Error(t, AutoClean(db)) + }) + }) + t.Run("valid", func(t *testing.T) { + db, err := Open(`host=/var/run/postgresql dbname=testing sslmode=disable`) + assert.NoError(t, err) + defer func() { assert.NoError(t, db.Close()) }() + t.Run("auto migrate/clean", func(t *testing.T) { + assert.NoError(t, AutoMigrate(db)) + assert.NoError(t, AutoClean(db)) + }) + }) +} diff --git a/internal/handler/handler.go b/internal/handler/handler.go new file mode 100644 index 0000000..f39e6f8 --- /dev/null +++ b/internal/handler/handler.go @@ -0,0 +1,94 @@ +package handler + +import ( + "math" + + "gitlab.com/toby3d/test/internal/model" + "gitlab.com/toby3d/test/internal/model/store" + "golang.org/x/net/context" +) + +// Handler представляет собой объект хендлеров с хранилищем данных +type Handler struct { + cartManager store.CartManager + productReader store.ProductReader +} + +// NewHandler создаёт хендлеры сервера с указанным хранилищем +func NewHandler(cartManager store.CartManager, productReader store.ProductReader) *Handler { + return &Handler{ + cartManager: cartManager, + productReader: productReader, + } +} + +// Add добавляет объект в хранилище (если его не существует) или обновляет количество существующего объекта +func (h *Handler) Add(ctx context.Context, req *model.AddRequest) (*model.Response, error) { + var resp model.Response + + if err := h.cartManager.Add(&model.Item{ + ProductId: req.GetProductId(), + Quanity: req.GetQuanity(), + }); err != nil { + resp.Description = err.Error() + return &resp, err + } + + resp.Ok = true + resp.Result = &model.Response_Item{Item: h.cartManager.GetById(req.GetProductId())} + return &resp, nil +} + +func (h *Handler) Get(ctx context.Context, req *model.GetRequest) (*model.Response, error) { + var resp model.Response + + var result model.Cart + count, items := h.cartManager.GetList() + result.Items = items + result.ItemsCount = int32(count) + for _, item := range items { + result.QuanityCount += item.GetQuanity() + product := h.productReader.GetById(item.GetProductId()) + result.TotalPrice += product.GetPrice() * float32(item.GetQuanity()) + } + // NOTE(toby3d): округляем до двух знаков после запятой + result.TotalPrice = float32(math.Round(float64(result.GetTotalPrice())*100) / 100) + + resp.Ok = true + resp.Result = &model.Response_Cart{Cart: &result} + return &resp, nil +} + +// Update обновляет конкретные товары в корзине. +func (h *Handler) Update(ctx context.Context, req *model.UpdateRequest) (*model.Response, error) { + var resp model.Response + + if err := h.cartManager.Update(&model.Item{ + ProductId: req.GetProductId(), + Quanity: req.GetQuanity(), + }); err != nil { + resp.Description = err.Error() + return &resp, err + } + + // NOTE(toby3d): Если количество отрицательно, то Result должен быть пустой как и в случае Remove + if req.GetQuanity() > 0 { + resp.Result = &model.Response_Item{Item: h.cartManager.GetById(req.GetProductId())} + } + + resp.Ok = true + return &resp, nil +} + +// Remove удаляет конкретные товары в корзине вне зависимости от их количества. +func (h *Handler) Remove(ctx context.Context, req *model.RemoveRequest) (*model.Response, error) { + var resp model.Response + + if err := h.cartManager.Delete(req.GetProductId()); err != nil { + resp.Description = err.Error() + return &resp, err + } + + resp.Ok = true + return &resp, nil +} diff --git a/internal/handler/handler_test.go b/internal/handler/handler_test.go new file mode 100644 index 0000000..7756449 --- /dev/null +++ b/internal/handler/handler_test.go @@ -0,0 +1,281 @@ +package handler + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "gitlab.com/toby3d/test/internal/model" + "gitlab.com/toby3d/test/internal/store" + "golang.org/x/net/context" +) + +var productsManager = store.InMemoryProductStore{Products: []*model.Product{ + &model.Product{Id: 24, Name: "Apple", Price: 4.99}, + &model.Product{Id: 42, Name: "Banana", Price: 2.49}, +}} + +func TestAdd(t *testing.T) { + t.Run("invalid", func(t *testing.T) { + handlerAdd := NewHandler(store.NewInMemoryCartStore(), &productsManager).Add + t.Run("empty", func(t *testing.T) { + resp, err := handlerAdd(context.TODO(), &model.AddRequest{}) + assert.Error(t, err) + assert.False(t, resp.GetOk()) + assert.NotEmpty(t, resp.GetDescription()) + assert.Nil(t, resp.GetResult()) + }) + t.Run("zero quanity", func(t *testing.T) { + resp, err := handlerAdd(context.TODO(), &model.AddRequest{ProductId: 42}) + assert.Error(t, err) + assert.False(t, resp.GetOk()) + assert.NotEmpty(t, resp.GetDescription()) + assert.Nil(t, resp.GetResult()) + }) + }) + t.Run("valid", func(t *testing.T) { + handlerAdd := NewHandler(store.NewInMemoryCartStore(), &productsManager).Add + itemOne := model.Item{ProductId: 42, Quanity: 5} + + resp, err := handlerAdd(context.TODO(), &model.AddRequest{ + ProductId: itemOne.GetProductId(), + Quanity: itemOne.GetQuanity(), + }) + assert.NoError(t, err) + assert.True(t, resp.GetOk()) + assert.Empty(t, resp.GetDescription()) + + assert.Equal(t, &itemOne, resp.GetItem()) + + t.Run("append to exist item", func(t *testing.T) { + resp, err = handlerAdd(context.TODO(), &model.AddRequest{ + ProductId: itemOne.GetProductId(), + Quanity: 24, + }) + assert.NoError(t, err) + assert.True(t, resp.GetOk()) + assert.Empty(t, resp.GetDescription()) + + assert.Equal(t, &model.Item{ + ProductId: itemOne.GetProductId(), + Quanity: itemOne.GetQuanity() + 24, + }, resp.GetItem()) + }) + }) +} + +func TestGet(t *testing.T) { + t.Run("empty", func(t *testing.T) { + s := store.NewInMemoryCartStore() + handlerGet := NewHandler(s, &productsManager).Get + + resp, err := handlerGet(context.TODO(), &model.GetRequest{}) + assert.NoError(t, err) + assert.True(t, resp.GetOk()) + assert.Empty(t, resp.GetDescription()) + + assert.Empty(t, resp.GetCart()) + }) + t.Run("have items", func(t *testing.T) { + for _, tc := range []struct { + name string + items []*model.Item + expCount int32 + expQuanity int32 + expPrice float32 + }{{ + name: "2 apples", + items: []*model.Item{ + &model.Item{ProductId: 24, Quanity: 2}, + }, + expCount: 1, + expQuanity: 2, + expPrice: 4.99 * 2, + }, { + name: "2 bananas", + items: []*model.Item{ + &model.Item{ProductId: 42, Quanity: 2}, + }, + expCount: 1, + expQuanity: 2, + expPrice: 2.49 * 2, + }, { + name: "2 bananas and apples", + items: []*model.Item{ + &model.Item{ProductId: 42, Quanity: 2}, + &model.Item{ProductId: 24, Quanity: 2}, + }, + expCount: 2, + expQuanity: 4, + expPrice: 2.49*2 + 4.99*2, + }, { + name: "5 bananas and 3 apples", + items: []*model.Item{ + &model.Item{ProductId: 42, Quanity: 5}, + &model.Item{ProductId: 24, Quanity: 3}, + }, + expCount: 2, + expQuanity: 8, + expPrice: 2.49*5 + 4.99*3, + }, { + name: "1+4 bananas and 3 apples", + items: []*model.Item{ + &model.Item{ProductId: 42, Quanity: 1}, + &model.Item{ProductId: 42, Quanity: 4}, + &model.Item{ProductId: 24, Quanity: 3}, + }, + expCount: 2, + expQuanity: 8, + expPrice: 2.49*5 + 4.99*3, + }, { + name: "2+3 bananas and 2+4 apples", + items: []*model.Item{ + &model.Item{ProductId: 42, Quanity: 2}, + &model.Item{ProductId: 24, Quanity: 2}, + &model.Item{ProductId: 42, Quanity: 3}, + &model.Item{ProductId: 24, Quanity: 4}, + }, + expCount: 2, + expQuanity: 11, + expPrice: 2.49*5 + 4.99*6, + }} { + tc := tc + t.Run(tc.name, func(t *testing.T) { + s := store.NewInMemoryCartStore() + for _, item := range tc.items { + assert.NoError(t, s.Add(item)) + } + handlerGet := NewHandler(s, &productsManager).Get + + resp, err := handlerGet(context.TODO(), &model.GetRequest{}) + assert.NoError(t, err) + assert.True(t, resp.GetOk()) + assert.Empty(t, resp.GetDescription()) + + result := resp.GetCart() + assert.Equal(t, tc.expCount, result.GetItemsCount()) + assert.Equal(t, tc.expQuanity, result.GetQuanityCount()) + assert.Equal(t, tc.expPrice, result.GetTotalPrice()) + }) + } + }) +} + +func TestUpdate(t *testing.T) { + t.Run("invalid", func(t *testing.T) { + handlerUpdate := NewHandler(store.NewInMemoryCartStore(), &productsManager).Update + t.Run("empty", func(t *testing.T) { + resp, err := handlerUpdate(context.TODO(), &model.UpdateRequest{}) + assert.Error(t, err) + assert.False(t, resp.GetOk()) + assert.NotEmpty(t, resp.GetDescription()) + assert.Nil(t, resp.GetResult()) + }) + t.Run("not exists", func(t *testing.T) { + resp, err := handlerUpdate(context.TODO(), &model.UpdateRequest{ProductId: 42}) + assert.Error(t, err) + assert.False(t, resp.GetOk()) + assert.NotEmpty(t, resp.GetDescription()) + assert.Nil(t, resp.GetResult()) + }) + }) + t.Run("valid", func(t *testing.T) { + t.Run("create", func(t *testing.T) { + s := store.NewInMemoryCartStore() + itemOne := model.Item{ProductId: 42, Quanity: 5} + handlerUpdate := NewHandler(s, &productsManager).Update + + resp, err := handlerUpdate( + context.TODO(), + &model.UpdateRequest{ProductId: itemOne.GetProductId(), Quanity: itemOne.GetQuanity()}, + ) + assert.NoError(t, err) + assert.True(t, resp.GetOk()) + assert.Empty(t, resp.GetDescription()) + + assert.Equal(t, &itemOne, resp.GetItem()) + }) + t.Run("update", func(t *testing.T) { + s := store.NewInMemoryCartStore() + itemOne := model.Item{ProductId: 42, Quanity: 5} + assert.NoError(t, s.Add(&itemOne)) + handlerUpdate := NewHandler(s, &productsManager).Update + + resp, err := handlerUpdate( + context.TODO(), + &model.UpdateRequest{ProductId: itemOne.GetProductId(), Quanity: 2}, + ) + assert.NoError(t, err) + assert.True(t, resp.GetOk()) + assert.Empty(t, resp.GetDescription()) + + assert.Equal(t, &model.Item{ + ProductId: itemOne.GetProductId(), + Quanity: 2, + }, resp.GetItem()) + }) + t.Run("delete", func(t *testing.T) { + s := store.NewInMemoryCartStore() + itemOne := model.Item{ProductId: 42, Quanity: 5} + assert.NoError(t, s.Add(&itemOne)) + handlerUpdate := NewHandler(s, &productsManager).Update + + resp, err := handlerUpdate( + context.TODO(), + &model.UpdateRequest{ProductId: itemOne.GetProductId(), Quanity: 0}, + ) + assert.NoError(t, err) + assert.True(t, resp.GetOk()) + assert.Empty(t, resp.GetDescription()) + + assert.Empty(t, resp.GetResult()) + }) + }) +} +func TestRemove(t *testing.T) { + t.Run("invalid", func(t *testing.T) { + itemOne := model.Item{ProductId: 42, Quanity: 5} + + s := store.NewInMemoryCartStore() + assert.NoError(t, s.Add(&itemOne)) + handlerRemove := NewHandler(s, &productsManager).Remove + + t.Run("empty", func(t *testing.T) { + resp, err := handlerRemove(context.TODO(), &model.RemoveRequest{}) + assert.Error(t, err) + assert.False(t, resp.GetOk()) + assert.NotEmpty(t, resp.GetDescription()) + assert.Nil(t, resp.GetResult()) + count, list := s.GetList() + assert.Contains(t, list, &itemOne) + assert.Equal(t, 1, count) + }) + t.Run("not exist", func(t *testing.T) { + resp, err := handlerRemove(context.TODO(), &model.RemoveRequest{ProductId: 24}) + assert.Error(t, err) + assert.False(t, resp.GetOk()) + assert.NotEmpty(t, resp.GetDescription()) + assert.Nil(t, resp.GetResult()) + count, list := s.GetList() + assert.Contains(t, list, &itemOne) + assert.Equal(t, 1, count) + }) + }) + t.Run("valid", func(t *testing.T) { + itemOne := model.Item{ProductId: 42, Quanity: 5} + + s := store.NewInMemoryCartStore() + assert.NoError(t, s.Add(&itemOne)) + handlerRemove := NewHandler(s, &productsManager).Remove + + resp, err := handlerRemove( + context.TODO(), &model.RemoveRequest{ProductId: itemOne.GetProductId()}, + ) + assert.NoError(t, err) + assert.True(t, resp.GetOk()) + assert.Empty(t, resp.GetDescription()) + assert.Empty(t, resp.GetResult()) + count, list := s.GetList() + assert.NotContains(t, list, &itemOne) + assert.Zero(t, count) + }) +} diff --git a/internal/model/model.pb.go b/internal/model/model.pb.go new file mode 100644 index 0000000..2ecba0e --- /dev/null +++ b/internal/model/model.pb.go @@ -0,0 +1,681 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: model.proto + +package model + +import ( + context "context" + fmt "fmt" + proto "github.com/golang/protobuf/proto" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +type Product struct { + Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Price float32 `protobuf:"fixed32,3,opt,name=price,proto3" json:"price,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Product) Reset() { *m = Product{} } +func (m *Product) String() string { return proto.CompactTextString(m) } +func (*Product) ProtoMessage() {} +func (*Product) Descriptor() ([]byte, []int) { + return fileDescriptor_4c16552f9fdb66d8, []int{0} +} + +func (m *Product) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Product.Unmarshal(m, b) +} +func (m *Product) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Product.Marshal(b, m, deterministic) +} +func (m *Product) XXX_Merge(src proto.Message) { + xxx_messageInfo_Product.Merge(m, src) +} +func (m *Product) XXX_Size() int { + return xxx_messageInfo_Product.Size(m) +} +func (m *Product) XXX_DiscardUnknown() { + xxx_messageInfo_Product.DiscardUnknown(m) +} + +var xxx_messageInfo_Product proto.InternalMessageInfo + +func (m *Product) GetId() uint64 { + if m != nil { + return m.Id + } + return 0 +} + +func (m *Product) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +func (m *Product) GetPrice() float32 { + if m != nil { + return m.Price + } + return 0 +} + +type Item struct { + ProductId uint64 `protobuf:"varint,1,opt,name=product_id,json=productId,proto3" json:"product_id,omitempty"` + Quanity int32 `protobuf:"varint,2,opt,name=quanity,proto3" json:"quanity,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Item) Reset() { *m = Item{} } +func (m *Item) String() string { return proto.CompactTextString(m) } +func (*Item) ProtoMessage() {} +func (*Item) Descriptor() ([]byte, []int) { + return fileDescriptor_4c16552f9fdb66d8, []int{1} +} + +func (m *Item) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Item.Unmarshal(m, b) +} +func (m *Item) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Item.Marshal(b, m, deterministic) +} +func (m *Item) XXX_Merge(src proto.Message) { + xxx_messageInfo_Item.Merge(m, src) +} +func (m *Item) XXX_Size() int { + return xxx_messageInfo_Item.Size(m) +} +func (m *Item) XXX_DiscardUnknown() { + xxx_messageInfo_Item.DiscardUnknown(m) +} + +var xxx_messageInfo_Item proto.InternalMessageInfo + +func (m *Item) GetProductId() uint64 { + if m != nil { + return m.ProductId + } + return 0 +} + +func (m *Item) GetQuanity() int32 { + if m != nil { + return m.Quanity + } + return 0 +} + +type Cart struct { + Items []*Item `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + TotalPrice float32 `protobuf:"fixed32,2,opt,name=total_price,json=totalPrice,proto3" json:"total_price,omitempty"` + ItemsCount int32 `protobuf:"varint,3,opt,name=items_count,json=itemsCount,proto3" json:"items_count,omitempty"` + QuanityCount int32 `protobuf:"varint,4,opt,name=quanity_count,json=quanityCount,proto3" json:"quanity_count,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Cart) Reset() { *m = Cart{} } +func (m *Cart) String() string { return proto.CompactTextString(m) } +func (*Cart) ProtoMessage() {} +func (*Cart) Descriptor() ([]byte, []int) { + return fileDescriptor_4c16552f9fdb66d8, []int{2} +} + +func (m *Cart) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Cart.Unmarshal(m, b) +} +func (m *Cart) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Cart.Marshal(b, m, deterministic) +} +func (m *Cart) XXX_Merge(src proto.Message) { + xxx_messageInfo_Cart.Merge(m, src) +} +func (m *Cart) XXX_Size() int { + return xxx_messageInfo_Cart.Size(m) +} +func (m *Cart) XXX_DiscardUnknown() { + xxx_messageInfo_Cart.DiscardUnknown(m) +} + +var xxx_messageInfo_Cart proto.InternalMessageInfo + +func (m *Cart) GetItems() []*Item { + if m != nil { + return m.Items + } + return nil +} + +func (m *Cart) GetTotalPrice() float32 { + if m != nil { + return m.TotalPrice + } + return 0 +} + +func (m *Cart) GetItemsCount() int32 { + if m != nil { + return m.ItemsCount + } + return 0 +} + +func (m *Cart) GetQuanityCount() int32 { + if m != nil { + return m.QuanityCount + } + return 0 +} + +type AddRequest struct { + ProductId uint64 `protobuf:"varint,1,opt,name=product_id,json=productId,proto3" json:"product_id,omitempty"` + Quanity int32 `protobuf:"varint,2,opt,name=quanity,proto3" json:"quanity,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *AddRequest) Reset() { *m = AddRequest{} } +func (m *AddRequest) String() string { return proto.CompactTextString(m) } +func (*AddRequest) ProtoMessage() {} +func (*AddRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_4c16552f9fdb66d8, []int{3} +} + +func (m *AddRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_AddRequest.Unmarshal(m, b) +} +func (m *AddRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_AddRequest.Marshal(b, m, deterministic) +} +func (m *AddRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_AddRequest.Merge(m, src) +} +func (m *AddRequest) XXX_Size() int { + return xxx_messageInfo_AddRequest.Size(m) +} +func (m *AddRequest) XXX_DiscardUnknown() { + xxx_messageInfo_AddRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_AddRequest proto.InternalMessageInfo + +func (m *AddRequest) GetProductId() uint64 { + if m != nil { + return m.ProductId + } + return 0 +} + +func (m *AddRequest) GetQuanity() int32 { + if m != nil { + return m.Quanity + } + return 0 +} + +type GetRequest struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetRequest) Reset() { *m = GetRequest{} } +func (m *GetRequest) String() string { return proto.CompactTextString(m) } +func (*GetRequest) ProtoMessage() {} +func (*GetRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_4c16552f9fdb66d8, []int{4} +} + +func (m *GetRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetRequest.Unmarshal(m, b) +} +func (m *GetRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetRequest.Marshal(b, m, deterministic) +} +func (m *GetRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetRequest.Merge(m, src) +} +func (m *GetRequest) XXX_Size() int { + return xxx_messageInfo_GetRequest.Size(m) +} +func (m *GetRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetRequest proto.InternalMessageInfo + +type UpdateRequest struct { + ProductId uint64 `protobuf:"varint,1,opt,name=product_id,json=productId,proto3" json:"product_id,omitempty"` + Quanity int32 `protobuf:"varint,2,opt,name=quanity,proto3" json:"quanity,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *UpdateRequest) Reset() { *m = UpdateRequest{} } +func (m *UpdateRequest) String() string { return proto.CompactTextString(m) } +func (*UpdateRequest) ProtoMessage() {} +func (*UpdateRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_4c16552f9fdb66d8, []int{5} +} + +func (m *UpdateRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_UpdateRequest.Unmarshal(m, b) +} +func (m *UpdateRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_UpdateRequest.Marshal(b, m, deterministic) +} +func (m *UpdateRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_UpdateRequest.Merge(m, src) +} +func (m *UpdateRequest) XXX_Size() int { + return xxx_messageInfo_UpdateRequest.Size(m) +} +func (m *UpdateRequest) XXX_DiscardUnknown() { + xxx_messageInfo_UpdateRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_UpdateRequest proto.InternalMessageInfo + +func (m *UpdateRequest) GetProductId() uint64 { + if m != nil { + return m.ProductId + } + return 0 +} + +func (m *UpdateRequest) GetQuanity() int32 { + if m != nil { + return m.Quanity + } + return 0 +} + +type RemoveRequest struct { + ProductId uint64 `protobuf:"varint,1,opt,name=product_id,json=productId,proto3" json:"product_id,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *RemoveRequest) Reset() { *m = RemoveRequest{} } +func (m *RemoveRequest) String() string { return proto.CompactTextString(m) } +func (*RemoveRequest) ProtoMessage() {} +func (*RemoveRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_4c16552f9fdb66d8, []int{6} +} + +func (m *RemoveRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_RemoveRequest.Unmarshal(m, b) +} +func (m *RemoveRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_RemoveRequest.Marshal(b, m, deterministic) +} +func (m *RemoveRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_RemoveRequest.Merge(m, src) +} +func (m *RemoveRequest) XXX_Size() int { + return xxx_messageInfo_RemoveRequest.Size(m) +} +func (m *RemoveRequest) XXX_DiscardUnknown() { + xxx_messageInfo_RemoveRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_RemoveRequest proto.InternalMessageInfo + +func (m *RemoveRequest) GetProductId() uint64 { + if m != nil { + return m.ProductId + } + return 0 +} + +type Response struct { + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + // Types that are valid to be assigned to Result: + // *Response_Item + // *Response_Cart + Result isResponse_Result `protobuf_oneof:"result"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Response) Reset() { *m = Response{} } +func (m *Response) String() string { return proto.CompactTextString(m) } +func (*Response) ProtoMessage() {} +func (*Response) Descriptor() ([]byte, []int) { + return fileDescriptor_4c16552f9fdb66d8, []int{7} +} + +func (m *Response) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Response.Unmarshal(m, b) +} +func (m *Response) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Response.Marshal(b, m, deterministic) +} +func (m *Response) XXX_Merge(src proto.Message) { + xxx_messageInfo_Response.Merge(m, src) +} +func (m *Response) XXX_Size() int { + return xxx_messageInfo_Response.Size(m) +} +func (m *Response) XXX_DiscardUnknown() { + xxx_messageInfo_Response.DiscardUnknown(m) +} + +var xxx_messageInfo_Response proto.InternalMessageInfo + +func (m *Response) GetOk() bool { + if m != nil { + return m.Ok + } + return false +} + +func (m *Response) GetDescription() string { + if m != nil { + return m.Description + } + return "" +} + +type isResponse_Result interface { + isResponse_Result() +} + +type Response_Item struct { + Item *Item `protobuf:"bytes,3,opt,name=item,proto3,oneof"` +} + +type Response_Cart struct { + Cart *Cart `protobuf:"bytes,4,opt,name=cart,proto3,oneof"` +} + +func (*Response_Item) isResponse_Result() {} + +func (*Response_Cart) isResponse_Result() {} + +func (m *Response) GetResult() isResponse_Result { + if m != nil { + return m.Result + } + return nil +} + +func (m *Response) GetItem() *Item { + if x, ok := m.GetResult().(*Response_Item); ok { + return x.Item + } + return nil +} + +func (m *Response) GetCart() *Cart { + if x, ok := m.GetResult().(*Response_Cart); ok { + return x.Cart + } + return nil +} + +// XXX_OneofWrappers is for the internal use of the proto package. +func (*Response) XXX_OneofWrappers() []interface{} { + return []interface{}{ + (*Response_Item)(nil), + (*Response_Cart)(nil), + } +} + +func init() { + proto.RegisterType((*Product)(nil), "model.Product") + proto.RegisterType((*Item)(nil), "model.Item") + proto.RegisterType((*Cart)(nil), "model.Cart") + proto.RegisterType((*AddRequest)(nil), "model.AddRequest") + proto.RegisterType((*GetRequest)(nil), "model.GetRequest") + proto.RegisterType((*UpdateRequest)(nil), "model.UpdateRequest") + proto.RegisterType((*RemoveRequest)(nil), "model.RemoveRequest") + proto.RegisterType((*Response)(nil), "model.Response") +} + +func init() { proto.RegisterFile("model.proto", fileDescriptor_4c16552f9fdb66d8) } + +var fileDescriptor_4c16552f9fdb66d8 = []byte{ + // 402 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x93, 0xcf, 0x4a, 0xc3, 0x40, + 0x10, 0xc6, 0xbb, 0x69, 0xd2, 0xa6, 0x93, 0x56, 0x71, 0xe9, 0x21, 0x08, 0x62, 0x1a, 0x2f, 0x01, + 0xa1, 0x60, 0x7d, 0x00, 0xa9, 0x45, 0xda, 0xde, 0xca, 0x8a, 0xe7, 0x12, 0xb3, 0x0b, 0x86, 0x36, + 0xd9, 0x34, 0xd9, 0x08, 0xbe, 0x83, 0x07, 0x9f, 0xcb, 0xa7, 0x92, 0xfd, 0xd3, 0xd6, 0x42, 0x05, + 0xd1, 0x5b, 0xe6, 0x9b, 0xdf, 0x7c, 0x99, 0x7c, 0x43, 0xc0, 0xcb, 0x38, 0x65, 0xeb, 0x61, 0x51, + 0x72, 0xc1, 0xb1, 0xa3, 0x8a, 0x70, 0x02, 0xed, 0x45, 0xc9, 0x69, 0x9d, 0x08, 0x7c, 0x02, 0x56, + 0x4a, 0x7d, 0x14, 0xa0, 0xc8, 0x26, 0x56, 0x4a, 0x31, 0x06, 0x3b, 0x8f, 0x33, 0xe6, 0x5b, 0x01, + 0x8a, 0x3a, 0x44, 0x3d, 0xe3, 0x3e, 0x38, 0x45, 0x99, 0x26, 0xcc, 0x6f, 0x06, 0x28, 0xb2, 0x88, + 0x2e, 0xc2, 0x3b, 0xb0, 0xe7, 0x82, 0x65, 0xf8, 0x02, 0xa0, 0xd0, 0x66, 0xcb, 0x9d, 0x53, 0xc7, + 0x28, 0x73, 0x8a, 0x7d, 0x68, 0x6f, 0xea, 0x38, 0x4f, 0xc5, 0x9b, 0xf2, 0x74, 0xc8, 0xb6, 0x0c, + 0x3f, 0x10, 0xd8, 0x93, 0xb8, 0x14, 0x78, 0x00, 0x4e, 0x2a, 0x58, 0x56, 0xf9, 0x28, 0x68, 0x46, + 0xde, 0xc8, 0x1b, 0xea, 0x95, 0xa5, 0x3b, 0xd1, 0x1d, 0x7c, 0x09, 0x9e, 0xe0, 0x22, 0x5e, 0x2f, + 0xf5, 0x22, 0x96, 0x5a, 0x04, 0x94, 0xb4, 0x90, 0x8a, 0x04, 0x14, 0xb9, 0x4c, 0x78, 0x9d, 0x0b, + 0xb5, 0xa9, 0x43, 0x40, 0x49, 0x13, 0xa9, 0xe0, 0x2b, 0xe8, 0x99, 0x17, 0x1b, 0xc4, 0x56, 0x48, + 0xd7, 0x88, 0x0a, 0x0a, 0x1f, 0x00, 0xc6, 0x94, 0x12, 0xb6, 0xa9, 0x59, 0x25, 0xfe, 0xfe, 0x65, + 0x5d, 0x80, 0x29, 0x13, 0xc6, 0x26, 0x9c, 0x41, 0xef, 0xa9, 0xa0, 0xb1, 0x60, 0xff, 0xf6, 0x1d, + 0x42, 0x8f, 0xb0, 0x8c, 0xbf, 0xfe, 0xd2, 0x29, 0x7c, 0x47, 0xe0, 0x12, 0x56, 0x15, 0x3c, 0xaf, + 0x98, 0xbc, 0x34, 0x5f, 0x29, 0xc6, 0x25, 0x16, 0x5f, 0xe1, 0x00, 0x3c, 0xca, 0xaa, 0xa4, 0x4c, + 0x0b, 0x91, 0xf2, 0xdc, 0x1c, 0xfc, 0xbb, 0x84, 0x07, 0x60, 0xcb, 0x00, 0x55, 0x98, 0x87, 0x67, + 0x99, 0x35, 0x88, 0x6a, 0x49, 0x24, 0x89, 0x4b, 0x1d, 0xe6, 0x1e, 0x91, 0x57, 0x95, 0x88, 0x6c, + 0xdd, 0xbb, 0xd0, 0x2a, 0x59, 0x55, 0xaf, 0xc5, 0xe8, 0x13, 0x81, 0xfb, 0xf8, 0xc2, 0x0b, 0x75, + 0xf4, 0x6b, 0x68, 0x8e, 0x29, 0xc5, 0x67, 0x66, 0x64, 0x1f, 0xfb, 0xf9, 0xa9, 0x91, 0xb6, 0x9b, + 0x87, 0x0d, 0x09, 0x4f, 0x99, 0xd8, 0xc1, 0xfb, 0x70, 0x8f, 0xc1, 0x37, 0xd0, 0xd2, 0x79, 0xe3, + 0xbe, 0x69, 0x1e, 0xc4, 0xff, 0xc3, 0x88, 0x0e, 0x76, 0x37, 0x72, 0x90, 0xf3, 0x91, 0x91, 0xe7, + 0x96, 0xfa, 0xa3, 0x6e, 0xbf, 0x02, 0x00, 0x00, 0xff, 0xff, 0xb4, 0x9b, 0xe9, 0x0b, 0x60, 0x03, + 0x00, 0x00, +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// ShopCartClient is the client API for ShopCart service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type ShopCartClient interface { + Add(ctx context.Context, in *AddRequest, opts ...grpc.CallOption) (*Response, error) + Get(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*Response, error) + Update(ctx context.Context, in *UpdateRequest, opts ...grpc.CallOption) (*Response, error) + Remove(ctx context.Context, in *RemoveRequest, opts ...grpc.CallOption) (*Response, error) +} + +type shopCartClient struct { + cc *grpc.ClientConn +} + +func NewShopCartClient(cc *grpc.ClientConn) ShopCartClient { + return &shopCartClient{cc} +} + +func (c *shopCartClient) Add(ctx context.Context, in *AddRequest, opts ...grpc.CallOption) (*Response, error) { + out := new(Response) + err := c.cc.Invoke(ctx, "/model.ShopCart/Add", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *shopCartClient) Get(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*Response, error) { + out := new(Response) + err := c.cc.Invoke(ctx, "/model.ShopCart/Get", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *shopCartClient) Update(ctx context.Context, in *UpdateRequest, opts ...grpc.CallOption) (*Response, error) { + out := new(Response) + err := c.cc.Invoke(ctx, "/model.ShopCart/Update", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *shopCartClient) Remove(ctx context.Context, in *RemoveRequest, opts ...grpc.CallOption) (*Response, error) { + out := new(Response) + err := c.cc.Invoke(ctx, "/model.ShopCart/Remove", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ShopCartServer is the server API for ShopCart service. +type ShopCartServer interface { + Add(context.Context, *AddRequest) (*Response, error) + Get(context.Context, *GetRequest) (*Response, error) + Update(context.Context, *UpdateRequest) (*Response, error) + Remove(context.Context, *RemoveRequest) (*Response, error) +} + +// UnimplementedShopCartServer can be embedded to have forward compatible implementations. +type UnimplementedShopCartServer struct { +} + +func (*UnimplementedShopCartServer) Add(ctx context.Context, req *AddRequest) (*Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method Add not implemented") +} +func (*UnimplementedShopCartServer) Get(ctx context.Context, req *GetRequest) (*Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method Get not implemented") +} +func (*UnimplementedShopCartServer) Update(ctx context.Context, req *UpdateRequest) (*Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method Update not implemented") +} +func (*UnimplementedShopCartServer) Remove(ctx context.Context, req *RemoveRequest) (*Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method Remove not implemented") +} + +func RegisterShopCartServer(s *grpc.Server, srv ShopCartServer) { + s.RegisterService(&_ShopCart_serviceDesc, srv) +} + +func _ShopCart_Add_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AddRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ShopCartServer).Add(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/model.ShopCart/Add", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ShopCartServer).Add(ctx, req.(*AddRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ShopCart_Get_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ShopCartServer).Get(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/model.ShopCart/Get", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ShopCartServer).Get(ctx, req.(*GetRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ShopCart_Update_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ShopCartServer).Update(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/model.ShopCart/Update", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ShopCartServer).Update(ctx, req.(*UpdateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ShopCart_Remove_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RemoveRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ShopCartServer).Remove(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/model.ShopCart/Remove", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ShopCartServer).Remove(ctx, req.(*RemoveRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _ShopCart_serviceDesc = grpc.ServiceDesc{ + ServiceName: "model.ShopCart", + HandlerType: (*ShopCartServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Add", + Handler: _ShopCart_Add_Handler, + }, + { + MethodName: "Get", + Handler: _ShopCart_Get_Handler, + }, + { + MethodName: "Update", + Handler: _ShopCart_Update_Handler, + }, + { + MethodName: "Remove", + Handler: _ShopCart_Remove_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "model.proto", +} diff --git a/internal/model/model.proto b/internal/model/model.proto new file mode 100644 index 0000000..bb15b8f --- /dev/null +++ b/internal/model/model.proto @@ -0,0 +1,52 @@ +syntax = "proto3"; +package model; + +service ShopCart { // Сервис корзины товаров + rpc Add (AddRequest) returns (Response) {} // Добавление товара в корзину + rpc Get (GetRequest) returns (Response) {} // Get получение текущего состояния корзины + rpc Update (UpdateRequest) returns (Response) {} // Обновление товара в корзине + rpc Remove (RemoveRequest) returns (Response) {} // Удаление товара из корзины +} + +message Product { // Модель продукта доступного для добавления в корзину + uint64 id = 1; // ID продукта + string name = 2; // Имя/название продукта + float price = 3; // Цена за единицу продукта +} + +message Item { // Модель объекта корзины + uint64 product_id = 1; // ID продукта + int32 quanity = 2; // Количество объекта в корзине +} + +message Cart { + repeated Item items = 1; // Список всех товаров в корзине + float total_price = 2; // Общая сумма за все товары + int32 items_count = 3; // Число уникальных товаров в корзине + int32 quanity_count = 4; // Общая сумма количества всех товаров +} + +message AddRequest { + uint64 product_id = 1; // ID продукта + int32 quanity = 2; // Добавляемое количество продуктов +} + +message GetRequest {} // Без каких-либо специальных параметров + +message UpdateRequest { + uint64 product_id = 1; // ID продукта + int32 quanity = 2; // Новое количество продуктов +} + +message RemoveRequest { + uint64 product_id = 1; // ID продукта +} + +message Response { // Модель ответа + bool ok = 1; // Тип ответа: успешный или нет + string description = 2; // Описание ошибки в случае невалидного ответа + oneof result { // Результаты запросов + Item item = 3; + Cart cart = 4; + } +} \ No newline at end of file diff --git a/internal/model/model_mock.go b/internal/model/model_mock.go new file mode 100644 index 0000000..e525295 --- /dev/null +++ b/internal/model/model_mock.go @@ -0,0 +1,233 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./../../internal/model/model.pb.go + +// Package model is a generated GoMock package. +package model + +import ( + context "context" + gomock "github.com/golang/mock/gomock" + grpc "google.golang.org/grpc" + reflect "reflect" +) + +// MockisResponse_Result is a mock of isResponse_Result interface +type MockisResponse_Result struct { + ctrl *gomock.Controller + recorder *MockisResponse_ResultMockRecorder +} + +// MockisResponse_ResultMockRecorder is the mock recorder for MockisResponse_Result +type MockisResponse_ResultMockRecorder struct { + mock *MockisResponse_Result +} + +// NewMockisResponse_Result creates a new mock instance +func NewMockisResponse_Result(ctrl *gomock.Controller) *MockisResponse_Result { + mock := &MockisResponse_Result{ctrl: ctrl} + mock.recorder = &MockisResponse_ResultMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockisResponse_Result) EXPECT() *MockisResponse_ResultMockRecorder { + return m.recorder +} + +// isResponse_Result mocks base method +func (m *MockisResponse_Result) isResponse_Result() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "isResponse_Result") +} + +// isResponse_Result indicates an expected call of isResponse_Result +func (mr *MockisResponse_ResultMockRecorder) isResponse_Result() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "isResponse_Result", reflect.TypeOf((*MockisResponse_Result)(nil).isResponse_Result)) +} + +// MockShopCartClient is a mock of ShopCartClient interface +type MockShopCartClient struct { + ctrl *gomock.Controller + recorder *MockShopCartClientMockRecorder +} + +// MockShopCartClientMockRecorder is the mock recorder for MockShopCartClient +type MockShopCartClientMockRecorder struct { + mock *MockShopCartClient +} + +// NewMockShopCartClient creates a new mock instance +func NewMockShopCartClient(ctrl *gomock.Controller) *MockShopCartClient { + mock := &MockShopCartClient{ctrl: ctrl} + mock.recorder = &MockShopCartClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockShopCartClient) EXPECT() *MockShopCartClientMockRecorder { + return m.recorder +} + +// Add mocks base method +func (m *MockShopCartClient) Add(ctx context.Context, in *AddRequest, opts ...grpc.CallOption) (*Response, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Add", varargs...) + ret0, _ := ret[0].(*Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Add indicates an expected call of Add +func (mr *MockShopCartClientMockRecorder) Add(ctx, in interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockShopCartClient)(nil).Add), varargs...) +} + +// Get mocks base method +func (m *MockShopCartClient) Get(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*Response, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Get", varargs...) + ret0, _ := ret[0].(*Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get +func (mr *MockShopCartClientMockRecorder) Get(ctx, in interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockShopCartClient)(nil).Get), varargs...) +} + +// Update mocks base method +func (m *MockShopCartClient) Update(ctx context.Context, in *UpdateRequest, opts ...grpc.CallOption) (*Response, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Update", varargs...) + ret0, _ := ret[0].(*Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update +func (mr *MockShopCartClientMockRecorder) Update(ctx, in interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockShopCartClient)(nil).Update), varargs...) +} + +// Remove mocks base method +func (m *MockShopCartClient) Remove(ctx context.Context, in *RemoveRequest, opts ...grpc.CallOption) (*Response, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Remove", varargs...) + ret0, _ := ret[0].(*Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Remove indicates an expected call of Remove +func (mr *MockShopCartClientMockRecorder) Remove(ctx, in interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockShopCartClient)(nil).Remove), varargs...) +} + +// MockShopCartServer is a mock of ShopCartServer interface +type MockShopCartServer struct { + ctrl *gomock.Controller + recorder *MockShopCartServerMockRecorder +} + +// MockShopCartServerMockRecorder is the mock recorder for MockShopCartServer +type MockShopCartServerMockRecorder struct { + mock *MockShopCartServer +} + +// NewMockShopCartServer creates a new mock instance +func NewMockShopCartServer(ctrl *gomock.Controller) *MockShopCartServer { + mock := &MockShopCartServer{ctrl: ctrl} + mock.recorder = &MockShopCartServerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockShopCartServer) EXPECT() *MockShopCartServerMockRecorder { + return m.recorder +} + +// Add mocks base method +func (m *MockShopCartServer) Add(arg0 context.Context, arg1 *AddRequest) (*Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Add", arg0, arg1) + ret0, _ := ret[0].(*Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Add indicates an expected call of Add +func (mr *MockShopCartServerMockRecorder) Add(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockShopCartServer)(nil).Add), arg0, arg1) +} + +// Get mocks base method +func (m *MockShopCartServer) Get(arg0 context.Context, arg1 *GetRequest) (*Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", arg0, arg1) + ret0, _ := ret[0].(*Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get +func (mr *MockShopCartServerMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockShopCartServer)(nil).Get), arg0, arg1) +} + +// Update mocks base method +func (m *MockShopCartServer) Update(arg0 context.Context, arg1 *UpdateRequest) (*Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", arg0, arg1) + ret0, _ := ret[0].(*Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update +func (mr *MockShopCartServerMockRecorder) Update(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockShopCartServer)(nil).Update), arg0, arg1) +} + +// Remove mocks base method +func (m *MockShopCartServer) Remove(arg0 context.Context, arg1 *RemoveRequest) (*Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Remove", arg0, arg1) + ret0, _ := ret[0].(*Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Remove indicates an expected call of Remove +func (mr *MockShopCartServerMockRecorder) Remove(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockShopCartServer)(nil).Remove), arg0, arg1) +} diff --git a/internal/model/store/store.go b/internal/model/store/store.go new file mode 100644 index 0000000..d098b89 --- /dev/null +++ b/internal/model/store/store.go @@ -0,0 +1,17 @@ +package store + +import "gitlab.com/toby3d/test/internal/model" + +type ( + CartManager interface { + Add(item *model.Item) error + Delete(id uint64) error + GetById(id uint64) *model.Item + GetList() (int, []*model.Item) + Update(item *model.Item) error + } + + ProductReader interface { + GetById(id uint64) *model.Product + } +) diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..27351a0 --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,54 @@ +package server + +import ( + "net" + + "gitlab.com/toby3d/test/internal/model" + "golang.org/x/xerrors" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +// Server представляет собой объект gRPC сервера +type Server struct { + listener net.Listener + server *grpc.Server +} + +// ErrServerNotInitialized описывает ошибку инициализации сервера +var ErrServerNotInitialized = xerrors.New("server is not initialized") + +// NewServer создаёт новое TCP соединение по указанному адресу с указанным набором хендлеров +func NewServer(addr string, handlers model.ShopCartServer) (*Server, error) { + s := Server{server: grpc.NewServer()} + + var err error + if s.listener, err = net.Listen("tcp", addr); err != nil { + return nil, err + } + + model.RegisterShopCartServer(s.server, handlers) + reflection.Register(s.server) + + return &s, nil +} + +// Start запускает gRPC сервер. +// Возвращает ошибку если была предпринята попытка запуска без предварительной инициализации сервера. +func (s *Server) Start() error { + if s == nil || s.server == nil || s.listener == nil { + return ErrServerNotInitialized + } + return s.server.Serve(s.listener) +} + +// Stop останавливает gRPC сервер. +// Возвращает ошибку если была предпринята попытка остановки без предварительной инициализации сервера. +func (s *Server) Stop() error { + if s == nil || s.server == nil { + return ErrServerNotInitialized + } + + s.server.Stop() + return nil +} diff --git a/internal/server/server_test.go b/internal/server/server_test.go new file mode 100644 index 0000000..27c30cb --- /dev/null +++ b/internal/server/server_test.go @@ -0,0 +1,33 @@ +package server + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "gitlab.com/toby3d/test/internal/handler" + "gitlab.com/toby3d/test/internal/store" +) + +func TestNewServer(t *testing.T) { + t.Run("invalid", func(t *testing.T) { + srv, err := NewServer("wtf", nil) + assert.Error(t, err) + t.Run("start/stop", func(t *testing.T) { + assert.Error(t, srv.Start()) + assert.Error(t, srv.Stop()) + }) + }) + t.Run("valid", func(t *testing.T) { + srv, err := NewServer(":2368", handler.NewHandler( + store.NewInMemoryCartStore(), store.NewInMemoryProductStore(), + )) + assert.NoError(t, err) + assert.NotNil(t, srv) + t.Run("start/stop", func(t *testing.T) { + go func() { assert.NoError(t, srv.Start()) }() + time.Sleep(100 * time.Millisecond) + assert.NoError(t, srv.Stop()) + }) + }) +} diff --git a/internal/store/in_memory_store.go b/internal/store/in_memory_store.go new file mode 100644 index 0000000..a29617b --- /dev/null +++ b/internal/store/in_memory_store.go @@ -0,0 +1,147 @@ +package store + +import ( + "sort" + "sync" + + "gitlab.com/toby3d/test/internal/model" + "golang.org/x/xerrors" +) + +type ( + // InMemoryProductStore представляет собой объект хранилища продуктов доступный только для чтения + InMemoryProductStore struct { + mutex sync.RWMutex + Products []*model.Product + } + + // InMemoryCartStore представляет собой простой менеджер объектов корзины. + InMemoryCartStore struct { + mutex sync.RWMutex + items []*model.Item + } +) + +var ( + ErrNoProductId = xerrors.New("product_id not provided") + ErrZeroQuanity = xerrors.New("item quanity is zero") + ErrNotExist = xerrors.New("item not exists or already removed") +) + +// NewInMemoryProductStore создаёт новый менеджер продуктов +func NewInMemoryProductStore() *InMemoryProductStore { + return &InMemoryProductStore{mutex: sync.RWMutex{}} +} + +// GetById возвращает информацию о продукте по его ID, если он существует +func (imps *InMemoryProductStore) GetById(id uint64) *model.Product { + for _, product := range imps.Products { + if product.GetId() != id { + continue + } + return product + } + return nil +} + +// NewInMemoryCartStore создаёт новый менеджер объектов корзины +func NewInMemoryCartStore() *InMemoryCartStore { return &InMemoryCartStore{mutex: sync.RWMutex{}} } + +// Add добавляет новый объект в корзину. +// +// * Если объекта не существует, то он будет добавлен в список. +// * Если объект уже существует в корзине, то его количество в корзине будет увеличено. +// * Ошибка будет возвращена если не был указан ID продукта или его количество равно или меньше ноля. +// +// BUG(toby3d): InMemoryStore не проверяет наличие продукта по его ID, подразумевая, что он всегда существует в базе. +func (ims *InMemoryCartStore) Add(i *model.Item) error { + switch { + case i == nil, i.GetProductId() <= 0: + return ErrNoProductId + case i.GetQuanity() <= 0: + return ErrZeroQuanity + } + + if item := ims.GetById(i.GetProductId()); item != nil { + ims.mutex.Lock() + item.Quanity += i.GetQuanity() + ims.mutex.Unlock() + return nil + } + + ims.mutex.Lock() + ims.items = append(ims.items, &model.Item{ + ProductId: i.GetProductId(), + Quanity: i.GetQuanity(), + }) + sort.Slice(ims.items, func(i, j int) bool { + return ims.items[i].GetProductId() < ims.items[j].GetProductId() + }) + ims.mutex.Unlock() + return nil +} + +// GetById возвращает объект корзины по ID его продукта (если существует). +func (ims *InMemoryCartStore) GetById(id uint64) *model.Item { + if id == 0 { + return nil + } + ims.mutex.RLock() + defer ims.mutex.RUnlock() + for _, item := range ims.items { + if item.GetProductId() != id { + continue + } + return item + } + return nil +} + +// GetList возвращает массив всех доступных в корзине объектов. +func (ims *InMemoryCartStore) GetList() (int, []*model.Item) { + ims.mutex.RLock() + defer ims.mutex.RUnlock() + return len(ims.items), ims.items +} + +// Update обновляет конкретный объект в корзине. +// +// * Если объекта с указанным ProductId ещё не существует в корзине, то он будет добавлен. +// * Если объект уже существует в корзине, то его количество будет перезаписано. +// * Если желаемое количество объекта равна нулю или отрицательно, то объект будет удалён из корзины. +func (ims *InMemoryCartStore) Update(i *model.Item) error { + if item := ims.GetById(i.GetProductId()); item != nil { + if i.GetQuanity() <= 0 { + return ims.Delete(i.GetProductId()) + } + + ims.mutex.Lock() + item.Quanity = i.GetQuanity() + ims.mutex.Unlock() + return nil + } + return ims.Add(i) +} + +// Delete удаляет объект из корзины по его ProductId (если он существует). +func (ims *InMemoryCartStore) Delete(id uint64) error { + if item := ims.GetById(id); item == nil { + return ErrNotExist + } + ims.mutex.Lock() + defer ims.mutex.Unlock() + for i := range ims.items { + if ims.items[i].GetProductId() != id { + continue + } + // NOTE(toby3d): см. https://github.com/golang/go/wiki/SliceTricks + ims.items[i] = ims.items[len(ims.items)-1] + ims.items[len(ims.items)-1] = nil + ims.items = ims.items[:len(ims.items)-1] + break + } + sort.Slice(ims.items, func(i, j int) bool { + return ims.items[i].GetProductId() < ims.items[j].GetProductId() + }) + return nil +} diff --git a/internal/store/in_memory_store_test.go b/internal/store/in_memory_store_test.go new file mode 100644 index 0000000..33e5786 --- /dev/null +++ b/internal/store/in_memory_store_test.go @@ -0,0 +1,275 @@ +package store + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "gitlab.com/toby3d/test/internal/model" +) + +func TestInMemoryAdd(t *testing.T) { + itemOne := model.Item{ProductId: 42, Quanity: 24} + itemTwo := model.Item{ProductId: 24, Quanity: 42} + t.Run("invalid", func(t *testing.T) { + s := NewInMemoryCartStore() + t.Run("empty", func(t *testing.T) { + assert.Error(t, s.Add(&model.Item{})) + count, list := s.GetList() + assert.Empty(t, list, 0) + assert.Zero(t, count) + }) + t.Run("zero quanity", func(t *testing.T) { + assert.Error(t, s.Add(&model.Item{ProductId: 42})) + count, list := s.GetList() + assert.Empty(t, list, 0) + assert.Zero(t, count) + }) + }) + t.Run("valid", func(t *testing.T) { + t.Run("single", func(t *testing.T) { + s := NewInMemoryCartStore() + assert.NoError(t, s.Add(&itemOne)) + count, list := s.GetList() + assert.Contains(t, list, &itemOne) + assert.Equal(t, 1, count) + }) + t.Run("add same", func(t *testing.T) { + s := NewInMemoryCartStore() + assert.NoError(t, s.Add(&itemOne)) + count, list := s.GetList() + assert.Contains(t, list, &itemOne) + assert.Equal(t, 1, count) + + assert.NoError(t, s.Add(&model.Item{ProductId: itemOne.ProductId, Quanity: 6})) + + count, list = s.GetList() + assert.Len(t, list, 1) + assert.Equal(t, 1, count) + assert.Contains(t, list, &model.Item{ + ProductId: itemOne.ProductId, + Quanity: itemOne.Quanity + 6, + }) + }) + t.Run("add different", func(t *testing.T) { + s := NewInMemoryCartStore() + assert.NoError(t, s.Add(&itemOne)) + count, list := s.GetList() + assert.Contains(t, list, &itemOne) + assert.Equal(t, 1, count) + + assert.NoError(t, s.Add(&itemTwo)) + + count, list = s.GetList() + assert.Len(t, list, 2) + assert.Equal(t, 2, count) + assert.Contains(t, list, &itemOne) + assert.Contains(t, list, &itemTwo) + }) + }) +} + +func TestInMemoryGetById(t *testing.T) { + t.Run("product", func(t *testing.T) { + t.Run("invalid", func(t *testing.T) { + t.Run("empty", func(t *testing.T) { + s := NewInMemoryProductStore() + assert.Nil(t, s.GetById(0)) + }) + t.Run("not exist", func(t *testing.T) { + s := NewInMemoryProductStore() + s.Products = append(s.Products, &model.Product{Id: 42, Name: "Apple", Price: 4.99}) + assert.Nil(t, s.GetById(24)) + }) + }) + t.Run("valid", func(t *testing.T) { + s := NewInMemoryProductStore() + productOne := model.Product{Id: 42, Name: "Apple", Price: 4.99} + productTwo := model.Product{Id: 24, Name: "Banana", Price: 2.49} + s.Products = append(s.Products, &productOne) + s.Products = append(s.Products, &productTwo) + assert.Equal(t, &productOne, s.GetById(productOne.GetId())) + assert.Equal(t, &productTwo, s.GetById(productTwo.GetId())) + }) + }) + t.Run("cart", func(t *testing.T) { + t.Run("invalid", func(t *testing.T) { + t.Run("empty", func(t *testing.T) { + s := NewInMemoryCartStore() + assert.Nil(t, s.GetById(0)) + }) + t.Run("not exist", func(t *testing.T) { + s := NewInMemoryCartStore() + assert.NoError(t, s.Add(&model.Item{ProductId: 42, Quanity: 4})) + assert.Nil(t, s.GetById(24)) + }) + }) + t.Run("valid", func(t *testing.T) { + s := NewInMemoryCartStore() + itemOne := model.Item{ProductId: 42, Quanity: 4} + itemTwo := model.Item{ProductId: 24, Quanity: 6} + assert.NoError(t, s.Add(&itemOne)) + assert.NoError(t, s.Add(&itemTwo)) + assert.Equal(t, &itemOne, s.GetById(itemOne.GetProductId())) + assert.Equal(t, &itemTwo, s.GetById(itemTwo.GetProductId())) + }) + }) +} + +func TestInMemoryGetList(t *testing.T) { + s := NewInMemoryCartStore() + count, list := s.GetList() + assert.Empty(t, list) + assert.Zero(t, count) + + itemOne := model.Item{ProductId: 42, Quanity: 4} + itemTwo := model.Item{ProductId: 24, Quanity: 16} + + assert.NoError(t, s.Add(&itemOne)) + + count, list = s.GetList() + assert.Len(t, list, 1) + assert.Equal(t, 1, count) + assert.Contains(t, list, &itemOne) + + assert.NoError(t, s.Add(&itemTwo)) + + count, list = s.GetList() + assert.Len(t, list, 2) + assert.Equal(t, 2, count) + assert.Contains(t, list, &itemOne) + assert.Contains(t, list, &itemTwo) + assert.Equal(t, list[0], &itemTwo, "list must be sorted by product_id") + assert.Equal(t, list[1], &itemOne, "list must be sorted by product_id") +} + +func TestInMemoryUpdate(t *testing.T) { + itemOne := model.Item{ProductId: 42, Quanity: 16} + t.Run("invalid", func(t *testing.T) { + t.Run("empty", func(t *testing.T) { + s := NewInMemoryCartStore() + assert.Error(t, s.Update(&model.Item{})) + count, list := s.GetList() + assert.Empty(t, list) + assert.Zero(t, count) + }) + t.Run("zero quanity", func(t *testing.T) { + s := NewInMemoryCartStore() + assert.Error(t, s.Update(&model.Item{ProductId: itemOne.ProductId})) + count, list := s.GetList() + assert.Empty(t, list) + assert.Zero(t, count) + }) + }) + t.Run("valid", func(t *testing.T) { + t.Run("create", func(t *testing.T) { + s := NewInMemoryCartStore() + count, list := s.GetList() + assert.Empty(t, list) + assert.Zero(t, count) + + assert.NoError(t, s.Update(&itemOne)) + + count, list = s.GetList() + assert.Contains(t, list, &itemOne) + assert.Equal(t, 1, count) + }) + t.Run("change", func(t *testing.T) { + s := NewInMemoryCartStore() + count, list := s.GetList() + assert.Empty(t, list) + assert.Zero(t, count) + + assert.NoError(t, s.Add(&itemOne)) + + count, list = s.GetList() + assert.Contains(t, list, &itemOne) + assert.Equal(t, 1, count) + + assert.NoError(t, s.Update(&model.Item{ + ProductId: itemOne.ProductId, + Quanity: 7, + })) + + count, list = s.GetList() + assert.Len(t, list, 1, "length of items store must not be changed") + assert.Equal(t, 1, count) + assert.Contains(t, list, &model.Item{ + ProductId: itemOne.ProductId, + Quanity: 7, + }) + }) + t.Run("remove", func(t *testing.T) { + t.Run("zero", func(t *testing.T) { + s := NewInMemoryCartStore() + assert.NoError(t, s.Add(&itemOne)) + + count, list := s.GetList() + assert.Contains(t, list, &itemOne) + assert.Equal(t, 1, count) + + assert.NoError(t, s.Update(&model.Item{ + ProductId: itemOne.ProductId, + Quanity: itemOne.Quanity * 0, + })) + + count, list = s.GetList() + assert.Empty(t, list) + assert.Zero(t, count) + }) + t.Run("less than quanity", func(t *testing.T) { + s := NewInMemoryCartStore() + assert.NoError(t, s.Add(&itemOne)) + + count, list := s.GetList() + assert.Contains(t, list, &itemOne) + assert.Equal(t, 1, count) + + assert.NoError(t, s.Update(&model.Item{ + ProductId: itemOne.ProductId, + Quanity: itemOne.Quanity * -1, + })) + count, list = s.GetList() + assert.Empty(t, list) + assert.Zero(t, count) + }) + }) + }) +} + +func TestInMemoryDelete(t *testing.T) { + itemOne := model.Item{ProductId: 42, Quanity: 4} + itemTwo := model.Item{ProductId: 24, Quanity: 15} + itemThree := model.Item{ProductId: 420, Quanity: 7} + t.Run("invalid", func(t *testing.T) { + s := NewInMemoryCartStore() + assert.NoError(t, s.Add(&itemOne)) + count, list := s.GetList() + assert.Contains(t, list, &itemOne) + assert.Equal(t, 1, count) + + assert.Error(t, s.Delete(itemTwo.ProductId)) + + count, list = s.GetList() + assert.Contains(t, list, &itemOne) + assert.Equal(t, 1, count) + }) + t.Run("valid", func(t *testing.T) { + s := NewInMemoryCartStore() + assert.NoError(t, s.Add(&itemOne)) + assert.NoError(t, s.Add(&itemTwo)) + assert.NoError(t, s.Add(&itemThree)) + + count, list := s.GetList() + assert.Contains(t, list, &itemOne) + assert.Contains(t, list, &itemTwo) + assert.Contains(t, list, &itemThree) + assert.Equal(t, count, 3) + + assert.NoError(t, s.Delete(itemThree.ProductId)) + count, list = s.GetList() + assert.Contains(t, list, &itemOne) + assert.Contains(t, list, &itemTwo) + assert.NotContains(t, list, &itemThree) + assert.Equal(t, 2, count) + }) +} diff --git a/internal/store/store.go b/internal/store/store.go new file mode 100644 index 0000000..8b67926 --- /dev/null +++ b/internal/store/store.go @@ -0,0 +1,119 @@ +package store + +import ( + "github.com/jmoiron/sqlx" + "gitlab.com/toby3d/test/internal/model" +) + +type ( + CartStore struct{ conn *sqlx.DB } + ProductStore struct{ conn *sqlx.DB } + + itemResult struct { + ProductID uint64 `db:"product_id"` + Quanity int32 `db:"quanity"` + } + + productResult struct { + ID uint64 `db:"id"` + Name string `db:"name"` + Price float32 `db:"price"` + } +) + +func NewCartStore(conn *sqlx.DB) *CartStore { return &CartStore{conn: conn} } + +func NewProductStore(conn *sqlx.DB) *ProductStore { return &ProductStore{conn: conn} } + +func (s *CartStore) Add(item *model.Item) (err error) { + switch { + case item == nil, item.GetProductId() <= 0: + return ErrNoProductId + case item.GetQuanity() <= 0: + return ErrZeroQuanity + } + + // NOTE(toby3d): Сначала проверяем существование продукта, который мы хотим добавить в корзину + var p productResult + if err = s.conn.Get(&p, `SELECT * FROM products WHERE id = $1`, item.GetProductId()); err != nil { + return err + } + + // NOTE(toby3d): возможно продукт уже в корзине и мы просто хотим увеличить его количество + if i := s.GetById(item.ProductId); i != nil { // NOTE(toby3d): продукт уже в корзине, увеличиваем + i.Quanity += item.Quanity + _, err = s.conn.Exec(`UPDATE cart SET quanity = $2 WHERE product_id = $1`, p.ID, i.GetQuanity()) + } else { // NOTE(toby3d): объекта в корзине ещё нет, добавляем + _, err = s.conn.Exec( + "INSERT INTO cart (product_id, quanity) VALUES ($1, $2)", p.ID, item.GetQuanity(), + ) + } + return err +} + +func (s *CartStore) Delete(id uint64) (err error) { + _, err = s.conn.Exec(`DELETE FROM cart WHERE product_id = $1`, id) + return +} + +func (s *CartStore) GetById(id uint64) *model.Item { + var item itemResult + if err := s.conn.Get(&item, "SELECT * FROM cart WHERE product_id=$1", id); err != nil { + return nil + } + + return &model.Item{ + ProductId: item.ProductID, + Quanity: item.Quanity, + } +} + +func (s *CartStore) GetList() (int, []*model.Item) { + rows, err := s.conn.Queryx(`SELECT * FROM cart ORDER BY product_id ASC`) + if err != nil { + return 0, nil + } + defer rows.Close() + + var items []*model.Item + for rows.Next() { + var item itemResult + if err = rows.StructScan(&item); err != nil { + continue + } + items = append(items, &model.Item{ + ProductId: item.ProductID, + Quanity: item.Quanity, + }) + } + if rows.Err() != nil { + return 0, nil + } + + return len(items), items +} + +func (s *CartStore) Update(item *model.Item) (err error) { + if i := s.GetById(item.GetProductId()); i != nil { + if item.GetQuanity() <= 0 { + return s.Delete(i.GetProductId()) + } + + _, err = s.conn.Exec(`UPDATE cart SET quanity = $2 WHERE product_id = $1`, i.GetProductId(), item.GetQuanity()) + return err + } + return s.Add(item) +} + +func (s *ProductStore) GetById(id uint64) *model.Product { + var product productResult + if err := s.conn.Get(&product, "SELECT * FROM products WHERE id=$1", id); err != nil { + return nil + } + + return &model.Product{ + Id: product.ID, + Name: product.Name, + Price: product.Price, + } +} diff --git a/internal/store/store_test.go b/internal/store/store_test.go new file mode 100644 index 0000000..9a52700 --- /dev/null +++ b/internal/store/store_test.go @@ -0,0 +1,344 @@ +package store + +import ( + "database/sql" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/assert" + "gitlab.com/toby3d/test/internal/model" +) + +func newDataBase(t *testing.T) (*sqlx.DB, sqlmock.Sqlmock, func()) { + db, mock, err := sqlmock.New() + if !assert.NoError(t, err) { + assert.FailNow(t, err.Error()) + } + return sqlx.NewDb(db, "sqlmock"), mock, func() { assert.NoError(t, mock.ExpectationsWereMet()) } +} + +func TestAdd(t *testing.T) { + db, mock, release := newDataBase(t) + defer release() + + s := NewCartStore(db) + itemOne := model.Item{ProductId: 42, Quanity: 24} + itemTwo := model.Item{ProductId: 24, Quanity: 42} + + t.Run("invalid", func(t *testing.T) { + t.Run("empty", func(t *testing.T) { + assert.Error(t, s.Add(&model.Item{})) + count, list := s.GetList() + assert.Empty(t, list, 0) + assert.Zero(t, count) + }) + t.Run("zero quanity", func(t *testing.T) { + assert.Error(t, s.Add(&model.Item{ProductId: 42})) + count, list := s.GetList() + assert.Empty(t, list, 0) + assert.Zero(t, count) + }) + t.Run("not exists product", func(t *testing.T) { + mock.ExpectQuery(`SELECT \* FROM products WHERE id`). + WillReturnError(sql.ErrNoRows) + + assert.Error(t, s.Add(&model.Item{ProductId: 420, Quanity: 7})) + }) + }) + t.Run("valid", func(t *testing.T) { + t.Run("single", func(t *testing.T) { + mock.ExpectQuery(`SELECT \* FROM products WHERE id`).WillReturnRows( + mock.NewRows([]string{"id", "name", "price"}). + AddRow(itemOne.GetProductId(), "Apple", 4.99), + ) + mock.ExpectQuery(`SELECT \* FROM cart WHERE product_id`) + mock.ExpectExec(`INSERT INTO cart`). + WithArgs(itemOne.GetProductId(), itemOne.GetQuanity()). + WillReturnResult(sqlmock.NewResult(int64(itemOne.GetProductId()), 1)) + + assert.NoError(t, s.Add(&itemOne)) + + mock.ExpectQuery(`SELECT \* FROM cart`). + WillReturnRows(sqlmock.NewRows([]string{"product_id", "quanity"}). + AddRow(itemOne.GetProductId(), itemOne.GetQuanity()), + ) + + count, list := s.GetList() + assert.Contains(t, list, &itemOne) + assert.Equal(t, 1, count) + }) + t.Run("append", func(t *testing.T) { + mock.ExpectQuery(`SELECT \* FROM products WHERE id`).WillReturnRows( + mock.NewRows([]string{"id", "name", "price"}). + AddRow(itemOne.GetProductId(), "Apple", 4.99), + ) + mock.ExpectQuery(`SELECT \* FROM cart WHERE product_id`). + WillReturnRows(mock.NewRows([]string{"product_id", "quanity"}). + AddRow(itemOne.GetProductId(), itemOne.GetQuanity()), + ) + mock.ExpectExec(`UPDATE cart SET quanity`). + WithArgs(itemOne.GetProductId(), itemOne.GetQuanity()+6). + WillReturnResult(sqlmock.NewResult(int64(itemOne.GetProductId()), 1)) + + assert.NoError(t, s.Add(&model.Item{ + ProductId: itemOne.GetProductId(), + Quanity: 6, + })) + + mock.ExpectQuery(`SELECT \* FROM cart`). + WillReturnRows(sqlmock.NewRows([]string{"product_id", "quanity"}). + AddRow(itemOne.GetProductId(), itemOne.GetQuanity()+6), + ) + + count, list := s.GetList() + assert.Equal(t, 1, count) + assert.Contains(t, list, &model.Item{ + ProductId: itemOne.ProductId, + Quanity: itemOne.GetQuanity() + 6, + }) + }) + t.Run("add different", func(t *testing.T) { + mock.ExpectQuery(`SELECT \* FROM products WHERE id`).WillReturnRows( + mock.NewRows([]string{"id", "name", "price"}). + AddRow(itemTwo.GetProductId(), "Banana", 2.49), + ) + mock.ExpectQuery(`SELECT \* FROM cart WHERE product_id`) + mock.ExpectExec(`INSERT INTO cart`). + WithArgs(itemTwo.GetProductId(), itemTwo.GetQuanity()). + WillReturnResult(sqlmock.NewResult(int64(itemTwo.GetProductId()), 1)) + + assert.NoError(t, s.Add(&itemTwo)) + + mock.ExpectQuery(`SELECT \* FROM cart`). + WillReturnRows(sqlmock.NewRows([]string{"product_id", "quanity"}). + AddRow(itemOne.GetProductId(), itemOne.GetQuanity()). + AddRow(itemTwo.GetProductId(), itemTwo.GetQuanity()), + ) + + count, list := s.GetList() + assert.Equal(t, 2, count) + assert.Contains(t, list, &itemOne) + assert.Contains(t, list, &itemTwo) + }) + }) +} + +func TestDelete(t *testing.T) { + db, mock, release := newDataBase(t) + defer release() + t.Run("invalid", func(t *testing.T) { + mock.ExpectExec(`DELETE FROM cart WHERE product_id`).WithArgs(240) + assert.Error(t, NewCartStore(db).Delete(240)) + }) + t.Run("valid", func(t *testing.T) { + mock.ExpectExec(`DELETE FROM cart WHERE product_id`).WithArgs(42). + WillReturnResult(sqlmock.NewResult(42, 1)) + assert.NoError(t, NewCartStore(db).Delete(42)) + }) +} + +func TestGetById(t *testing.T) { + t.Run("cart", func(t *testing.T) { + t.Run("invalid", func(t *testing.T) { + db, mock, release := newDataBase(t) + defer release() + mock.ExpectQuery(`SELECT \* FROM cart WHERE product_id`).WithArgs(24). + WillReturnError(sql.ErrNoRows) + assert.Nil(t, NewCartStore(db).GetById(24)) + }) + t.Run("valid", func(t *testing.T) { + db, mock, release := newDataBase(t) + defer release() + mock.ExpectQuery(`SELECT \* FROM cart WHERE product_id`).WithArgs(42). + WillReturnRows(mock.NewRows([]string{"product_id", "quanity"}).AddRow(42, 24)) + + assert.Equal(t, &model.Item{ + ProductId: 42, + Quanity: 24, + }, NewCartStore(db).GetById(42)) + }) + }) + t.Run("product", func(t *testing.T) { + t.Run("invalid", func(t *testing.T) { + db, mock, release := newDataBase(t) + defer release() + mock.ExpectQuery(`SELECT \* FROM products WHERE id`).WithArgs(24). + WillReturnError(sql.ErrNoRows) + assert.Nil(t, NewProductStore(db).GetById(24)) + }) + t.Run("valid", func(t *testing.T) { + db, mock, release := newDataBase(t) + defer release() + mock.ExpectQuery(`SELECT \* FROM products WHERE id`).WithArgs(42). + WillReturnRows( + mock.NewRows([]string{"id", "name", "price"}).AddRow(42, "Apple", 4.99), + ) + + assert.Equal(t, &model.Product{ + Id: 42, + Name: "Apple", + Price: 4.99, + }, NewProductStore(db).GetById(42)) + }) + }) +} + +func TestGetList(t *testing.T) { + db, mock, release := newDataBase(t) + defer release() + mock.ExpectQuery(`SELECT \* FROM cart ORDER BY product_id ASC`). + WillReturnRows(mock.NewRows([]string{"product_id", "quanity"}). + AddRow(24, 7). + AddRow(420, 11). + AddRow(42, 24), + ) + + count, list := NewCartStore(db).GetList() + assert.Equal(t, 3, count) + assert.Contains(t, list, &model.Item{ProductId: 24, Quanity: 7}) + assert.Contains(t, list, &model.Item{ProductId: 42, Quanity: 24}) + assert.Contains(t, list, &model.Item{ProductId: 420, Quanity: 11}) +} + +func TestUpdate(t *testing.T) { + itemOne := model.Item{ProductId: 42, Quanity: 16} + t.Run("invalid", func(t *testing.T) { + t.Run("empty", func(t *testing.T) { + db, _, release := newDataBase(t) + defer release() + s := NewCartStore(db) + + assert.Error(t, s.Update(&model.Item{})) + count, list := s.GetList() + assert.Empty(t, list) + assert.Zero(t, count) + }) + t.Run("zero quanity", func(t *testing.T) { + db, _, release := newDataBase(t) + defer release() + s := NewCartStore(db) + + assert.Error(t, s.Update(&model.Item{ProductId: itemOne.ProductId})) + count, list := s.GetList() + assert.Empty(t, list) + assert.Zero(t, count) + }) + t.Run("non exists product", func(t *testing.T) { + db, mock, release := newDataBase(t) + defer release() + s := NewCartStore(db) + + mock.ExpectQuery(`SELECT \* FROM products WHERE id`). + WillReturnError(sql.ErrNoRows) + + assert.Error(t, s.Update(&itemOne)) + count, list := s.GetList() + assert.Empty(t, list) + assert.Zero(t, count) + }) + }) + t.Run("valid", func(t *testing.T) { + t.Run("create", func(t *testing.T) { + db, mock, release := newDataBase(t) + defer release() + s := NewCartStore(db) + + mock.ExpectQuery(`SELECT \* FROM cart WHERE product_id`) + mock.ExpectQuery(`SELECT \* FROM products WHERE id`).WillReturnRows( + mock.NewRows([]string{"id", "name", "price"}). + AddRow(itemOne.GetProductId(), "Apple", 4.99), + ) + mock.ExpectQuery(`SELECT \* FROM cart WHERE product_id`) + mock.ExpectExec(`INSERT INTO cart`). + WithArgs(itemOne.GetProductId(), itemOne.GetQuanity()). + WillReturnResult(sqlmock.NewResult(int64(itemOne.GetProductId()), 1)) + + assert.NoError(t, s.Update(&itemOne)) + + mock.ExpectQuery(`SELECT \* FROM cart`). + WillReturnRows(sqlmock.NewRows([]string{"product_id", "quanity"}). + AddRow(itemOne.GetProductId(), itemOne.GetQuanity()), + ) + + count, list := s.GetList() + assert.Contains(t, list, &itemOne) + assert.Equal(t, 1, count) + }) + t.Run("change", func(t *testing.T) { + db, mock, release := newDataBase(t) + defer release() + s := NewCartStore(db) + + mock.ExpectQuery(`SELECT \* FROM cart WHERE product_id`). + WillReturnRows(mock.NewRows([]string{"product_id", "quanity"}). + AddRow(itemOne.GetProductId(), itemOne.GetQuanity()), + ) + mock.ExpectExec(`UPDATE cart SET quanity`). + WithArgs(itemOne.GetProductId(), 7). + WillReturnResult(sqlmock.NewResult(int64(itemOne.GetProductId()), 1)) + + assert.NoError(t, s.Update(&model.Item{ + ProductId: itemOne.GetProductId(), + Quanity: 7, + })) + + mock.ExpectQuery(`SELECT \* FROM cart`). + WillReturnRows(sqlmock.NewRows([]string{"product_id", "quanity"}). + AddRow(itemOne.GetProductId(), 7), + ) + + count, list := s.GetList() + assert.Len(t, list, 1, "length of items store must not be changed") + assert.Equal(t, 1, count) + assert.Contains(t, list, &model.Item{ + ProductId: itemOne.GetProductId(), + Quanity: 7, + }) + }) + t.Run("remove", func(t *testing.T) { + t.Run("zero", func(t *testing.T) { + db, mock, release := newDataBase(t) + defer release() + s := NewCartStore(db) + + mock.ExpectQuery(`SELECT \* FROM cart WHERE product_id`). + WillReturnRows(mock.NewRows([]string{"product_id", "quanity"}). + AddRow(itemOne.GetProductId(), itemOne.GetQuanity()), + ) + mock.ExpectExec(`DELETE FROM cart WHERE product_id`).WithArgs(itemOne.GetProductId()). + WillReturnResult(sqlmock.NewResult(int64(itemOne.GetProductId()), 1)) + + assert.NoError(t, s.Update(&model.Item{ + ProductId: itemOne.ProductId, + Quanity: itemOne.Quanity * 0, + })) + + count, list := s.GetList() + assert.Empty(t, list) + assert.Zero(t, count) + }) + t.Run("less than quanity", func(t *testing.T) { + db, mock, release := newDataBase(t) + defer release() + s := NewCartStore(db) + + mock.ExpectQuery(`SELECT \* FROM cart WHERE product_id`). + WillReturnRows(mock.NewRows([]string{"product_id", "quanity"}). + AddRow(itemOne.GetProductId(), itemOne.GetQuanity()), + ) + mock.ExpectExec(`DELETE FROM cart WHERE product_id`).WithArgs(itemOne.GetProductId()). + WillReturnResult(sqlmock.NewResult(int64(itemOne.GetProductId()), 1)) + + assert.NoError(t, s.Update(&model.Item{ + ProductId: itemOne.ProductId, + Quanity: itemOne.Quanity * -1, + })) + + count, list := s.GetList() + assert.Empty(t, list) + assert.Zero(t, count) + }) + }) + }) +}