Compare commits

...

No commits in common. "master" and "diginavis" have entirely different histories.

19 changed files with 2593 additions and 13 deletions

View File

@ -1,16 +1,14 @@
# Тестовые задания
Это единый репозиторий для всех тестовых заданий что я делал по просьбе
компаний, кому было недостаточно просмотра кода в других моих репозиториях.
# Тестовое задание
Реализовать сервис корзины товаров:
## Нафига?
Иногда мне предлагают тестовое, которое я уже решал. Иногда меня просят
публиковать решения в различных площадках, затрудняя их поддержку. Этим
репозиторием я хочу решить все эти проблемы, сэкономив как своё время, так и
время рекрутеров.
* Метод добавления/обновления товара в корзину с указанием количества товара
* Метод удаление товара из корзины
* Метод получения списка товаров, их количества и суммы
## Как?
Каждое решение тестового задания пушится **в отдельную ветку** с именем
компании со своей историей коммитов. После сдачи решения её код в репозитории
не будет обновляться, дабы зафиксировать опыт.
Протокол: gRPC
Язык: Golang
БД: postgres
Веселитесь!
Приложение должно быть покрыто тестами, код разместить в виде публичного репозитория на github.
Для поднятия тестового окружения можно использовать docker & docker-compose, для CI - travis.

50
cmd/client/main.go Normal file
View File

@ -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(),
)
}

44
cmd/server/main.go Normal file
View File

@ -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())
}
}

37
internal/client/client.go Normal file
View File

@ -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()
}

View File

@ -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())
})
})
}

66
internal/db/db.go Normal file
View File

@ -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
}

28
internal/db/db_test.go Normal file
View File

@ -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))
})
})
}

View File

@ -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
}

View File

@ -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)
})
}

681
internal/model/model.pb.go Normal file
View File

@ -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",
}

View File

@ -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;
}
}

View File

@ -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)
}

View File

@ -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
}
)

54
internal/server/server.go Normal file
View File

@ -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
}

View File

@ -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())
})
})
}

View File

@ -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
}

View File

@ -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)
})
}

119
internal/store/store.go Normal file
View File

@ -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,
}
}

View File

@ -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)
})
})
})
}