Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of puppy store #679

Merged
merged 10 commits into from
Jul 29, 2020
73 changes: 73 additions & 0 deletions 06_puppy/jimbotech/mapstore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package main

import (
"errors"
"fmt"

"github.com/google/uuid"
)

// MapStore stores puppies.
type MapStore map[uint32]*Puppy

// length used for testing.
func (s MapStore) length() int {
return len(s)
}

// ErrNotConstructed returned if the interface was called without
// first constructing the underlaying structure.
var ErrNotConstructed = errors.New("store not created")

// CreatePuppy add a puppy to storage
// but will modify the member ID.
func (s MapStore) CreatePuppy(p *Puppy) (uint32, error) {
if s == nil {
return 0, ErrNotConstructed
}
p.ID = uuid.New().ID()
sp := *p
s[p.ID] = &sp
return p.ID, nil
}

// ReadPuppy retrieve your puppy.
func (s MapStore) ReadPuppy(id uint32) (*Puppy, error) {
if s == nil {
return nil, ErrNotConstructed
}
val, found := s[id]
if !found {
return nil, fmt.Errorf("no puppy with ID %v found", id)
}
retVal := *val
return &retVal, nil
}

// UpdatePuppy update your puppy store.
func (s MapStore) UpdatePuppy(id uint32, puppy *Puppy) error {
if s == nil {
return ErrNotConstructed
}
if _, ok := s[id]; !ok {
return fmt.Errorf("no puppy with ID %v found", id)
}
puppy.ID = id
sp := *puppy
s[id] = &sp
return nil
}

// DeletePuppy remove the puppy from store.
func (s MapStore) DeletePuppy(id uint32) error {
if s == nil {
return ErrNotConstructed
}
delete(s, id)
return nil
}

// NewMapStore constructor creates the map.
func NewMapStore() MapStore {
return MapStore{}
}
23 changes: 23 additions & 0 deletions 06_puppy/jimbotech/mapstore_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package main

import (
"testing"

"github.com/stretchr/testify/assert"
)

// TestMapStoreWithoutContructor may not be required as it may not be
// possible to create an instance of that type without using the constructor
func TestMapStoreWithoutContructor(t *testing.T) {
var puppyStore MapStore
pup := Puppy{1, "kelpie", "brown", "indispensable"}

_, err := puppyStore.CreatePuppy(&pup)
assert.Equal(t, ErrNotConstructed, err)
err = puppyStore.UpdatePuppy(1, &pup)
assert.Equal(t, ErrNotConstructed, err)
_, err = puppyStore.ReadPuppy(1)
assert.Equal(t, ErrNotConstructed, err)
err = puppyStore.DeletePuppy(1)
assert.Equal(t, ErrNotConstructed, err)
}
18 changes: 18 additions & 0 deletions 06_puppy/jimbotech/puppy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package main

import (
"fmt"
"io"
"os"
)

var out io.Writer = os.Stdout

func main() {
var puppyStore Storer = NewMapStore()
pup := Puppy{Breed: "kelpie", Colour: "brown", Value: "indispensable"}
id, _ := puppyStore.CreatePuppy(&pup)
if pup, err := puppyStore.ReadPuppy(id); err == nil {
fmt.Fprintf(out, "retrieved: %v %v %v\n", pup.Breed, pup.Colour, pup.Value)
}
}
17 changes: 17 additions & 0 deletions 06_puppy/jimbotech/puppy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package main

import (
"bytes"
"testing"
)

func TestMain(t *testing.T) {
expected := "retrieved: kelpie brown indispensable\n"
var buf bytes.Buffer
out = &buf
main()
actual := buf.String()
if actual != expected {
t.Errorf("expected %v, actual %v", expected, actual)
}
}
22 changes: 22 additions & 0 deletions 06_puppy/jimbotech/storer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package main

// Storer defines standard CRUD operations for Puppy
type Storer interface {
CreatePuppy(p *Puppy) (uint32, error)
ReadPuppy(ID uint32) (*Puppy, error)
UpdatePuppy(ID uint32, Puppy *Puppy) error
DeletePuppy(ID uint32) error
}

// Puppy stores puppy details.
type Puppy struct {
ID uint32
Breed string
Colour string
Value string
}

// mapTest used during testing to verify underlaying map changes
type mapTest interface {
length() int
}
137 changes: 137 additions & 0 deletions 06_puppy/jimbotech/storer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package main

import (
"fmt"
"testing"

"github.com/stretchr/testify/suite"
)

type storesSuite struct {
suite.Suite
store Storer
mapper mapTest
}

const brown = "brown"
const black = "black"
const grey = "grey"

func TestSuite(t *testing.T) {
suite.Run(t, &storesSuite{store: NewMapStore()})
suite.Run(t, &storesSuite{store: &SyncMapStore{}})
}

//SetupTest creates the correct empty map for each test
func (s *storesSuite) SetupTest() {
switch s.store.(type) {
case MapStore:
s.store = NewMapStore()
case *SyncMapStore:
s.store = &SyncMapStore{}
default:
s.Fail("Unknown Storer implementation")
}
s.mapper = s.store.(mapTest)
}

