Skip to content

Commit

Permalink
Refactor a bunch of things:
Browse files Browse the repository at this point in the history
- move validation for duplicate beers out of storage and to the service
- change how we generate beer IDs, and from int to string
- add a test for adding beers (with known limitations)
  • Loading branch information
katzien committed May 17, 2020
1 parent 2bb2ec2 commit 2289b0f
Show file tree
Hide file tree
Showing 16 changed files with 169 additions and 53 deletions.
31 changes: 24 additions & 7 deletions domain-hex/pkg/adding/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,57 @@ package adding

import (
"errors"
"github.com/katzien/go-structure-examples/domain-hex-actor/pkg/adding"
"github.com/katzien/go-structure-examples/domain-hex/pkg/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)
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 {
bR Repository
r Repository
}

// NewService creates an adding service with the necessary dependencies
func NewService(r Repository) Service {
return &service{r}
}

// AddBeer adds the given beer(s) to the database
func (s *service) AddBeer(b ...Beer) {
// 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 adding.ErrDuplicate
}
}
}

// any validation can be done here
// any other validation can be done here

for _, beer := range b {
_ = s.bR.AddBeer(beer) // error handling omitted for simplicity
_ = s.r.AddBeer(beer) // error handling omitted for simplicity
}

return nil
}

// AddSampleBeers adds some sample beers to the database
Expand All @@ -44,6 +61,6 @@ func (s *service) AddSampleBeers(b []Beer) {
// any validation can be done here

for _, bb := range b {
_ = s.bR.AddBeer(bb) // error handling omitted for simplicity
_ = s.r.AddBeer(bb) // error handling omitted for simplicity
}
}
60 changes: 60 additions & 0 deletions domain-hex/pkg/adding/service_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package adding

import (
"github.com/katzien/go-structure-examples/domain-hex/pkg/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
}
3 changes: 2 additions & 1 deletion domain-hex/pkg/http/rest/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ func addBeer(s adding.Service) func(w http.ResponseWriter, r *http.Request, _ ht
return
}

