diff --git a/09_json/n0npax/README.md b/09_json/n0npax/README.md new file mode 100644 index 000000000..29c374a16 --- /dev/null +++ b/09_json/n0npax/README.md @@ -0,0 +1,32 @@ +# n0npax's puppy + +This project is source completed lab08 from [go course](https://github.com/anz-bank/go-course/) + +## Prerequisites + +- Install `go 1.12` according to [official installation instruction](https://golang.org/doc/install) +- Clone this project outside your `$GOPATH` to enable [Go Modules](https://github.com/golang/go/wiki/Modules) +- Install `golangci-lint` according to [instructions](https://github.com/golangci/golangci-lint#local-installation) + +## Build, execute, test, lint + +Build and install this project with + + go install ./... + +Build and execute its binary with + + go build -o lab08 cmd/puppy-server/main.go + ./lab08 + +Test it with + + go test ./... + +Lint it with + + golangci-lint run + +Review coverage with + + go test -coverprofile=coverage.out ./... && go tool cover -html=coverage.out diff --git a/09_json/n0npax/cmd/puppy-server/main.go b/09_json/n0npax/cmd/puppy-server/main.go new file mode 100644 index 000000000..44b7bf131 --- /dev/null +++ b/09_json/n0npax/cmd/puppy-server/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + + "gopkg.in/alecthomas/kingpin.v2" + + puppy "github.com/anz-bank/go-course/09_json/n0npax/pkg/puppy" + store "github.com/anz-bank/go-course/09_json/n0npax/pkg/puppy/store" +) + +var out io.Writer = os.Stdout + +var ( + args = os.Args[1:] + puppyFilePath = kingpin.Flag("data", "path to file with puppies data").Short('d').ExistingFile() +) + +func main() { + _, err := kingpin.CommandLine.Parse(args) + checkError(err) + if *puppyFilePath != "" { + puppies := readPuppies(*puppyFilePath) + store := store.MemStore{} + feedStore(store, puppies) + } +} + +func readPuppies(path string) []puppy.Puppy { + b, err := ioutil.ReadFile(path) + checkError(err) + + var puppies []puppy.Puppy + err = json.Unmarshal(b, &puppies) + checkError(err) + return puppies +} + +func feedStore(s puppy.Storer, puppies []puppy.Puppy) { + for _, p := range puppies { + p := p + _, err := s.CreatePuppy(&p) + checkError(err) + } +} + +func checkError(err error) { + if err != nil { + fmt.Fprintln(out, err) + os.Exit(1) + } + +} diff --git a/09_json/n0npax/cmd/puppy-server/main_test.go b/09_json/n0npax/cmd/puppy-server/main_test.go new file mode 100644 index 000000000..d308844b2 --- /dev/null +++ b/09_json/n0npax/cmd/puppy-server/main_test.go @@ -0,0 +1,47 @@ +package main + +import ( + "bytes" + "os" + "testing" + + "bou.ke/monkey" + + "github.com/stretchr/testify/assert" +) + +func TestMainNoArgs(t *testing.T) { + var buf bytes.Buffer + out = &buf + args = []string{} + main() + + expected := "" + actual := buf.String() + assert.Equal(t, expected, actual) +} + +func TestMainWithArgs(t *testing.T) { + var buf bytes.Buffer + out = &buf + args = []string{"--data", "../../test-data/data.json"} + main() + + expected := "" + actual := buf.String() + assert.Equal(t, expected, actual) +} + +func TestMainCorruptedData(t *testing.T) { + fakeExit := func(int) { + panic("os.Exit called") + } + patch := monkey.Patch(os.Exit, fakeExit) + defer patch.Unpatch() + + var buf bytes.Buffer + out = &buf + args = []string{"--data", "/dev/null"} + + assert.PanicsWithValue(t, "os.Exit called", main, "os.Exit was not called") +} diff --git a/09_json/n0npax/pkg/puppy/errors.go b/09_json/n0npax/pkg/puppy/errors.go new file mode 100644 index 000000000..25186aefb --- /dev/null +++ b/09_json/n0npax/pkg/puppy/errors.go @@ -0,0 +1,31 @@ +package puppy + +import ( + "fmt" +) + +// Error codes +const ( + ErrInvalidInputCode = 400 + ErrNotFoundCode = 404 + ErrInternalErrorCode = 500 +) + +// Error wrapps errors with code, message and error itself +type Error struct { + Message string + Code int +} + +// Error returns error as a string +func (e *Error) Error() string { + return fmt.Sprintf("Error code: %v, message : %v", e.Code, e.Message) +} + +// Errorf creates a new Error with formatting +func Errorf(code int, format string, args ...interface{}) *Error { + return &Error{ + Message: fmt.Sprintf(format, args...), + Code: code, + } +} diff --git a/09_json/n0npax/pkg/puppy/errors_test.go b/09_json/n0npax/pkg/puppy/errors_test.go new file mode 100644 index 000000000..91ecfa808 --- /dev/null +++ b/09_json/n0npax/pkg/puppy/errors_test.go @@ -0,0 +1,14 @@ +package puppy + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestErrorf(t *testing.T) { + err := Errorf(ErrInternalErrorCode, "internal error") + assert.Equal(t, ErrInternalErrorCode, err.Code) + errMessage := err.Error() + assert.Equal(t, "Error code: 500, message : internal error", errMessage) +} diff --git a/09_json/n0npax/pkg/puppy/store/leveldbStore.go b/09_json/n0npax/pkg/puppy/store/leveldbStore.go new file mode 100644 index 000000000..126b75d97 --- /dev/null +++ b/09_json/n0npax/pkg/puppy/store/leveldbStore.go @@ -0,0 +1,97 @@ +package store + +import ( + "encoding/json" + "fmt" + "strconv" + "sync" + + puppy "github.com/anz-bank/go-course/09_json/n0npax/pkg/puppy" + + "github.com/syndtr/goleveldb/leveldb" +) + +var levelDBPath = "/tmp/leveldb" + +// LevelDBStore provides sync for leveldb +type LevelDBStore struct { + ldb *leveldb.DB + sync.Mutex + nextID int +} + +// NewLevelDBStorer creates new storer for leveldb +func NewLevelDBStorer() *LevelDBStore { + db, err := leveldb.OpenFile(levelDBPath, nil) + dbErrorPanic(err) + return &LevelDBStore{nextID: 0, ldb: db} +} + +// CreatePuppy creates puppy +func (l *LevelDBStore) CreatePuppy(p *puppy.Puppy) (int, error) { + l.Lock() + defer l.Unlock() + id, err := l.putPuppy(l.nextID, p) + l.nextID++ + return id, err +} + +// ReadPuppy reads puppy from backend +func (l *LevelDBStore) ReadPuppy(id int) (*puppy.Puppy, error) { + byteID := []byte(strconv.Itoa(id)) + if puppyData, err := l.ldb.Get(byteID, nil); err == nil { + var p puppy.Puppy + err := json.Unmarshal(puppyData, &p) + if err != nil { + return nil, puppy.Errorf(puppy.ErrInternalErrorCode, "Internal error. Could not cast stored data to puppy object") + } + return &p, nil + } + return nil, puppy.Errorf(puppy.ErrNotFoundCode, fmt.Sprintf("Puppy with ID (%v) not found", id)) +} + +// UpdatePuppy updates puppy +func (l *LevelDBStore) UpdatePuppy(id int, p *puppy.Puppy) error { + if id != p.ID { + return puppy.Errorf(puppy.ErrInvalidInputCode, "ID is corrupted. Please ensure object ID matched provided ID") + } + l.Lock() + defer l.Unlock() + if _, err := l.ReadPuppy(id); err != nil { + return puppy.Errorf(puppy.ErrNotFoundCode, fmt.Sprintf("Puppy with ID (%v) not found", id)) + } + _, err := l.putPuppy(id, p) + return err +} + +// DeletePuppy deletes puppy +func (l *LevelDBStore) DeletePuppy(id int) (bool, error) { + l.Lock() + defer l.Unlock() + if _, err := l.ReadPuppy(id); err != nil { + return false, puppy.Errorf(puppy.ErrNotFoundCode, fmt.Sprintf("Puppy with ID (%v) not found", id)) + } + byteID := []byte(strconv.Itoa(id)) + err := l.ldb.Delete(byteID, nil) + dbErrorPanic(err) + return true, nil +} + +// putPuppy stores puppy in backend +func (l *LevelDBStore) putPuppy(id int, p *puppy.Puppy) (int, error) { + if p.Value < 0 { + return -1, puppy.Errorf(puppy.ErrInvalidInputCode, "Puppy value have to be positive number") + } + puppyByte, _ := json.Marshal(p) + byteID := []byte(strconv.Itoa(id)) + err := l.ldb.Put(byteID, puppyByte, nil) + dbErrorPanic(err) + return id, nil +} + +// dbErrorPanic causes panic in error is not nil +func dbErrorPanic(err error) { + if err != nil { + panic(err) + } +} diff --git a/09_json/n0npax/pkg/puppy/store/leveldbStore_test.go b/09_json/n0npax/pkg/puppy/store/leveldbStore_test.go new file mode 100644 index 000000000..188440113 --- /dev/null +++ b/09_json/n0npax/pkg/puppy/store/leveldbStore_test.go @@ -0,0 +1,17 @@ +package store + +import ( + "testing" + + puppy "github.com/anz-bank/go-course/09_json/n0npax/pkg/puppy" +) + +func TestDbErrorPanic(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("Panic expected") + } + }() + err := puppy.Errorf(puppy.ErrInvalidInputCode, "test invalid input") + dbErrorPanic(err) +} diff --git a/09_json/n0npax/pkg/puppy/store/memStore.go b/09_json/n0npax/pkg/puppy/store/memStore.go new file mode 100644 index 000000000..d0f79191f --- /dev/null +++ b/09_json/n0npax/pkg/puppy/store/memStore.go @@ -0,0 +1,57 @@ +package store + +import ( + "fmt" + + puppy "github.com/anz-bank/go-course/09_json/n0npax/pkg/puppy" +) + +// MemStore map based type for storing puppies data +type MemStore map[int]puppy.Puppy + +// NewMemStore creates new storer for map +func NewMemStore() MemStore { + return MemStore{} +} + +// CreatePuppy creates puppy +func (m MemStore) CreatePuppy(p *puppy.Puppy) (int, error) { + if p.Value < 0 { + return -1, puppy.Errorf(puppy.ErrInvalidInputCode, "Puppy value have to be positive number") + } + id := len(m) + m[id] = *p + return id, nil +} + +// ReadPuppy reads puppy from backend +func (m MemStore) ReadPuppy(id int) (*puppy.Puppy, error) { + if puppy, ok := m[id]; ok { + return &puppy, nil + } + return nil, puppy.Errorf(puppy.ErrNotFoundCode, fmt.Sprintf("Puppy with ID (%v) not found", id)) +} + +// UpdatePuppy updates puppy +func (m MemStore) UpdatePuppy(id int, p *puppy.Puppy) error { + if p.Value < 0 { + return puppy.Errorf(puppy.ErrInvalidInputCode, "Puppy value have to be positive number") + } + if id != p.ID { + return puppy.Errorf(puppy.ErrInvalidInputCode, "ID is corrupted. Please ensure object ID matched provided ID") + } + if _, ok := m[id]; !ok { + return puppy.Errorf(puppy.ErrNotFoundCode, fmt.Sprintf("Puppy with ID (%v) not found", id)) + } + m[id] = *p + return nil +} + +// DeletePuppy deletes puppy +func (m MemStore) DeletePuppy(id int) (bool, error) { + if _, ok := m[id]; !ok { + return false, puppy.Errorf(puppy.ErrNotFoundCode, fmt.Sprintf("Puppy with ID (%v) not found", id)) + } + delete(m, id) + return true, nil +} diff --git a/09_json/n0npax/pkg/puppy/store/store_test.go b/09_json/n0npax/pkg/puppy/store/store_test.go new file mode 100644 index 000000000..fee44fa70 --- /dev/null +++ b/09_json/n0npax/pkg/puppy/store/store_test.go @@ -0,0 +1,216 @@ +package store + +import ( + "encoding/json" + "os" + "strconv" + "testing" + + puppy "github.com/anz-bank/go-course/09_json/n0npax/pkg/puppy" + "github.com/stretchr/testify/assert" + tassert "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "github.com/syndtr/goleveldb/leveldb" +) + +type storerImpl int + +const ( + syncStorer storerImpl = iota + memStorer + leveldbStorer +) + +var ( + puppy0 = func() puppy.Puppy { + return puppy.Puppy{ + Breed: "Type A", + Colour: "Grey", + Value: 300, + } + } + puppy1 = func() puppy.Puppy { + return puppy.Puppy{ + Breed: "Type B", + Colour: "Brown", + Value: 400, + } + } + puppyNegativeValue = func() puppy.Puppy { + return puppy.Puppy{ + Breed: "Type C", + Colour: "Red", + Value: -1, + } + } + puppyNegativeID = func() puppy.Puppy { + return puppy.Puppy{ + Breed: "Type D", + Colour: "Blue", + Value: 100, + ID: -1, + } + } +) + +type storerSuite struct { + suite.Suite + store puppy.Storer + impl storerImpl +} + +func (s *storerSuite) SetupSuite() { + // Remove old db if exists + os.RemoveAll(levelDBPath) +} + +func (s *storerSuite) TearDownTest() { + if ldbs, ok := s.store.(*LevelDBStore); ok { + ldbs.ldb.Close() + } +} + +func (s *storerSuite) SetupTest() { + switch s.impl { + case syncStorer: + s.store = NewSyncStore() + case memStorer: + s.store = NewMemStore() + case leveldbStorer: + s.store = NewLevelDBStorer() + default: + panic("Unrecognised storer implementation") + } + p := puppy0() + _, err := s.store.CreatePuppy(&p) + if err != nil { + panic(err) + } +} + +func (s *storerSuite) TestCreatePuppySuccessful() { + assert := tassert.New(s.T()) + newPuppy0, newPuppy1 := puppy0(), puppy1() + id0, err := s.store.CreatePuppy(&newPuppy0) + assert.NoError(err, "Creating p should be ok") + id1, err := s.store.CreatePuppy(&newPuppy1) + assert.NoError(err, "Creating p should be ok") + assert.Equal(id0, id1-1, "2nd id should be 1st +1, got %v and %v", id0, id1) +} + +func (s *storerSuite) TestCreatePuppyNegativeValue() { + assert := tassert.New(s.T()) + newPuppy := puppyNegativeValue() + _, err := s.store.CreatePuppy(&newPuppy) + assert.Error(err, "negative ID should cause an error") +} + +func (s *storerSuite) TestReadPuppySuccessful() { + assert := tassert.New(s.T()) + newPuppy := puppy0() + id, err := s.store.CreatePuppy(&newPuppy) + assert.NoError(err, "Creating p should be ok") + readPuppy, err := s.store.ReadPuppy(id) + if assert.NoError(err, "Should be able to read puppy0 from store") { + assert.Equal(&newPuppy, readPuppy, "store should return identic puppy") + } +} + +func (s *storerSuite) TestReadPuppyIDDoesNotExist() { + assert := tassert.New(s.T()) + _, err := s.store.ReadPuppy(1000) + assert.Error(err, "Should get an error when attempting to read an non-existing puppy") +} + +func (s *storerSuite) TestReadPuppyNegativeID() { + assert := tassert.New(s.T()) + _, err := s.store.ReadPuppy(-1) + assert.Error(err, "negative ID should cause an error") +} + +func (s *storerSuite) TestUpdatePuppy() { + assert := tassert.New(s.T()) + existingPuppy, err := s.store.ReadPuppy(0) + assert.NoError(err, "Reading p should not return error") + existingPuppy.Colour = "Purple" + err = s.store.UpdatePuppy(0, existingPuppy) + assert.NoError(err, "Update should not return any error") + p, err := s.store.ReadPuppy(0) + assert.NoError(err, "Reading p should not return error") + assert.Equal(existingPuppy.Colour, p.Colour, "Updated colour missmatch") +} + +func (s *storerSuite) TestUpdatePuppyCorruptedID() { + assert := tassert.New(s.T()) + existingPuppy, err := s.store.ReadPuppy(0) + assert.NoError(err, "Reading p should be ok") + err = s.store.UpdatePuppy(1000, existingPuppy) + assert.Error(err, "Should get an error when attempting to update with corrupted id") +} + +func (s *storerSuite) TestUpdatePuppyIDDoesNotExist() { + assert := tassert.New(s.T()) + newPuppy := puppy0() + err := s.store.UpdatePuppy(1000, &newPuppy) + assert.Error(err, "Should get an error when attempting to update an non-existing puppy") +} + +func (s *storerSuite) TestUpdatePuppyNegativeID() { + assert := tassert.New(s.T()) + newPuppy := puppyNegativeID() + err := s.store.UpdatePuppy(newPuppy.ID, &newPuppy) + assert.Error(err, "negative ID should cause an error") +} + +func (s *storerSuite) TestUpdatePuppyNegativeValue() { + assert := tassert.New(s.T()) + newPuppy := puppyNegativeValue() + err := s.store.UpdatePuppy(newPuppy.ID, &newPuppy) + assert.Error(err, "negative ID should cause an error") +} + +func (s *storerSuite) TestDeletePuppySuccessful() { + assert := tassert.New(s.T()) + existingPuppy := puppy0() + deleted, err := s.store.DeletePuppy(0) + assert.NoError(err, "Delete should successfully delete a puppy") + assert.True(deleted, "Delete should return true indicating a p was deleted") + _, err = s.store.ReadPuppy(existingPuppy.ID) + assert.Error(err, "Should not be able to read a deleted ID") +} + +func (s *storerSuite) TestDeletePuppyIDDoesNotExist() { + assert := tassert.New(s.T()) + _, err := s.store.DeletePuppy(1000) + assert.Error(err, "Should not be able to delete p with non existing ID") +} + +func (s *storerSuite) TestDeletePuppyNegativeID() { + assert := tassert.New(s.T()) + _, err := s.store.DeletePuppy(-1) + assert.Error(err, "negative ID should cause an error") +} + +func TestStorer(t *testing.T) { + suite.Run(t, &storerSuite{impl: syncStorer}) + suite.Run(t, &storerSuite{impl: memStorer}) + suite.Run(t, &storerSuite{impl: leveldbStorer}) +} + +func TestBrokenDataInLevelDB(t *testing.T) { + // Prepare corrupted data to cause internal error + func() { + assert := tassert.New(t) + db, _ := leveldb.OpenFile(levelDBPath, nil) + defer db.Close() + puppyByte, err := json.Marshal("this string cannot be casted to puppy") + assert.NoError(err, "no error expected during marshaling string") + byteID := []byte(strconv.Itoa(999)) + err = db.Put(byteID, puppyByte, nil) + assert.NoError(err, "no error expected during preparing corrupted data in db") + }() + s := NewLevelDBStorer() + defer s.ldb.Close() + _, err := s.ReadPuppy(999) + assert.Error(t, err) +} diff --git a/09_json/n0npax/pkg/puppy/store/syncStore.go b/09_json/n0npax/pkg/puppy/store/syncStore.go new file mode 100644 index 000000000..999741db5 --- /dev/null +++ b/09_json/n0npax/pkg/puppy/store/syncStore.go @@ -0,0 +1,70 @@ +package store + +import ( + "fmt" + "sync" + + puppy "github.com/anz-bank/go-course/09_json/n0npax/pkg/puppy" +) + +//SyncStore sync.Map based type for storing puppies data +type SyncStore struct { + sync.Map + sync.Mutex + nextID int +} + +// NewSyncStore creates new storer for SyncMap +func NewSyncStore() *SyncStore { + return &SyncStore{} +} + +// CreatePuppy creates puppy +func (s *SyncStore) CreatePuppy(p *puppy.Puppy) (int, error) { + if p.Value < 0 { + return -1, puppy.Errorf(puppy.ErrInvalidInputCode, "Puppy value have to be positive number") + } + s.Lock() + defer s.Unlock() + p.ID = s.nextID + s.nextID++ + s.Store(p.ID, *p) + return p.ID, nil +} + +// ReadPuppy reads puppy from backend +func (s *SyncStore) ReadPuppy(id int) (*puppy.Puppy, error) { + if puppyData, ok := s.Load(id); ok { + p := puppyData.(puppy.Puppy) + return &p, nil + } + return nil, puppy.Errorf(puppy.ErrNotFoundCode, fmt.Sprintf("Puppy with ID (%v) not found", id)) +} + +// UpdatePuppy updates puppy +func (s *SyncStore) UpdatePuppy(id int, p *puppy.Puppy) error { + if p.Value < 0 { + return puppy.Errorf(puppy.ErrInvalidInputCode, "Puppy value have to be positive number") + } + if id != p.ID { + return puppy.Errorf(puppy.ErrInvalidInputCode, "ID is corrupted. Please ensure object ID matched provided ID") + } + s.Lock() + defer s.Unlock() + if _, ok := s.Load(id); !ok { + return puppy.Errorf(puppy.ErrNotFoundCode, fmt.Sprintf("Puppy with ID (%v) not found", id)) + } + s.Store(id, *p) + return nil +} + +// DeletePuppy deletes puppy +func (s *SyncStore) DeletePuppy(id int) (bool, error) { + s.Lock() + defer s.Unlock() + if _, ok := s.Load(id); !ok { + return false, puppy.Errorf(puppy.ErrNotFoundCode, fmt.Sprintf("Puppy with ID (%v) not found", id)) + } + s.Delete(id) + return true, nil +} diff --git a/09_json/n0npax/pkg/puppy/types.go b/09_json/n0npax/pkg/puppy/types.go new file mode 100644 index 000000000..e5bfaa93c --- /dev/null +++ b/09_json/n0npax/pkg/puppy/types.go @@ -0,0 +1,17 @@ +package puppy + +// Puppy contains information about single puppy +type Puppy struct { + ID int `json:"id"` + Value int `json:"value"` + Breed string `json:"breed"` + Colour string `json:"colour"` +} + +// Storer interface for Store implementations +type Storer interface { + ReadPuppy(ID int) (*Puppy, error) + UpdatePuppy(ID int, puppy *Puppy) error + CreatePuppy(puppy *Puppy) (int, error) + DeletePuppy(ID int) (bool, error) +} diff --git a/09_json/n0npax/pkg/puppy/types_test.go b/09_json/n0npax/pkg/puppy/types_test.go new file mode 100644 index 000000000..38dcbb73c --- /dev/null +++ b/09_json/n0npax/pkg/puppy/types_test.go @@ -0,0 +1,24 @@ +package puppy + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestJson(t *testing.T) { + testCases := []struct { + object Puppy + marshaled string + }{ + {marshaled: `{"id":0,"value":222,"breed":"Type: R","colour":"Red"}`, + object: Puppy{Colour: "Red", Value: 222, Breed: "Type: R"}}, + {marshaled: `{"id":0,"value":0,"breed":"","colour":""}`, + object: Puppy{}}, + } + for _, test := range testCases { + b, _ := json.Marshal(test.object) + assert.Equal(t, test.marshaled, string(b)) + } +}