Implement produce service #12

Merged
davidlick merged 1 commits from produce-service into main 2021-06-20 22:18:27 +00:00
14 changed files with 537 additions and 37 deletions
Showing only changes of commit 0136e616fb - Show all commits
+1
View File
@@ -3,6 +3,7 @@ module github.com/davidlick/supermarket-api
go 1.16
require (
github.com/Rhymond/go-money v1.0.2 // indirect
github.com/go-chi/chi v1.5.4 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/google/btree v1.0.1 // indirect
+2
View File
@@ -1,3 +1,5 @@
github.com/Rhymond/go-money v1.0.2 h1:KklB66H3VlpNMkm8T5BH/MROK88o7q9CCn1hl853TzI=
github.com/Rhymond/go-money v1.0.2/go.mod h1:iHvCuIvitxu2JIlAlhF0g9jHqjRSr+rpdOs7Omqlupg=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+10
View File
@@ -0,0 +1,10 @@
package interfaces
import "github.com/davidlick/supermarket-api/pkg/ramdb"
type RamDB interface {
Get(column, key string) (r *ramdb.Record, err error)
Select(column string) (rr []*ramdb.Record, err error)
Insert(r *ramdb.Record) error
Delete(r *ramdb.Record) error
}
+92
View File
@@ -0,0 +1,92 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: ./internal/interfaces/ramdb.go
// Package mocks is a generated GoMock package.
package mocks
import (
ramdb "github.com/davidlick/supermarket-api/pkg/ramdb"
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockRamDB is a mock of RamDB interface
type MockRamDB struct {
ctrl *gomock.Controller
recorder *MockRamDBMockRecorder
}
// MockRamDBMockRecorder is the mock recorder for MockRamDB
type MockRamDBMockRecorder struct {
mock *MockRamDB
}
// NewMockRamDB creates a new mock instance
func NewMockRamDB(ctrl *gomock.Controller) *MockRamDB {
mock := &MockRamDB{ctrl: ctrl}
mock.recorder = &MockRamDBMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockRamDB) EXPECT() *MockRamDBMockRecorder {
return m.recorder
}
// Get mocks base method
func (m *MockRamDB) Get(column, key string) (*ramdb.Record, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", column, key)
ret0, _ := ret[0].(*ramdb.Record)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Get indicates an expected call of Get
func (mr *MockRamDBMockRecorder) Get(column, key interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockRamDB)(nil).Get), column, key)
}
// Select mocks base method
func (m *MockRamDB) Select(column string) ([]*ramdb.Record, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Select", column)
ret0, _ := ret[0].([]*ramdb.Record)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Select indicates an expected call of Select
func (mr *MockRamDBMockRecorder) Select(column interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Select", reflect.TypeOf((*MockRamDB)(nil).Select), column)
}
// Insert mocks base method
func (m *MockRamDB) Insert(r *ramdb.Record) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Insert", r)
ret0, _ := ret[0].(error)
return ret0
}
// Insert indicates an expected call of Insert
func (mr *MockRamDBMockRecorder) Insert(r interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockRamDB)(nil).Insert), r)
}
// Delete mocks base method
func (m *MockRamDB) Delete(r *ramdb.Record) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", r)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete
func (mr *MockRamDBMockRecorder) Delete(r interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRamDB)(nil).Delete), r)
}
+24
View File
@@ -0,0 +1,24 @@
# Produce Service
This produce service allows persisting produce in a database. It supports adding multiple produce items, removing a produce item, getting a produce item by produce code, and selecting all produce items from the database.
## Example
```go
// Create database and table.
db := ramdb.NewDatabase()
_ = db.CreateTable("produce", KeyProduceCode)
produceSvc := produce.NewService(db)
produceItem := produce.Item{
Code: "A12T-4GH7-QPL9-3N4M",
Name: "Lettuce",
Price: money.New(346, "USD"),
}
_ = produceSvc.Add([]Item{produceItem})
_, _ = produceSvc.Get(produceItem.Code)
_, _ = produceSvc.All()
_ = produceSvc.Remove(produceItem)
```
+5
View File
@@ -0,0 +1,5 @@
package produce
const (
KeyProduceCode = "produce_code"
)
+10
View File
@@ -0,0 +1,10 @@
package produce
import "github.com/Rhymond/go-money"
// Item models a produce item.
type Item struct {
Code string
Name string
Price *money.Money
}
+77
View File
@@ -0,0 +1,77 @@
package produce
import (
"strings"
"github.com/davidlick/supermarket-api/internal/interfaces"
"github.com/davidlick/supermarket-api/pkg/ramdb"
)
type service struct {
db interfaces.RamDB
}
// NewService creates a new produce service for storing produce items.
func NewService(db interfaces.RamDB) *service {
return &service{
db: db,
}
}
// Add adds the Items to the database.
func (s *service) Add(items []Item) error {
for _, item := range items {
rec, err := ramdb.NewRecord(strings.ToLower(item.Code), KeyProduceCode, item)
if err != nil {
return err
}
err = s.db.Insert(rec)
if err != nil {
return err
}
}
return nil
}
// Remove removes the item from the database.
func (s *service) Remove(item Item) error {
rec, err := ramdb.NewRecord(strings.ToLower(item.Code), KeyProduceCode, item)
if err != nil {
return err
}
return s.db.Delete(rec)
}
// Get fetches the produceCode from the database.
func (s *service) Get(produceCode string) (item Item, err error) {
rec, err := s.db.Get(KeyProduceCode, strings.ToLower(produceCode))
if err != nil {
return
}
err = rec.Deserialize(&item)
return
}
// All returns all produce items stored in the database.
func (s *service) All() (items []Item, err error) {
recs, err := s.db.Select(KeyProduceCode)
if err != nil {
return
}
for _, rec := range recs {
var item Item
err = rec.Deserialize(&item)
if err != nil {
return
}
items = append(items, item)
}
return
}
+279
View File
@@ -0,0 +1,279 @@
package produce
import (
"errors"
"testing"
"github.com/Rhymond/go-money"
"github.com/davidlick/supermarket-api/internal/mocks"
"github.com/davidlick/supermarket-api/pkg/ramdb"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
)
func TestService_Add(t *testing.T) {
tests := []struct {
test string
expectFunc func(t *testing.T, mockRamDB *mocks.MockRamDB) []Item
expectedError error
}{
{
test: "it should add all Items",
expectFunc: func(t *testing.T, mockRamDB *mocks.MockRamDB) []Item {
items := []Item{
{Code: "code-1", Name: "name-1", Price: money.New(101, "USD")},
{Code: "code-2", Name: "name-2", Price: money.New(202, "USD")},
{Code: "code-3", Name: "name-3", Price: money.New(303, "USD")},
{Code: "code-4", Name: "name-4", Price: money.New(404, "USD")},
{Code: "code-5", Name: "name-5", Price: money.New(505, "USD")},
}
for _, item := range items {
rec, err := ramdb.NewRecord(item.Code, KeyProduceCode, item)
if err != nil {
t.Error(err)
}
mockRamDB.EXPECT().Insert(rec).Return(nil)
}
return items
},
},
{
test: "it should return an error from ramdb",
expectFunc: func(t *testing.T, mockRamDB *mocks.MockRamDB) []Item {
mockRamDB.EXPECT().Insert(gomock.Any()).Return(errors.New("test error"))
return []Item{
{Code: "code-1", Name: "name-1", Price: money.New(101, "USD")},
}
},
expectedError: errors.New("test error"),
},
}
for _, tc := range tests {
t.Run(tc.test, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRamDB := mocks.NewMockRamDB(ctrl)
items := tc.expectFunc(t, mockRamDB)
svc := NewService(mockRamDB)
err := svc.Add(items)
assert.Equal(t, tc.expectedError, err)
})
}
}
func TestService_Add_Lowercased(t *testing.T) {
t.Run("it should store produce codes lowercased", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
item := Item{
Code: "TEST_CODE",
Name: "Name",
Price: money.New(101, "USD"),
}
mockRamDB := mocks.NewMockRamDB(ctrl)
rec, err := ramdb.NewRecord("test_code", KeyProduceCode, item)
if err != nil {
t.Error(err)
}
mockRamDB.EXPECT().Insert(rec).Return(nil)
svc := NewService(mockRamDB)
err = svc.Add([]Item{item})
assert.Nil(t, err)
})
}
func TestService_Remove(t *testing.T) {
tests := []struct {
test string
expectFunc func(t *testing.T, mockRamDB *mocks.MockRamDB) Item
expectedError error
}{
{
test: "it should successfully remove an item",
expectFunc: func(t *testing.T, mockRamDB *mocks.MockRamDB) Item {
item := Item{Code: "code-1", Name: "name-1", Price: money.New(101, "USD")}
rec, err := ramdb.NewRecord(item.Code, KeyProduceCode, item)
if err != nil {
t.Error(err)
}
mockRamDB.EXPECT().Delete(rec).Return(nil)
return item
},
},
{
test: "it should return an error from ramdb",
expectFunc: func(t *testing.T, mockRamDB *mocks.MockRamDB) Item {
mockRamDB.EXPECT().Delete(gomock.Any()).Return(errors.New("test error"))
return Item{}
},
expectedError: errors.New("test error"),
},
}
for _, tc := range tests {
t.Run(tc.test, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRamDB := mocks.NewMockRamDB(ctrl)
item := tc.expectFunc(t, mockRamDB)
svc := NewService(mockRamDB)
err := svc.Remove(item)
assert.Equal(t, tc.expectedError, err)
})
}
}
func TestService_Get(t *testing.T) {
tests := []struct {
test string
expectFunc func(t *testing.T, mockRamDB *mocks.MockRamDB) Item
expectedError error
}{
{
test: "it should return the item successfully",
expectFunc: func(t *testing.T, mockRamDB *mocks.MockRamDB) Item {
item := Item{Code: "produce_code", Name: "name-1", Price: money.New(101, "USD")}
rec, err := ramdb.NewRecord(item.Code, KeyProduceCode, item)
if err != nil {
t.Error(err)
}
mockRamDB.EXPECT().Get(KeyProduceCode, "test_code").Return(rec, nil)
return item
},
},
{
test: "it should return an error from ramdb",
expectFunc: func(t *testing.T, mockRamDB *mocks.MockRamDB) Item {
mockRamDB.EXPECT().Get(KeyProduceCode, "test_code").Return(nil, errors.New("test error"))
return Item{}
},
expectedError: errors.New("test error"),
},
}
for _, tc := range tests {
t.Run(tc.test, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRamDB := mocks.NewMockRamDB(ctrl)
expectedItem := tc.expectFunc(t, mockRamDB)
svc := NewService(mockRamDB)
item, err := svc.Get("test_code")
assert.Equal(t, expectedItem, item)
assert.Equal(t, tc.expectedError, err)
})
}
}
func TestService_Get_CaseInsensitive(t *testing.T) {
t.Run("it should lowercase produce codes before getting", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
searchItem := Item{
Code: "TEST_CODE",
}
expectedItem := Item{
Code: "test_code",
}
rec, err := ramdb.NewRecord("test_code", KeyProduceCode, expectedItem)
if err != nil {
t.Error(err)
}
mockRamDB := mocks.NewMockRamDB(ctrl)
mockRamDB.EXPECT().Get(KeyProduceCode, "test_code").Return(rec, nil)
svc := NewService(mockRamDB)
item, err := svc.Get(searchItem.Code)
assert.Equal(t, item, expectedItem)
assert.Nil(t, err)
})
}
func TestService_All(t *testing.T) {
tests := []struct {
test string
expectFunc func(t *testing.T, mockRamDB *mocks.MockRamDB) []Item
expectedError error
}{
{
test: "it should return all items",
expectFunc: func(t *testing.T, mockRamDB *mocks.MockRamDB) []Item {
items := []Item{
{Code: "code-1", Name: "name-1", Price: money.New(101, "USD")},
{Code: "code-2", Name: "name-2", Price: money.New(202, "USD")},
{Code: "code-3", Name: "name-3", Price: money.New(303, "USD")},
{Code: "code-4", Name: "name-4", Price: money.New(404, "USD")},
{Code: "code-5", Name: "name-5", Price: money.New(505, "USD")},
}
var recs []*ramdb.Record
for _, item := range items {
rec, err := ramdb.NewRecord(item.Code, KeyProduceCode, item)
if err != nil {
t.Error(err)
}
recs = append(recs, rec)
}
mockRamDB.EXPECT().Select(KeyProduceCode).Return(recs, nil)
return items
},
},
{
test: "it should return a ramdb error",
expectFunc: func(t *testing.T, mockRamDB *mocks.MockRamDB) []Item {
mockRamDB.EXPECT().Select(gomock.Any()).Return(nil, errors.New("test error"))
return nil
},
expectedError: errors.New("test error"),
},
}
for _, tc := range tests {
t.Run(tc.test, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRamDB := mocks.NewMockRamDB(ctrl)
expectedItems := tc.expectFunc(t, mockRamDB)
svc := NewService(mockRamDB)
items, err := svc.All()
assert.Equal(t, expectedItems, items)
assert.Equal(t, tc.expectedError, err)
})
}
}
+1 -1
View File
@@ -27,7 +27,7 @@ indog := HotDog{
Brat: true,
}
// Create a new record and insert.
// Create a new Record and insert.
rec, _ := ramdb.NewRecord("1", "frank_id", indog)
_ = db.From("hotdogs").Insert(rec)
+11 -11
View File
@@ -3,7 +3,7 @@ package ramdb
import "github.com/google/btree"
// Get validates that the table and index for the given column exists and searches for the given key in the tree.
func (t *table) Get(column, key string) (r *record, err error) {
func (t *table) Get(column, key string) (r *Record, err error) {
if !t.exists {
return nil, ErrNoTable
}
@@ -16,18 +16,18 @@ func (t *table) Get(column, key string) (r *record, err error) {
}
// keyLookup tries to find the given key in the tree. It returns ErrNoRecord if not found.
func (t *table) keyLookup(key string, index *index) (r *record, err error) {
item := &record{id: keyHash(key)}
func (t *table) keyLookup(key string, index *index) (r *Record, err error) {
item := &Record{id: keyHash(key)}
result := index.tree.Get(item)
if result == nil {
return nil, ErrNoRecord
}
return result.(*record), nil
return result.(*Record), nil
}
// Select returns all of the records in the database sorted in ascending order by id.
func (t *table) Select(column string) (rr []*record, err error) {
// Select returns all of the Records in the database sorted in ascending order by id.
func (t *table) Select(column string) (rr []*Record, err error) {
if !t.exists {
return nil, ErrNoTable
}
@@ -37,7 +37,7 @@ func (t *table) Select(column string) (rr []*record, err error) {
}
t.indexes[column].tree.Ascend(func(item btree.Item) bool {
r := item.(*record)
r := item.(*Record)
rr = append(rr, r)
return true
})
@@ -45,8 +45,8 @@ func (t *table) Select(column string) (rr []*record, err error) {
return
}
// Insert adds the record to the database. It returns ErrRecordExists if the record already exists. Insert is thread safe.
func (t *table) Insert(r *record) error {
// Insert adds the Record to the database. It returns ErrRecordExists if the Record already exists. Insert is thread safe.
func (t *table) Insert(r *Record) error {
if !t.exists {
return ErrNoTable
}
@@ -69,8 +69,8 @@ func (t *table) Insert(r *record) error {
}
// Delete removes the item from the database. It returns ErrNoRecord if the record does not exist and ErrNotDeleted if the removal fails. Delete is thread safe.
func (t *table) Delete(r *record) error {
// Delete removes the item from the database. It returns ErrNoRecord if the Record does not exist and ErrNotDeleted if the removal fails. Delete is thread safe.
func (t *table) Delete(r *Record) error {
if !t.exists {
return ErrNoTable
}
+15 -15
View File
@@ -14,7 +14,7 @@ func TestTable_Get(t *testing.T) {
tests := []struct {
test string
tableConfig func() *table
expectedRecord *record
expectedRecord *Record
expectedError error
}{
{
@@ -34,7 +34,7 @@ func TestTable_Get(t *testing.T) {
expectedError: ErrNoIndex,
},
{
test: "it should return ErrNoRecord if no record was found for key",
test: "it should return ErrNoRecord if no Record was found for key",
tableConfig: func() *table {
return &table{
exists: true,
@@ -48,7 +48,7 @@ func TestTable_Get(t *testing.T) {
expectedError: ErrNoRecord,
},
{
test: "it should return a record when one is found",
test: "it should return a Record when one is found",
tableConfig: func() *table {
tbl := &table{
exists: true,
@@ -65,7 +65,7 @@ func TestTable_Get(t *testing.T) {
tbl.indexes["test_column"].tree.ReplaceOrInsert(rec)
return tbl
},
expectedRecord: &record{
expectedRecord: &Record{
serialized: []uint8{0x7b, 0x7d},
key: "test_key",
keyColumn: "test_column",
@@ -78,9 +78,9 @@ func TestTable_Get(t *testing.T) {
t.Run(tc.test, func(t *testing.T) {
tbl := tc.tableConfig()
record, err := tbl.Get("test_column", "test_key")
Record, err := tbl.Get("test_column", "test_key")
assert.Equal(t, tc.expectedRecord, record)
assert.Equal(t, tc.expectedRecord, Record)
assert.Equal(t, tc.expectedError, err)
})
}
@@ -89,13 +89,13 @@ func TestTable_Get(t *testing.T) {
func TestTable_Select(t *testing.T) {
tests := []struct {
test string
tableConfig func() (*table, []*record)
expectedRecords []*record
tableConfig func() (*table, []*Record)
expectedRecords []*Record
expectedError error
}{
{
test: "it should return ErrNoTable if an invalid table is supplied",
tableConfig: func() (*table, []*record) {
tableConfig: func() (*table, []*Record) {
return &table{
mutex: &sync.Mutex{},
}, nil
@@ -104,7 +104,7 @@ func TestTable_Select(t *testing.T) {
},
{
test: "it should return ErrNoIndex if no index exists for column",
tableConfig: func() (*table, []*record) {
tableConfig: func() (*table, []*Record) {
return &table{
exists: true,
mutex: &sync.Mutex{},
@@ -114,8 +114,8 @@ func TestTable_Select(t *testing.T) {
expectedError: ErrNoIndex,
},
{
test: "it should return all records in the database",
tableConfig: func() (*table, []*record) {
test: "it should return all Records in the database",
tableConfig: func() (*table, []*Record) {
tbl := &table{
exists: true,
mutex: &sync.Mutex{},
@@ -126,7 +126,7 @@ func TestTable_Select(t *testing.T) {
},
}
expectedRecords := make([]*record, 0)
expectedRecords := make([]*Record, 0)
for i := 0; i < 10; i++ {
key := fmt.Sprintf("key-%d", i)
rec, err := NewRecord(key, "test_column", struct{}{})
@@ -185,7 +185,7 @@ func TestTable_Insert(t *testing.T) {
expectedError: ErrNoIndex,
},
{
test: "it should return ErrRecordExists if a record with key exists",
test: "it should return ErrRecordExists if a Record with key exists",
tableConfig: func() *table {
tbl := &table{
exists: true,
@@ -265,7 +265,7 @@ func TestTable_Delete(t *testing.T) {
expectedError: ErrNoIndex,
},
{
test: "it should return ErrNoRecord if the record does not exist",
test: "it should return ErrNoRecord if the Record does not exist",
tableConfig: func() *table {
return &table{
exists: true,
+8 -8
View File
@@ -8,21 +8,21 @@ import (
"github.com/google/btree"
)
type record struct {
type Record struct {
serialized []byte
keyColumn string
key string
id uint64
}
// NewRecord returns a pointer to a record populated with data, key, and a hash of the key used for ordering in the tree.
func NewRecord(key, keyColumn string, data interface{}) (*record, error) {
// NewRecord returns a pointer to a Record populated with data, key, and a hash of the key used for ordering in the tree.
func NewRecord(key, keyColumn string, data interface{}) (*Record, error) {
serialized, err := json.Marshal(data)
if err != nil {
return nil, err
}
var r record
var r Record
r.serialized = serialized
r.keyColumn = keyColumn
r.key = key
@@ -39,13 +39,13 @@ func keyHash(s string) uint64 {
}
// Deserialize unmarshals the serialized data into `into`.
func (r *record) Deserialize(into interface{}) error {
func (r *Record) Deserialize(into interface{}) error {
err := json.Unmarshal(r.serialized, &into)
return err
}
// Less is used to order items and for looking up records in the tree.
func (r *record) Less(than btree.Item) bool {
re := than.(*record)
// Less is used to order items and for looking up Records in the tree.
func (r *Record) Less(than btree.Item) bool {
re := than.(*Record)
return r.id < re.id
}
+2 -2
View File
@@ -12,7 +12,7 @@ func TestRecord_NewRecord(t *testing.T) {
key string
keyColumn string
data interface{}
expectedRecord *record
expectedRecord *Record
expectedError string
}{
{
@@ -29,7 +29,7 @@ func TestRecord_NewRecord(t *testing.T) {
key: "test-record",
keyColumn: "test-column",
data: struct{}{},
expectedRecord: &record{
expectedRecord: &Record{
serialized: []byte("{}"),
key: "test-record",
keyColumn: "test-column",