Skip to content

Commit

Permalink
Complete lab09
Browse files Browse the repository at this point in the history
  • Loading branch information
n0npax committed Jun 25, 2019
1 parent 66287ed commit ecf933d
Show file tree
Hide file tree
Showing 13 changed files with 689 additions and 0 deletions.
36 changes: 36 additions & 0 deletions 09_json/n0npax/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# n0npax's puppy

This project is source completed lab09 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 lab09 cmd/puppy-server/main.go
./lab09

Help

./lab09 --help

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
57 changes: 57 additions & 0 deletions 09_json/n0npax/cmd/puppy-server/main.go
Original file line number Diff line number Diff line change
@@ -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)
}

}
47 changes: 47 additions & 0 deletions 09_json/n0npax/cmd/puppy-server/main_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
31 changes: 31 additions & 0 deletions 09_json/n0npax/pkg/puppy/errors.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
14 changes: 14 additions & 0 deletions 09_json/n0npax/pkg/puppy/errors_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
97 changes: 97 additions & 0 deletions 09_json/n0npax/pkg/puppy/store/leveldbStore.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
17 changes: 17 additions & 0 deletions 09_json/n0npax/pkg/puppy/store/leveldbStore_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
57 changes: 57 additions & 0 deletions 09_json/n0npax/pkg/puppy/store/memStore.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit ecf933d

Please sign in to comment.