s.AddBeer(newBeer)
_ := s.AddBeer(newBeer)
// error handling omitted for simplicity

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode("New beer added.")
Expand Down
2 changes: 1 addition & 1 deletion domain-hex/pkg/listing/beer.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (

// Beer defines the properties of a beer to be listed
type Beer struct {
ID int `json:"id"`
ID string `json:"id"`
Name string `json:"name"`
Brewery string `json:"brewery"`
Abv float32 `json:"abv"`
Expand Down
2 changes: 1 addition & 1 deletion domain-hex/pkg/listing/review.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
// Review defines a beer review
type Review struct {
ID string `json:"id"`
BeerID int `json:"beer_id"`
BeerID string `json:"beer_id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Score int `json:"score"`
Expand Down
12 changes: 6 additions & 6 deletions domain-hex/pkg/listing/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@ 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(int) (Beer, error)
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(int) []Review
GetAllReviews(string) []Review
}

// Service provides beer and review listing operations.
type Service interface {
GetBeer(int) (Beer, error)
GetBeer(string) (Beer, error)
GetBeers() []Beer
GetBeerReviews(int) []Review
GetBeerReviews(string) []Review
}

type service struct {
Expand All @@ -39,11 +39,11 @@ func (s *service) GetBeers() []Beer {
}

// GetBeer returns a beer
func (s *service) GetBeer(id int) (Beer, error) {
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 int) []Review {
func (s *service) GetBeerReviews(beerID string) []Review {
return s.r.GetAllReviews(beerID)
}
2 changes: 1 addition & 1 deletion domain-hex/pkg/reviewing/review.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package reviewing

// Review defines a beer review
type Review struct {
BeerID int `json:"beer_id"`
BeerID string `json:"beer_id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Score int `json:"score"`
Expand Down
6 changes: 3 additions & 3 deletions domain-hex/pkg/reviewing/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type Service interface {
}

type service struct {
rR Repository
r Repository
}

// NewService creates an adding service with the necessary dependencies
Expand All @@ -30,12 +30,12 @@ func NewService(r Repository) Service {

// AddBeerReview saves a new beer review in the database
func (s *service) AddBeerReview(r Review) {
_ = s.rR.AddReview(r) // error handling omitted for simplicity
_ = 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.rR.AddReview(rr) // error handling omitted for simplicity
_ = s.r.AddReview(rr) // error handling omitted for simplicity
}
}
20 changes: 20 additions & 0 deletions domain-hex/pkg/storage/idgen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package storage

import (
"crypto/rand"
"fmt"
)

// GetID returns a random ID string of the format prefix_random16chars, e.g. beer_ts72nf6ak8dts73g.
// This is a simple (naive) implementation using the rand package,
// 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)
_, err := rand.Read(b)
if err != nil {
return "", err
}

return fmt.Sprintf("%s_%x", prefix, b), nil
}
26 changes: 26 additions & 0 deletions domain-hex/pkg/storage/idgen_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package storage

import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"strings"
"testing"
)

func TestGetID(t *testing.T) {
id, err := GetID("testing")

require.NoError(t, err)

assert.True(t, strings.HasPrefix(id, "testing_"))
assert.Len(t, id, 24)
}

func TestGetIDEmptyPrefix(t *testing.T) {
id, err := GetID("")

require.NoError(t, err)

assert.True(t, strings.HasPrefix(id, "_"))
assert.Len(t, id, 17)
}
2 changes: 1 addition & 1 deletion domain-hex/pkg/storage/json/beer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import "time"

// Beer defines the storage form of a beer
type Beer struct {
ID int `json:"id"`
ID string `json:"id"`
Name string `json:"name"`
Brewery string `json:"brewery"`
Abv float32 `json:"abv"`
Expand Down
29 changes: 11 additions & 18 deletions domain-hex/pkg/storage/json/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ package json
import (
"encoding/json"
"fmt"
"github.com/katzien/go-structure-examples/domain-hex/pkg/storage"
"log"
"path"
"runtime"
"strconv"
"time"

"github.com/katzien/go-structure-examples/domain-hex/pkg/adding"
Expand Down Expand Up @@ -48,27 +49,21 @@ func NewStorage() (*Storage, error) {

// AddBeer saves the given beer to the repository
func (s *Storage) AddBeer(b adding.Beer) error {

existingBeers := s.GetAllBeers()
for _, e := range existingBeers {
if b.Abv == e.Abv &&
b.Brewery == e.Brewery &&
b.Name == e.Name {
return adding.ErrDuplicate
}
id, err := storage.GetID("beer")
if err != nil {
log.Fatal(err)
}

newB := Beer{
ID: len(existingBeers) + 1,
ID: id,
Created: time.Now(),
Name: b.Name,
Brewery: b.Brewery,
Abv: b.Abv,
ShortDesc: b.ShortDesc,
}

resource := strconv.Itoa(newB.ID)
if err := s.db.Write(CollectionBeer, resource, newB); err != nil {
if err := s.db.Write(CollectionBeer, newB.ID, newB); err != nil {
return err
}
return nil
Expand All @@ -78,7 +73,7 @@ func (s *Storage) AddBeer(b adding.Beer) error {
func (s *Storage) AddReview(r reviewing.Review) error {

var beer Beer
if err := s.db.Read(CollectionBeer, strconv.Itoa(r.BeerID), &beer); err != nil {
if err := s.db.Read(CollectionBeer, r.BeerID, &beer); err != nil {
return listing.ErrNotFound
}

Expand All @@ -101,13 +96,11 @@ func (s *Storage) AddReview(r reviewing.Review) error {
}

// Get returns a beer with the specified ID
func (s *Storage) GetBeer(id int) (listing.Beer, error) {
func (s *Storage) GetBeer(id string) (listing.Beer, error) {
var b Beer
var beer listing.Beer

var resource = strconv.Itoa(id)

if err := s.db.Read(CollectionBeer, resource, &b); err != nil {
if err := s.db.Read(CollectionBeer, id, &b); err != nil {
// err handling omitted for simplicity
return beer, listing.ErrNotFound
}
Expand Down Expand Up @@ -155,7 +148,7 @@ func (s *Storage) GetAllBeers() []listing.Beer {
}

// GetAll returns all reviews for a given beer
func (s *Storage) GetAllReviews(beerID int) []listing.Review {
func (s *Storage) GetAllReviews(beerID string) []listing.Review {
list := []listing.Review{}

records, err := s.db.ReadAll(CollectionReview)
Expand Down
2 changes: 1 addition & 1 deletion domain-hex/pkg/storage/json/review.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "time"
// Review defines the storage form of a beer review
type Review struct {
ID string `json:"id"`
BeerID int `json:"beer_id"`
BeerID string `json:"beer_id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Score int `json:"score"`
Expand Down
4 changes: 2 additions & 2 deletions domain-hex/pkg/storage/memory/beer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import (
"time"
)

// Beer defines the properties of a beer to be listed
// Beer defines the storage form of a beer
type Beer struct {
ID int
ID string
Name string
Brewery string
Abv float32
Expand Down
Loading

0 comments on commit 2289b0f

Please sign in to comment.