diff --git a/06_puppy/jimbotech/mapstore.go b/06_puppy/jimbotech/mapstore.go new file mode 100644 index 000000000..f0179b4aa --- /dev/null +++ b/06_puppy/jimbotech/mapstore.go @@ -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{} +} diff --git a/06_puppy/jimbotech/mapstore_test.go b/06_puppy/jimbotech/mapstore_test.go new file mode 100644 index 000000000..3f9385543 --- /dev/null +++ b/06_puppy/jimbotech/mapstore_test.go @@ -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) +} diff --git a/06_puppy/jimbotech/puppy.go b/06_puppy/jimbotech/puppy.go new file mode 100644 index 000000000..46ebb7158 --- /dev/null +++ b/06_puppy/jimbotech/puppy.go @@ -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) + } +} diff --git a/06_puppy/jimbotech/puppy_test.go b/06_puppy/jimbotech/puppy_test.go new file mode 100644 index 000000000..ee788ccf0 --- /dev/null +++ b/06_puppy/jimbotech/puppy_test.go @@ -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) + } +} diff --git a/06_puppy/jimbotech/storer.go b/06_puppy/jimbotech/storer.go new file mode 100644 index 000000000..d677e4e20 --- /dev/null +++ b/06_puppy/jimbotech/storer.go @@ -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 +} diff --git a/06_puppy/jimbotech/storer_test.go b/06_puppy/jimbotech/storer_test.go new file mode 100644 index 000000000..87611b72a --- /dev/null +++ b/06_puppy/jimbotech/storer_test.go @@ -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()) +} diff --git a/06_puppy/jimbotech/sync_mapstore.go b/06_puppy/jimbotech/sync_mapstore.go new file mode 100644 index 000000000..0c912a4a8 --- /dev/null +++ b/06_puppy/jimbotech/sync_mapstore.go @@ -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 { + 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 +}