diff --git a/new/adding/beer.go b/new/adding/beer.go deleted file mode 100644 index 8d32e61..0000000 --- a/new/adding/beer.go +++ /dev/null @@ -1,9 +0,0 @@ -package adding - -// Beer defines the properties of a beer to be added -type Beer struct { - Name string `json:"name"` - Brewery string `json:"brewery"` - Abv float32 `json:"abv"` - ShortDesc string `json:"short_description"` -} diff --git a/new/adding/service.go b/new/adding/service.go deleted file mode 100644 index 3f75a60..0000000 --- a/new/adding/service.go +++ /dev/null @@ -1,65 +0,0 @@ -package adding - -import ( - "errors" - "github.com/katzien/go-structure-examples/new/listing" -) - -// ErrDuplicate is used when a beer already exists. -var ErrDuplicate = errors.New("beer already exists") - -// Service provides beer adding operations. -type Service interface { - AddBeer(...Beer) error - AddSampleBeers([]Beer) -} - -// Repository provides access to beer repository. -type Repository interface { - // AddBeer saves a given beer to the repository. - AddBeer(Beer) error - // GetAllBeers returns all beers saved in storage. - GetAllBeers() []listing.Beer -} - -type service struct { - r Repository -} - -// NewService creates an adding service with the necessary dependencies -func NewService(r Repository) Service { - return &service{r} -} - -// AddBeer persists the given beer(s) to storage -func (s *service) AddBeer(b ...Beer) error { - // make sure we don't add any duplicates - existingBeers := s.r.GetAllBeers() - for _, bb := range b { - for _, e := range existingBeers { - if bb.Abv == e.Abv && - bb.Brewery == e.Brewery && - bb.Name == e.Name { - return ErrDuplicate - } - } - } - - // any other validation can be done here - - for _, beer := range b { - _ = s.r.AddBeer(beer) // error handling omitted for simplicity - } - - return nil -} - -// AddSampleBeers adds some sample beers to the database -func (s *service) AddSampleBeers(b []Beer) { - - // any validation can be done here - - for _, bb := range b { - _ = s.r.AddBeer(bb) // error handling omitted for simplicity - } -} diff --git a/new/adding/service_test.go b/new/adding/service_test.go deleted file mode 100644 index 9a55e4d..0000000 --- a/new/adding/service_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package adding - -import ( - "github.com/katzien/go-structure-examples/new/listing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "testing" -) - -func TestAddBeers(t *testing.T) { - b1 := Beer{ - Name: "Test Beer 1", - Brewery: "Brewery One", - Abv: 3.6, - ShortDesc: "Lorem Ipsum", - } - - b2 := Beer{ - Name: "Test Beer 2", - Brewery: "Brewery Two", - Abv: 4.8, - ShortDesc: "Bacon Ipsum", - } - - mR := new(mockStorage) - - s := NewService(mR) - - err := s.AddBeer(b1, b2) - require.NoError(t, err) - - beers := mR.GetAllBeers() - assert.Len(t, beers, 2) -} - -type mockStorage struct { - beers []Beer -} - -func (m *mockStorage) AddBeer(b Beer) error { - m.beers = append(m.beers, b) - - return nil -} - -func (m *mockStorage) GetAllBeers() []listing.Beer { - beers := []listing.Beer{} - - for _, bb := range m.beers { - b := listing.Beer{ - Name: bb.Name, - Brewery: bb.Brewery, - Abv: bb.Abv, - ShortDesc: bb.ShortDesc, - } - beers = append(beers, b) - } - - return beers -} diff --git a/new/cmd/beer-server/main.go b/new/cmd/beer-server/main.go deleted file mode 100644 index e6f9734..0000000 --- a/new/cmd/beer-server/main.go +++ /dev/null @@ -1,57 +0,0 @@ -package main - -import ( - "fmt" - http2 "github.com/katzien/go-structure-examples/new/http" - "log" - "net/http" - - "github.com/katzien/go-structure-examples/new/adding" - "github.com/katzien/go-structure-examples/new/listing" - "github.com/katzien/go-structure-examples/new/reviewing" - "github.com/katzien/go-structure-examples/new/storage/json" - "github.com/katzien/go-structure-examples/new/storage/memory" -) - -// StorageType defines available storage types -type Type int - -const ( - // JSON will store data in JSON files saved on disk - JSON Type = iota - // Memory will store data in memory - Memory -) - -func main() { - - // set up storage - storageType := JSON // this could be a flag; hardcoded here for simplicity - - var adder adding.Service - var lister listing.Service - var reviewer reviewing.Service - - switch storageType { - case Memory: - s := new(memory.Storage) - - adder = adding.NewService(s) - lister = listing.NewService(s) - reviewer = reviewing.NewService(s) - - case JSON: - // error handling omitted for simplicity - s, _ := json.NewStorage() - - adder = adding.NewService(s) - lister = listing.NewService(s) - reviewer = reviewing.NewService(s) - } - - // set up the HTTP server - router := http2.Handler(adder, lister, reviewer) - - fmt.Println("The beer server is on tap now: http://localhost:8080") - log.Fatal(http.ListenAndServe(":8080", router)) -} diff --git a/new/cmd/sample-data/main.go b/new/cmd/sample-data/main.go deleted file mode 100644 index 1b6499c..0000000 --- a/new/cmd/sample-data/main.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/katzien/go-structure-examples/new/adding" - "github.com/katzien/go-structure-examples/new/reviewing" - "github.com/katzien/go-structure-examples/new/storage/json" -) - -func main() { - - var adder adding.Service - var reviewer reviewing.Service - - // error handling omitted for simplicity - s, _ := json.NewStorage() - - adder = adding.NewService(s) - reviewer = reviewing.NewService(s) - - // add some sample data - adder.AddSampleBeers(DefaultBeers) - reviewer.AddSampleReviews(DefaultReviews) - - fmt.Println("Finished adding sample data.") -} diff --git a/new/cmd/sample-data/sample-data b/new/cmd/sample-data/sample-data deleted file mode 100755 index b83a1bd..0000000 Binary files a/new/cmd/sample-data/sample-data and /dev/null differ diff --git a/new/domain/beer.go b/new/domain/beer.go new file mode 100644 index 0000000..2d86e54 --- /dev/null +++ b/new/domain/beer.go @@ -0,0 +1,30 @@ +package domain + +import ( + "errors" + "time" +) + +var ( + // ErrBeerNotFound is used when a beer could not be found + ErrBeerNotFound = errors.New("beer not found") + + // ErrDuplicateBeer is used when a beer already exists + ErrDuplicateBeer = errors.New("beer already exists") +) + +// Beer defines the properties of a beer +type Beer struct { + ID string + Name string + Brewery string + Abv float32 + ShortDesc string + Created time.Time +} + +type BeerRepository interface { + GetBeer(ID string) (Beer, error) + GetBeers() ([]Beer, error) + AddBeer(b Beer) error +} diff --git a/new/domain/review.go b/new/domain/review.go new file mode 100644 index 0000000..81cda11 --- /dev/null +++ b/new/domain/review.go @@ -0,0 +1,19 @@ +package domain + +import "time" + +// Review defines a beer review +type Review struct { + ID string + BeerID string + FirstName string + LastName string + Score int + Text string + Added time.Time +} + +type ReviewRepository interface { + AddReview(r Review) error + GetReviews(beerID string) ([]Review, error) +} diff --git a/new/http/rest.go b/new/http/rest.go deleted file mode 100644 index 614060a..0000000 --- a/new/http/rest.go +++ /dev/null @@ -1,95 +0,0 @@ -package http - -import ( - "encoding/json" - "github.com/julienschmidt/httprouter" - "github.com/katzien/go-structure-examples/new/adding" - "github.com/katzien/go-structure-examples/new/listing" - "github.com/katzien/go-structure-examples/new/reviewing" - "net/http" -) - -func Handler(a adding.Service, l listing.Service, r reviewing.Service) http.Handler { - router := httprouter.New() - - router.GET("/beers", getBeers(l)) - router.GET("/beers/:id", getBeer(l)) - router.GET("/beers/:id/reviews", getBeerReviews(l)) - - router.POST("/beers", addBeer(a)) - router.POST("/beers/:id/reviews", addBeerReview(r)) - - return router -} - -// addBeer returns a handler for POST /beers requests -func addBeer(s adding.Service) func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - decoder := json.NewDecoder(r.Body) - - var newBeer adding.Beer - err := decoder.Decode(&newBeer) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - s.AddBeer(newBeer) - // error handling omitted for simplicity - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode("New beer added.") - } -} - -// addBeerReview returns a handler for POST /beers/:id/reviews requests -func addBeerReview(s reviewing.Service) func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { - return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { - var newReview reviewing.Review - decoder := json.NewDecoder(r.Body) - - if err := decoder.Decode(&newReview); err != nil { - http.Error(w, "Failed to parse review", http.StatusBadRequest) - } - - newReview.BeerID = p.ByName("id") - - s.AddBeerReview(newReview) - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode("New beer review added.") - } -} - -// getBeers returns a handler for GET /beers requests -func getBeers(s listing.Service) func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - w.Header().Set("Content-Type", "application/json") - list := s.GetBeers() - json.NewEncoder(w).Encode(list) - } -} - -// getBeer returns a handler for GET /beers/:id requests -func getBeer(s listing.Service) func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { - return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { - beer, err := s.GetBeer(p.ByName("id")) - if err == listing.ErrNotFound { - http.Error(w, "The beer you requested does not exist.", http.StatusNotFound) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(beer) - } -} - -// getBeerReviews returns a handler for GET /beers/:id/reviews requests -func getBeerReviews(s listing.Service) func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { - return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { - reviews := s.GetBeerReviews(p.ByName("id")) - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(reviews) - } -} diff --git a/new/storage/idgen.go b/new/lib/idgen.go similarity index 93% rename from new/storage/idgen.go rename to new/lib/idgen.go index ca71d33..1043317 100644 --- a/new/storage/idgen.go +++ b/new/lib/idgen.go @@ -1,4 +1,4 @@ -package storage +package lib import ( "crypto/rand" @@ -10,7 +10,7 @@ import ( // just to avoid importing external UUID packages in this demo app. // This implementation in no way guarantees uniqueness, so please don't use it for any production purposes! func GetID(prefix string) (string, error) { - b := make([]byte, 8) + b := make([]byte, 8) _, err := rand.Read(b) if err != nil { return "", err diff --git a/new/storage/idgen_test.go b/new/lib/idgen_test.go similarity index 96% rename from new/storage/idgen_test.go rename to new/lib/idgen_test.go index 14f3137..b75842b 100644 --- a/new/storage/idgen_test.go +++ b/new/lib/idgen_test.go @@ -1,4 +1,4 @@ -package storage +package lib import ( "github.com/stretchr/testify/assert" diff --git a/new/listing/beer.go b/new/listing/beer.go deleted file mode 100644 index d4d3fc5..0000000 --- a/new/listing/beer.go +++ /dev/null @@ -1,15 +0,0 @@ -package listing - -import ( - "time" -) - -// Beer defines the properties of a beer to be listed -type Beer struct { - ID string `json:"id"` - Name string `json:"name"` - Brewery string `json:"brewery"` - Abv float32 `json:"abv"` - ShortDesc string `json:"short_description"` - Created time.Time `json:"created"` -} diff --git a/new/listing/review.go b/new/listing/review.go deleted file mode 100644 index abff413..0000000 --- a/new/listing/review.go +++ /dev/null @@ -1,16 +0,0 @@ -package listing - -import ( - "time" -) - -// Review defines a beer review -type Review struct { - ID string `json:"id"` - BeerID string `json:"beer_id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Score int `json:"score"` - Text string `json:"text"` - Created time.Time `json:"created"` -} diff --git a/new/listing/service.go b/new/listing/service.go deleted file mode 100644 index e758a47..0000000 --- a/new/listing/service.go +++ /dev/null @@ -1,49 +0,0 @@ -package listing - -import ( - "errors" -) - -// ErrNotFound is used when a beer could not be found. -var ErrNotFound = errors.New("beer not found") - -// Repository provides access to the beer and review storage. -type Repository interface { - // GetBeer returns the beer with given ID. - GetBeer(string) (Beer, error) - // GetAllBeers returns all beers saved in storage. - GetAllBeers() []Beer - // GetAllReviews returns a list of all reviews for a given beer ID. - GetAllReviews(string) []Review -} - -// Service provides beer and review listing operations. -type Service interface { - GetBeer(string) (Beer, error) - GetBeers() []Beer - GetBeerReviews(string) []Review -} - -type service struct { - r Repository -} - -// NewService creates a listing service with the necessary dependencies -func NewService(r Repository) Service { - return &service{r} -} - -// GetBeers returns all beers -func (s *service) GetBeers() []Beer { - return s.r.GetAllBeers() -} - -// GetBeer returns a beer -func (s *service) GetBeer(id string) (Beer, error) { - return s.r.GetBeer(id) -} - -// GetBeerReviews returns all requests for a beer -func (s *service) GetBeerReviews(beerID string) []Review { - return s.r.GetAllReviews(beerID) -} diff --git a/new/main.go b/new/main.go new file mode 100644 index 0000000..369398b --- /dev/null +++ b/new/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "fmt" + http2 "github.com/katzien/go-structure-examples/new/rest" + "github.com/katzien/go-structure-examples/new/service" + "github.com/katzien/go-structure-examples/new/storage" + "log" + "net/http" +) + +// Type defines available storage types +type Type int + +const ( + // JSON will store data in JSON files saved on disk + JSON Type = iota + // Memory will store data in memory + Memory +) + +var ( + db service.Repository + err error +) + +func main() { + // set up storage + storageType := Memory // this could be a flag; hardcoded here for simplicity + + switch storageType { + case Memory: + db = &storage.Memory{} + + case JSON: + db, err = storage.NewFileStorage() + if err != nil { + log.Fatalf("failed to initialise file storage: %v", err) + return + } + } + + service.SetDB(db) + + // set up the HTTP server + router := http2.Handle() + + fmt.Println("šŸ» Beer server is on tap now: http://localhost:8080") + log.Fatal(http.ListenAndServe(":8080", router)) +} diff --git a/new/rest/beer.go b/new/rest/beer.go new file mode 100644 index 0000000..a53d639 --- /dev/null +++ b/new/rest/beer.go @@ -0,0 +1,89 @@ +package rest + +import ( + "encoding/json" + "fmt" + "github.com/julienschmidt/httprouter" + "github.com/katzien/go-structure-examples/new/domain" + "github.com/katzien/go-structure-examples/new/service" + "net/http" + "time" +) + +// Beer defines the properties of a beer +type Beer struct { + ID string `json:"id"` + Name string `json:"name"` + Brewery string `json:"brewery"` + Abv float32 `json:"abv"` + ShortDesc string `json:"short_description"` + Created time.Time `json:"created"` +} + +// getBeer returns a handler for GET /beers/:id requests +func getBeer() func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + beer, err := service.GetBeer(p.ByName("id")) + if err != nil { + if err == domain.ErrBeerNotFound { + http.Error(w, "the beer you requested does not exist", http.StatusNotFound) + return + } + + http.Error(w, fmt.Sprintf("failed to get beer: %s", err.Error()), http.StatusInternalServerError) + return + } + + // TODO - marshall between domain and rest type? + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(beer) + } +} + +// getBeers returns a handler for GET /beers requests +func getBeers() func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + w.Header().Set("Content-Type", "application/json") + list, err := service.GetBeers() + if err != nil { + http.Error(w, fmt.Sprintf("failed to get beers: %s", err.Error()), http.StatusInternalServerError) + return + } + + // TODO - marshall between domain and rest type? + + json.NewEncoder(w).Encode(list) + } +} + +// addBeer returns a handler for POST /beers requests +func addBeer() func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + var b Beer + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&b) + if err != nil { + http.Error(w, fmt.Sprintf("failed to parse beer: %s", err.Error()), http.StatusBadRequest) + return + } + + // TODO - double check if we need the marshalling here? + + newBeer := domain.Beer{ + Name: b.Name, + Brewery: b.Brewery, + Abv: b.Abv, + ShortDesc: b.ShortDesc, + } + + beer, err := service.AddBeer(newBeer) + if err != nil { + http.Error(w, fmt.Sprintf("failed to add beer: %s", err.Error()), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(beer) // TODO - check how this gets returned, need to marshall? + } +} diff --git a/new/rest/review.go b/new/rest/review.go new file mode 100644 index 0000000..ec44498 --- /dev/null +++ b/new/rest/review.go @@ -0,0 +1,64 @@ +package rest + +import ( + "encoding/json" + "fmt" + "github.com/julienschmidt/httprouter" + "github.com/katzien/go-structure-examples/new/domain" + "github.com/katzien/go-structure-examples/new/service" + "net/http" + "time" +) + +// Review defines a beer review +type Review struct { + ID string `json:"id"` + BeerID string `json:"beer_id"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Score int `json:"score"` + Text string `json:"text"` + Created time.Time `json:"created"` +} + +// getBeerReviews returns a handler for GET /beers/:id/reviews requests +func getBeerReviews() func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + reviews, err := service.GetBeerReviews(p.ByName("id")) + if err != nil { + http.Error(w, fmt.Sprintf("failed to get reviews: %s", err.Error()), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(reviews) // TODO - need marshalling? + } +} + +// addBeerReview returns a handler for POST /beers/:id/reviews requests +func addBeerReview() func(w http.ResponseWriter, req *http.Request, p httprouter.Params) { + return func(w http.ResponseWriter, req *http.Request, p httprouter.Params) { + var r Review + decoder := json.NewDecoder(req.Body) + if err := decoder.Decode(&r); err != nil { + http.Error(w, fmt.Sprintf("failed to parse review: %s", err.Error()), http.StatusBadRequest) + } + + newReview := domain.Review{ + BeerID: p.ByName("id"), + FirstName: r.FirstName, + LastName: r.LastName, + Score: r.Score, + Text: r.Text, + } + + review, err := service.AddBeerReview(newReview) + if err != nil { + http.Error(w, fmt.Sprintf("failed to add review: %s", err.Error()), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(review) // TODO marshalling? + } +} diff --git a/new/rest/router.go b/new/rest/router.go new file mode 100644 index 0000000..e0be76e --- /dev/null +++ b/new/rest/router.go @@ -0,0 +1,19 @@ +package rest + +import ( + "github.com/julienschmidt/httprouter" + "net/http" +) + +func Handle() http.Handler { + router := httprouter.New() + + router.GET("/beers", getBeers()) + router.GET("/beers/:id", getBeer()) + router.POST("/beers", addBeer()) + + router.GET("/beers/:id/reviews", getBeerReviews()) + router.POST("/beers/:id/reviews", addBeerReview()) + + return router +} diff --git a/new/reviewing/review.go b/new/reviewing/review.go deleted file mode 100644 index 2d26b41..0000000 --- a/new/reviewing/review.go +++ /dev/null @@ -1,10 +0,0 @@ -package reviewing - -// Review defines a beer review -type Review struct { - BeerID string `json:"beer_id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Score int `json:"score"` - Text string `json:"text"` -} diff --git a/new/reviewing/service.go b/new/reviewing/service.go deleted file mode 100644 index 8f85149..0000000 --- a/new/reviewing/service.go +++ /dev/null @@ -1,41 +0,0 @@ -package reviewing - -import ( - "errors" -) - -// ErrNotFound is used when a beer could not be found. -var ErrNotFound = errors.New("beer not found") - -// Repository provides access to the review storage. -type Repository interface { - // AddReview saves a given review. - AddReview(Review) error -} - -// Service provides reviewing operations. -type Service interface { - AddBeerReview(Review) - AddSampleReviews([]Review) -} - -type service struct { - r Repository -} - -// NewService creates an adding service with the necessary dependencies -func NewService(r Repository) Service { - return &service{r} -} - -// AddBeerReview saves a new beer review in the database -func (s *service) AddBeerReview(r Review) { - _ = s.r.AddReview(r) // error handling omitted for simplicity -} - -// AddSampleReviews adds some sample reviews to the database -func (s *service) AddSampleReviews(r []Review) { - for _, rr := range r { - _ = s.r.AddReview(rr) // error handling omitted for simplicity - } -} diff --git a/new/sample-data/main.go b/new/sample-data/main.go new file mode 100644 index 0000000..3ac5b7c --- /dev/null +++ b/new/sample-data/main.go @@ -0,0 +1,41 @@ +package main + +import ( + "fmt" + "github.com/katzien/go-structure-examples/new/service" + "github.com/katzien/go-structure-examples/new/storage" +) + +var ( + DB service.Repository + err error +) + +func main() { + DB, err = storage.NewFileStorage() + if err != nil { + fmt.Printf("😵 Failed to initialise file storage: %s\n", err.Error()) + } + + // add some sample beers + for _, b := range DefaultBeers { + _, err := service.AddBeer(b) + if err != nil { + fmt.Printf("😱 Error adding beer: %s\n", err.Error()) + } + } + + fmt.Printf("\nāœ… Added %d beers\n", len(DefaultBeers)) + + // add some sample reviews + for _, r := range DefaultReviews { + _, err := service.AddBeerReview(r) + if err != nil { + fmt.Printf("😱 Error adding review: %s\n", err.Error()) + } + } + + fmt.Printf("\nāœ… Added %d reviews\n", len(DefaultReviews)) + + fmt.Printf("\nšŸš€ Finished adding sample data\n") +} diff --git a/new/cmd/sample-data/sample_beers.go b/new/sample-data/sample_beers.go similarity index 97% rename from new/cmd/sample-data/sample_beers.go rename to new/sample-data/sample_beers.go index 3279d2e..c32e7af 100644 --- a/new/cmd/sample-data/sample_beers.go +++ b/new/sample-data/sample_beers.go @@ -1,8 +1,10 @@ package main -import "github.com/katzien/go-structure-examples/new/adding" +import ( + "github.com/katzien/go-structure-examples/new/domain" +) -var DefaultBeers = []adding.Beer{ +var DefaultBeers = []domain.Beer{ { Name: "Pliny the Elder", Brewery: "Russian River Brewing Company", diff --git a/new/cmd/sample-data/sample_reviews.go b/new/sample-data/sample_reviews.go similarity index 87% rename from new/cmd/sample-data/sample_reviews.go rename to new/sample-data/sample_reviews.go index 8b3f508..f70e938 100644 --- a/new/cmd/sample-data/sample_reviews.go +++ b/new/sample-data/sample_reviews.go @@ -1,8 +1,10 @@ package main -import "github.com/katzien/go-structure-examples/new/reviewing" +import ( + "github.com/katzien/go-structure-examples/new/domain" +) -var DefaultReviews = []reviewing.Review{ +var DefaultReviews = []domain.Review{ {BeerID: "1", FirstName: "Joe", LastName: "Tribiani", Score: 5, Text: "This is good but this is not pizza!"}, {BeerID: "2", FirstName: "Chandler", LastName: "Bing", Score: 1, Text: "I would SO NOT drink this ever again."}, {BeerID: "1", FirstName: "Ross", LastName: "Geller", Score: 4, Text: "Drank while on a break, was pretty good!"}, diff --git a/new/service/beer.go b/new/service/beer.go new file mode 100644 index 0000000..c7233e8 --- /dev/null +++ b/new/service/beer.go @@ -0,0 +1,59 @@ +package service + +import ( + "github.com/katzien/go-structure-examples/new/domain" + "github.com/katzien/go-structure-examples/new/lib" + "time" +) + +// GetBeers returns all beers +// TODO - confirm if this should return pointers +func GetBeers() ([]domain.Beer, error) { + return db.GetBeers() +} + +// GetBeer returns a single beer +func GetBeer(id string) (*domain.Beer, error) { + b, err := db.GetBeer(id) + if err != nil { + return nil, err + } + + return &b, nil +} + +// AddBeer persists the given beer to storage +// TODO - should this be in the domain?? +// TODO - confirm pointer? +func AddBeer(b domain.Beer) (*domain.Beer, error) { + // make sure we don't add any duplicates + existingBeers, err := db.GetBeers() + if err != nil { + return nil, err + } + + for _, e := range existingBeers { + if b.Abv == e.Abv && + b.Brewery == e.Brewery && + b.Name == e.Name { + return nil, domain.ErrDuplicateBeer + } + } + + // TODO: any other validation (e.g. valid brewery/ABV etc.) can be done here using calls to the domain + + id, err := lib.GetID("beer") // TODO should be a lib really, or domain func? + if err != nil { + return nil, err + } + b.ID = id + + b.Created = time.Now().UTC() + + err = db.AddBeer(b) + if err != nil { + return nil, err + } + + return &b, nil +} diff --git a/new/service/beer_test.go b/new/service/beer_test.go new file mode 100644 index 0000000..eae62c6 --- /dev/null +++ b/new/service/beer_test.go @@ -0,0 +1,30 @@ +package service_test + +import ( + "github.com/katzien/go-structure-examples/new/domain" + "github.com/katzien/go-structure-examples/new/service" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestBeerService(t *testing.T) { + b := domain.Beer{ + Name: "Test Beer 1", + Brewery: "Brewery One", + Abv: 3.6, + ShortDesc: "Lorem Ipsum", + } + + addedBeer, err := service.AddBeer(b) + require.NoError(t, err) + + beers, err := service.GetBeers() + require.NoError(t, err) + assert.Len(t, beers, 1) + assert.Equal(t, addedBeer, beers[0]) + + beer, err := service.GetBeer(addedBeer.ID) + require.NoError(t, err) + assert.Equal(t, addedBeer, beer) +} diff --git a/new/service/repository.go b/new/service/repository.go new file mode 100644 index 0000000..8baa586 --- /dev/null +++ b/new/service/repository.go @@ -0,0 +1,14 @@ +package service + +import "github.com/katzien/go-structure-examples/new/domain" + +var db Repository + +func SetDB(r Repository) { + db = r +} + +type Repository interface { + domain.BeerRepository + domain.ReviewRepository +} diff --git a/new/service/review.go b/new/service/review.go new file mode 100644 index 0000000..417d898 --- /dev/null +++ b/new/service/review.go @@ -0,0 +1,35 @@ +package service + +import ( + "github.com/katzien/go-structure-examples/new/domain" + "github.com/katzien/go-structure-examples/new/lib" + "time" +) + +// GetBeerReviews returns all requests for a beer +func GetBeerReviews(beerID string) ([]domain.Review, error) { + return db.GetReviews(beerID) +} + +// AddBeerReview saves a new beer review +func AddBeerReview(r domain.Review) (*domain.Review, error) { + // make sure we're adding a review for an existing beer + _, err := db.GetBeer(r.BeerID) + if err != nil { + return nil, err + } + + id, err := lib.GetID("review") // TODO should be a lib really, or domain func? + if err != nil { + return nil, err + } + r.ID = id + + r.Added = time.Now().UTC() + + err = db.AddReview(r) + if err != nil { + return nil, err + } + return &r, nil +} diff --git a/new/service/review_test.go b/new/service/review_test.go new file mode 100644 index 0000000..0729315 --- /dev/null +++ b/new/service/review_test.go @@ -0,0 +1,51 @@ +package service_test + +import ( + "github.com/katzien/go-structure-examples/new/domain" + "github.com/katzien/go-structure-examples/new/service" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestReviewMustBeForExistingBeer(t *testing.T) { + r := domain.Review{ + BeerID: "does-not-exist-yet", + FirstName: "Foo", + LastName: "Bar", + Score: 5, + Text: "This is good stuff!", + } + + _, err := service.AddBeerReview(r) + require.Error(t, err) + assert.EqualError(t, domain.ErrBeerNotFound, err.Error(), "expected an error because we're trying to add a review for a beer which doesn't exist") +} + +func TestReviewService(t *testing.T) { + b := domain.Beer{ + Name: "Test Beer 1", + Brewery: "Brewery One", + Abv: 3.6, + ShortDesc: "Lorem Ipsum", + } + + addedBeer, err := service.AddBeer(b) + require.NoError(t, err) + + r := domain.Review{ + BeerID: addedBeer.ID, + FirstName: "Foo", + LastName: "Bar", + Score: 5, + Text: "This is good stuff!", + } + + addedReview, err := service.AddBeerReview(r) + require.NoError(t, err) + + reviews, err := service.GetBeerReviews(addedBeer.ID) + require.NoError(t, err) + assert.Len(t, reviews, 1) + assert.Equal(t, addedReview, reviews[0]) +} diff --git a/new/storage/json.go b/new/storage/json.go new file mode 100644 index 0000000..c3d140e --- /dev/null +++ b/new/storage/json.go @@ -0,0 +1,181 @@ +package storage + +import ( + "encoding/json" + "path" + "runtime" + "time" + + "github.com/katzien/go-structure-examples/new/domain" + "github.com/nanobox-io/golang-scribble" +) + +const ( + // dir defines the name of the directory where the files are stored + dir = "/data/" + + // CollectionBeer identifier for the JSON collection of beers + CollectionBeer = "beers" + // CollectionReview identifier for the JSON collection of reviews + CollectionReview = "reviews" +) + +// Beer defines the storage form of a beer +type Beer struct { + ID string `json:"id"` + Name string `json:"name"` + Brewery string `json:"brewery"` + Abv float32 `json:"abv"` + ShortDesc string `json:"short_description"` + Created time.Time `json:"created"` +} + +// Review defines the storage form of a beer review +type Review struct { + ID string `json:"id"` + BeerID string `json:"beer_id"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Score int `json:"score"` + Text string `json:"text"` + Added time.Time `json:"added"` +} + +// File storage keeps data in JSON files +type file struct { + db *scribble.Driver +} + +// NewFileStorage returns a new file storage +func NewFileStorage() (*file, error) { + _, filename, _, _ := runtime.Caller(0) + p := path.Dir(filename) + + driver, err := scribble.New(p+dir, nil) + if err != nil { + return nil, err + } + + return &file{db: driver}, nil +} + +// AddBeer saves the given beer to the repository +func (s *file) AddBeer(b domain.Beer) error { + bJSON := Beer{ + ID: b.ID, + Name: b.Name, + Brewery: b.Brewery, + Abv: b.Abv, + ShortDesc: b.ShortDesc, + Created: b.Created, + } + + if err := s.db.Write(CollectionBeer, bJSON.ID, bJSON); err != nil { + return err + } + + return nil +} + +// GetBeer returns a beer with the specified ID +func (s *file) GetBeer(ID string) (domain.Beer, error) { + var b Beer + + if err := s.db.Read(CollectionBeer, ID, &b); err != nil { + return domain.Beer{}, err + } + + beer := domain.Beer{ + ID: b.ID, + Name: b.Name, + Brewery: b.Brewery, + Abv: b.Abv, + ShortDesc: b.ShortDesc, + Created: b.Created, + } + + return beer, nil +} + +// GetBeers returns all beers +func (s *file) GetBeers() ([]domain.Beer, error) { + var list []domain.Beer + + records, err := s.db.ReadAll(CollectionBeer) + if err != nil { + return list, err + } + + for _, r := range records { + var b Beer + if err := json.Unmarshal([]byte(r), &b); err != nil { + return list, err + } + + beer := domain.Beer{ + ID: b.ID, + Name: b.Name, + Brewery: b.Brewery, + Abv: b.Abv, + ShortDesc: b.ShortDesc, + Created: b.Created, + } + + list = append(list, beer) + } + + return list, nil +} + +// AddReview saves the given review in the repository +func (s *file) AddReview(r domain.Review) error { + rJSON := Review{ + ID: r.ID, + BeerID: r.BeerID, + FirstName: r.FirstName, + LastName: r.LastName, + Score: r.Score, + Text: r.Text, + Added: r.Added, + } + + if err := s.db.Write(CollectionReview, rJSON.ID, rJSON); err != nil { + return err + } + + return nil +} + +// GetReviews returns all reviews for a given beer +func (s *file) GetReviews(beerID string) ([]domain.Review, error) { + var list []domain.Review + + records, err := s.db.ReadAll(CollectionReview) + if err != nil { + return list, err + } + + for _, b := range records { + var r Review + + if err := json.Unmarshal([]byte(b), &r); err != nil { + return list, err + } + + if r.BeerID == beerID { + review := domain.Review{ + ID: r.ID, + BeerID: r.BeerID, + FirstName: r.FirstName, + LastName: r.LastName, + Score: r.Score, + Text: r.Text, + Added: r.Added, + } + + list = append(list, review) + } + } + + return list, nil +} diff --git a/new/storage/json/beer.go b/new/storage/json/beer.go deleted file mode 100644 index ec0719d..0000000 --- a/new/storage/json/beer.go +++ /dev/null @@ -1,13 +0,0 @@ -package json - -import "time" - -// Beer defines the storage form of a beer -type Beer struct { - ID string `json:"id"` - Name string `json:"name"` - Brewery string `json:"brewery"` - Abv float32 `json:"abv"` - ShortDesc string `json:"short_description"` - Created time.Time `json:"created"` -} diff --git a/new/storage/json/repository.go b/new/storage/json/repository.go deleted file mode 100644 index ef0ad3b..0000000 --- a/new/storage/json/repository.go +++ /dev/null @@ -1,184 +0,0 @@ -package json - -import ( - "encoding/json" - "fmt" - "github.com/katzien/go-structure-examples/new/storage" - "log" - "path" - "runtime" - "time" - - "github.com/katzien/go-structure-examples/new/adding" - "github.com/katzien/go-structure-examples/new/listing" - "github.com/katzien/go-structure-examples/new/reviewing" - "github.com/nanobox-io/golang-scribble" -) - -const ( - // dir defines the name of the directory where the files are stored - dir = "/data/" - - // CollectionBeer identifier for the JSON collection of beers - CollectionBeer = "beers" - // CollectionReview identifier for the JSON collection of reviews - CollectionReview = "reviews" -) - -// Storage stores beer data in JSON files -type Storage struct { - db *scribble.Driver -} - -// NewStorage returns a new JSON storage -func NewStorage() (*Storage, error) { - var err error - - s := new(Storage) - - _, filename, _, _ := runtime.Caller(0) - p := path.Dir(filename) - - s.db, err = scribble.New(p+dir, nil) - if err != nil { - return nil, err - } - - return s, nil -} - -// AddBeer saves the given beer to the repository -func (s *Storage) AddBeer(b adding.Beer) error { - id, err := storage.GetID("beer") - if err != nil { - log.Fatal(err) - } - - newB := Beer{ - ID: id, - Created: time.Now(), - Name: b.Name, - Brewery: b.Brewery, - Abv: b.Abv, - ShortDesc: b.ShortDesc, - } - - if err := s.db.Write(CollectionBeer, newB.ID, newB); err != nil { - return err - } - return nil -} - -// AddReview saves the given review in the repository -func (s *Storage) AddReview(r reviewing.Review) error { - - var beer Beer - if err := s.db.Read(CollectionBeer, r.BeerID, &beer); err != nil { - return listing.ErrNotFound - } - - created := time.Now() - newR := Review{ - ID: fmt.Sprintf("%d_%s_%s_%d", r.BeerID, r.FirstName, r.LastName, created.Unix()), - Created: created, - BeerID: r.BeerID, - FirstName: r.FirstName, - LastName: r.LastName, - Score: r.Score, - Text: r.Text, - } - - if err := s.db.Write(CollectionReview, newR.ID, r); err != nil { - return err - } - - return nil -} - -// Get returns a beer with the specified ID -func (s *Storage) GetBeer(id string) (listing.Beer, error) { - var b Beer - var beer listing.Beer - - if err := s.db.Read(CollectionBeer, id, &b); err != nil { - // err handling omitted for simplicity - return beer, listing.ErrNotFound - } - - beer.ID = b.ID - beer.Name = b.Name - beer.Brewery = b.Brewery - beer.Abv = b.Abv - beer.ShortDesc = b.ShortDesc - beer.Created = b.Created - - return beer, nil -} - -// GetAll returns all beers -func (s *Storage) GetAllBeers() []listing.Beer { - list := []listing.Beer{} - - records, err := s.db.ReadAll(CollectionBeer) - if err != nil { - // err handling omitted for simplicity - return list - } - - for _, r := range records { - var b Beer - var beer listing.Beer - - if err := json.Unmarshal([]byte(r), &b); err != nil { - // err handling omitted for simplicity - return list - } - - beer.ID = b.ID - beer.Name = b.Name - beer.Brewery = b.Brewery - beer.Abv = b.Abv - beer.ShortDesc = b.ShortDesc - beer.Created = b.Created - - list = append(list, beer) - } - - return list -} - -// GetAll returns all reviews for a given beer -func (s *Storage) GetAllReviews(beerID string) []listing.Review { - list := []listing.Review{} - - records, err := s.db.ReadAll(CollectionReview) - if err != nil { - // err handling omitted for simplicity - return list - } - - for _, b := range records { - var r Review - - if err := json.Unmarshal([]byte(b), &r); err != nil { - // err handling omitted for simplicity - return list - } - - if r.BeerID == beerID { - var review listing.Review - - review.ID = r.ID - review.BeerID = r.BeerID - review.FirstName = r.FirstName - review.LastName = r.LastName - review.Score = r.Score - review.Text = r.Text - review.Created = r.Created - - list = append(list, review) - } - } - - return list -} diff --git a/new/storage/json/review.go b/new/storage/json/review.go deleted file mode 100644 index 9bd27ab..0000000 --- a/new/storage/json/review.go +++ /dev/null @@ -1,14 +0,0 @@ -package json - -import "time" - -// Review defines the storage form of a beer review -type Review struct { - ID string `json:"id"` - BeerID string `json:"beer_id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Score int `json:"score"` - Text string `json:"text"` - Created time.Time `json:"created"` -} diff --git a/new/storage/memory.go b/new/storage/memory.go new file mode 100644 index 0000000..fd2b966 --- /dev/null +++ b/new/storage/memory.go @@ -0,0 +1,52 @@ +package storage + +import ( + "github.com/katzien/go-structure-examples/new/domain" +) + +// Memory storage keeps data in memory +type Memory struct { + beers []domain.Beer + reviews map[string][]domain.Review // map indexed by beer ID +} + +// GetBeer returns a beer with the specified ID +func (m *Memory) GetBeer(ID string) (domain.Beer, error) { + for _, b := range m.beers { + if b.ID == ID { + return b, nil + } + } + + return domain.Beer{}, domain.ErrBeerNotFound +} + +// GetBeers returns all beers +func (m *Memory) GetBeers() ([]domain.Beer, error) { + return m.beers, nil +} + +// AddBeer saves the given beer to the in-memory database +func (m *Memory) AddBeer(b domain.Beer) error { + m.beers = append(m.beers, b) + return nil +} + +// AddReview saves the given review in the in-memory database +func (m *Memory) AddReview(r domain.Review) error { + list, ok := m.reviews[r.BeerID] + if !ok { + // we haven't had a review for this beer yet, so we add a new map index + m.reviews[r.BeerID] = []domain.Review{r} + return nil + } + + // we already have other reviews for this beer, so we add it to the list + m.reviews[r.BeerID] = append(list, r) + return nil +} + +// GetReviews returns all reviews for a given beer +func (m *Memory) GetReviews(beerID string) ([]domain.Review, error) { + return m.reviews[beerID], nil +} diff --git a/new/storage/memory/beer.go b/new/storage/memory/beer.go deleted file mode 100644 index 2b8838f..0000000 --- a/new/storage/memory/beer.go +++ /dev/null @@ -1,15 +0,0 @@ -package memory - -import ( - "time" -) - -// Beer defines the storage form of a beer -type Beer struct { - ID string - Name string - Brewery string - Abv float32 - ShortDesc string - Created time.Time -} diff --git a/new/storage/memory/repository.go b/new/storage/memory/repository.go deleted file mode 100644 index 4deac13..0000000 --- a/new/storage/memory/repository.go +++ /dev/null @@ -1,134 +0,0 @@ -package memory - -import ( - "fmt" - "github.com/katzien/go-structure-examples/new/storage" - "log" - "time" - - "github.com/katzien/go-structure-examples/new/adding" - "github.com/katzien/go-structure-examples/new/listing" - "github.com/katzien/go-structure-examples/new/reviewing" -) - -// Memory storage keeps data in memory -type Storage struct { - beers []Beer - reviews []Review -} - -// Add saves the given beer to the repository -func (m *Storage) AddBeer(b adding.Beer) error { - id, err := storage.GetID("beer") - if err != nil { - log.Fatal(err) - } - - newB := Beer{ - ID: id, - Created: time.Now(), - Name: b.Name, - Brewery: b.Brewery, - Abv: b.Abv, - ShortDesc: b.ShortDesc, - } - m.beers = append(m.beers, newB) - - return nil -} - -// Add saves the given review in the repository -func (m *Storage) AddReview(r reviewing.Review) error { - found := false - for b := range m.beers { - if m.beers[b].ID == r.BeerID { - found = true - } - } - - if found { - created := time.Now() - id := fmt.Sprintf("%d_%s_%s_%d", r.BeerID, r.FirstName, r.LastName, created.Unix()) - - newR := Review{ - ID: id, - Created: created, - BeerID: r.BeerID, - FirstName: r.FirstName, - LastName: r.LastName, - Score: r.Score, - Text: r.Text, - } - - m.reviews = append(m.reviews, newR) - } else { - return listing.ErrNotFound - } - - return nil -} - -// Get returns a beer with the specified ID -func (m *Storage) GetBeer(id string) (listing.Beer, error) { - var beer listing.Beer - - for i := range m.beers { - - if m.beers[i].ID == id { - beer.ID = m.beers[i].ID - beer.Name = m.beers[i].Name - beer.Brewery = m.beers[i].Brewery - beer.Abv = m.beers[i].Abv - beer.ShortDesc = m.beers[i].ShortDesc - beer.Created = m.beers[i].Created - - return beer, nil - } - } - - return beer, listing.ErrNotFound -} - -// GetAll return all beers -func (m *Storage) GetAllBeers() []listing.Beer { - var beers []listing.Beer - - for i := range m.beers { - - beer := listing.Beer{ - ID: m.beers[i].ID, - Name: m.beers[i].Name, - Brewery: m.beers[i].Brewery, - Abv: m.beers[i].Abv, - ShortDesc: m.beers[i].ShortDesc, - Created: m.beers[i].Created, - } - - beers = append(beers, beer) - } - - return beers -} - -// GetAllReviews returns all reviews for a given beer -func (m *Storage) GetAllReviews(beerID string) []listing.Review { - var list []listing.Review - - for i := range m.reviews { - if m.reviews[i].BeerID == beerID { - r := listing.Review{ - ID: m.reviews[i].ID, - BeerID: m.reviews[i].BeerID, - FirstName: m.reviews[i].FirstName, - LastName: m.reviews[i].LastName, - Score: m.reviews[i].Score, - Text: m.reviews[i].Text, - Created: m.reviews[i].Created, - } - - list = append(list, r) - } - } - - return list -} diff --git a/new/storage/memory/review.go b/new/storage/memory/review.go deleted file mode 100644 index fc549aa..0000000 --- a/new/storage/memory/review.go +++ /dev/null @@ -1,16 +0,0 @@ -package memory - -import ( - "time" -) - -// Review defines the storage form of a beer review -type Review struct { - ID string - BeerID string - FirstName string - LastName string - Score int - Text string - Created time.Time -}