func (s *storesSuite) TestReadSuccess() {
pup := create(s)
// now check by reading the value back and compare
pup2, err2 := s.store.ReadPuppy(pup.ID)
s.Require().NoError(err2)
s.Equal(brown, pup2.Colour)
// modify the retured value to make sure the
// value in the store does not change
pup2.Colour = grey
pup3, err2 := s.store.ReadPuppy(pup.ID)
s.Require().NoError(err2)
s.Equal(brown, pup3.Colour)
s.NotEqual(pup2, pup3)
}

// TestCreateSuccess add to the store and verify
// by reading that it is in the store
func (s *storesSuite) TestCreateSuccess() {
pup := create(s)
// Now modify the original and make sure the
// value in the store will not change
pup.Colour = black
// now check by reading the value back and compare
pup2, err2 := s.store.ReadPuppy(pup.ID)
s.Require().NoError(err2)
s.Equal("kelpie", pup2.Breed)
s.Equal(brown, pup2.Colour)
s.Equal("indispensable", pup2.Value)
s.True(pup2.Colour == brown)
s.True(pup.Colour == black)
s.NotEqual(pup, pup2)
}

func create(s *storesSuite) *Puppy {
pup := Puppy{Breed: "kelpie", Colour: brown, Value: "indispensable"}
id, err := s.store.CreatePuppy(&pup)
s.Require().NoError(err)
s.Require().NotEqual(pup.ID, uint32(1))
s.Require().Equal(id, pup.ID, "Pup id must be set to actual id")
return &pup
}

func (s *storesSuite) TestUpdateSuccess() {
pup := create(s)
pup2 := Puppy{Breed: "kelpie", Colour: black, Value: "indispensable"}
err := s.store.UpdatePuppy(pup.ID, &pup2)
s.Require().NoError(err)
pup2.Colour = brown
// now check by reading the updated value back and compare
pup3, err2 := s.store.ReadPuppy(pup.ID)
if s.Nil(err2, "Reading back updated value should work") {
s.True(pup2.Colour == brown)
s.True(pup3.Colour == black)
s.NotEqual(pup2, *pup3)
}
}

//TestUpdateFailure checks the error returned when updating with an invalid id
func (s *storesSuite) TestUpdateFailure() {
create(s)
pup2 := Puppy{Breed: "kelpie", Colour: black, Value: "indispensable"}
err := s.store.UpdatePuppy(1, &pup2)
success := s.NotNil(err, "Update on id 1 should have failed")
if !success {
return
}
st := fmt.Sprintf("no puppy with ID %v found", 1)
s.Equal(st, err.Error())
}

func (s *storesSuite) TestDeleteSuccess() {
pup := create(s)
err := s.store.DeletePuppy(pup.ID)
s.Require().NoError(err)
_, err = s.store.ReadPuppy(pup.ID)
s.NotNil(err)
}

func (s *storesSuite) TestReadFailure() {
pup2, err := s.store.ReadPuppy(1)
s.Require().Nil(pup2)
s.Require().Error(err)
st := fmt.Sprintf("no puppy with ID %v found", 1)
s.Equal(st, err.Error())
}

func (s *storesSuite) TestMapChanges() {
s.Equal(0, s.mapper.length())
pup := Puppy{Breed: "kelpie", Colour: brown, Value: "high"}
id, err := s.store.CreatePuppy(&pup)
s.Require().Nil(err, "Create puppy failed")
s.Equal(1, s.mapper.length())
pup2 := Puppy{Breed: "kelpie", Colour: black, Value: "low"}
err = s.store.UpdatePuppy(id, &pup2)
s.Require().Nil(err, "Update puppy failed")
s.Equal(1, s.mapper.length())
err = s.store.DeletePuppy(id)
s.Require().Nil(err, "Delete puppy failed")
s.Equal(0, s.mapper.length())
}
65 changes: 65 additions & 0 deletions 06_puppy/jimbotech/sync_mapstore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package main

import (
"fmt"
"sync"

"github.com/google/uuid"
)

// SyncMapStore stores puppies threadsafe.
type SyncMapStore struct {
sync.Map
}

// length is not concorrency safe. As the go doc says:
// Range does not necessarily correspond to any consistent snapshot of the
// Map's contents: no key will be visited more than once, but if the value
// for any key is stored or deleted concurrently, Range may reflect any
// mapping for that key from any point during the Range call.
//
func (s *SyncMapStore) length() int {
var length int
s.Range(func(key interface{}, value interface{}) bool {
length++
return true
})
return length
}

// CreatePuppy threadsafe adding a puppy to storage
// but will modify the member ID.
func (s *SyncMapStore) CreatePuppy(p *Puppy) (uint32, error) {
p.ID = uuid.New().ID()
sp := *p
s.Store(p.ID, &sp)
return p.ID, nil
}

// ReadPuppy threadsafe retrieval of your puppy.
func (s *SyncMapStore) ReadPuppy(id uint32) (*Puppy, error) {
val, found := s.Load(id)
if !found {
return nil, fmt.Errorf("no puppy with ID %v found", id)
}
retPup := *val.(*Puppy)
return &retPup, nil
}

// UpdatePuppy threadsafe update your puppy store.
func (s *SyncMapStore) UpdatePuppy(id uint32, puppy *Puppy) error {
_, found := s.Load(id)
if !found {
camscale marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Errorf("no puppy with ID %v found", id)
}
puppy.ID = id
sp := *puppy
s.Store(id, &sp)
return nil
}

// DeletePuppy threadsafe removal of the puppy from store.
func (s *SyncMapStore) DeletePuppy(id uint32) error {
s.Delete(id)
return nil
}