Refactor router and storage

* Split and extract data-logic from application level
* Move storage to DeeSee package
* Move router into internal package
* Fix Makefile
* Fix documentation
* Fix tests
This commit is contained in:
2023-02-08 17:44:58 +00:00
parent 2cfe36590f
commit c60839bcfb
7 changed files with 135 additions and 56 deletions

View File

@@ -1,24 +1,26 @@
DIST_NAME=superhero DIST_NAME ?=deesee-superhero-service
DIST_PATH=dist DIST_PATH ?=build/package
PACKAGE_PATH=github.com/eslider/superherohub PORT ?=8080
KEY ?=5
objects =$(patsubst %.go,%.o,$(wildcard *.go))
BINARY_PATH=${DIST_PATH}/${DIST_NAME} BINARY_PATH=${DIST_PATH}/${DIST_NAME}
PACKAGE_PATH=github.com/eslider/superherohub
# Build the project # Build the project service
build: build:
go build -o ${BINARY_PATH} ${PACKAGE_PATH} go build -o ${BINARY_PATH} cmd/hub/main.go
# Run the project # Run the project
run: run:
go build -o ${BINARY_PATH} ${PACKAGE_PATH} go build -o ${BINARY_PATH} cmd/hub/main.go
${BINARY_PATH} ${BINARY_PATH}
# Test and build the project # Test the project
test: test:
go test -v -cover ${PACKAGE_PATH}/pkg/deesee go test -v -cover ./...
go test -v -cover ${PACKAGE_PATH}/pkg/people echo ${PORT}
go test -v -cover -count 10 ${PACKAGE_PATH}
# This test ork only by developer # This test ork only by developer
# Please ignore this # Please ignore this
@@ -29,7 +31,7 @@ test-store:
-H 'Cache-Control: no-cache' \ -H 'Cache-Control: no-cache' \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"name":"supermans","identity":{"firstName":"Kent","lastName":"Clark"},"superpowers":["flight","strength","invulnerability"],"birthday":"1977-04-18"}' \ -d '{"name":"supermans","identity":{"firstName":"Kent","lastName":"Clark"},"superpowers":["flight","strength","invulnerability"],"birthday":"1977-04-18"}' \
http://127.0.0.1:8080/superheroes http://127.0.0.1:${PORT}/superheroes
# Clean the project # Clean the project
clean: clean:

View File

@@ -2,8 +2,8 @@ package main
import ( import (
"fmt" "fmt"
"github.com/eslider/superherohub/api" "github.com/eslider/superherohub/internal/router"
"github.com/eslider/superherohub/pkg/deesee" "github.com/eslider/superherohub/pkg/deesee/storage"
"github.com/eslider/superherohub/pkg/file" "github.com/eslider/superherohub/pkg/file"
"log" "log"
"net/http" "net/http"
@@ -12,14 +12,11 @@ import (
"time" "time"
) )
var (
dataPath = "data/heros.json" // Heros to JSON file
port = 8080 // Port to serve
)
// main is the entry point of the application // main is the entry point of the application
func main() { func main() {
var ( var (
dataPath = "data/heros.json" // store to JSON file
port = 8080 // Port to serve
key = 5 // DeeSee encryption key key = 5 // DeeSee encryption key
err error err error
) )
@@ -49,9 +46,10 @@ func main() {
// logger.Print("Key number should be between 1 and 25") // logger.Print("Key number should be between 1 and 25")
// logger.Fatalf("Invalid key number : %d", key) // logger.Fatalf("Invalid key number : %d", key)
//} //}
} else {
logger.Fatalf("Key is not set. Please set KEY environment variable.")
} }
//else {
// logger.Fatalf("Key is not set. Please set KEY environment variable.")
//}
// Get data path from environment variable // Get data path from environment variable
if os.Getenv("DATA_PATH") != "" { if os.Getenv("DATA_PATH") != "" {
@@ -63,7 +61,9 @@ func main() {
logger.Fatalf("Invalid data path: %s", dataPath) logger.Fatalf("Invalid data path: %s", dataPath)
} }
herous, err := deesee.Load(dataPath) s, err := storage.New(dataPath)
//heros, err := deesee.Load(dataPath)
if err != nil { if err != nil {
logger.Fatalf("Invalid data format: %s", dataPath) logger.Fatalf("Invalid data format: %s", dataPath)
} }
@@ -71,12 +71,15 @@ func main() {
// Create a new server // Create a new server
srv := &http.Server{ srv := &http.Server{
Addr: fmt.Sprintf(":%d", port), // Define port and address to serve Addr: fmt.Sprintf(":%d", port), // Define port and address to serve
Handler: api.NewRouter(herous, key, logger).GetHandler(), Handler: router.New(s, key, logger).GetHandler(),
ErrorLog: logger, ErrorLog: logger,
ReadTimeout: 240 * time.Second, // Good practice: enforcing timeouts for serving. ReadTimeout: 240 * time.Second, // Good practice: enforcing timeouts for serving.
WriteTimeout: 240 * time.Second, // Good practice: enforcing timeouts for serving. WriteTimeout: 240 * time.Second, // Good practice: enforcing timeouts for serving.
} }
// Log info
logger.Printf("Serving on port %d", port)
// Serve and handle error // Serve and handle error
if err := srv.ListenAndServe(); err != nil { if err := srv.ListenAndServe(); err != nil {
logger.Fatal(err) logger.Fatal(err)

1
dist/.gitignore vendored
View File

@@ -1 +0,0 @@
*

View File

@@ -1,9 +1,9 @@
package api package router
import ( import (
"encoding/json" "encoding/json"
"fmt"
"github.com/eslider/superherohub/pkg/deesee" "github.com/eslider/superherohub/pkg/deesee"
"github.com/eslider/superherohub/pkg/deesee/storage"
"log" "log"
"net/http" "net/http"
"strings" "strings"
@@ -13,31 +13,31 @@ import (
// Router is a custom router that encapsulates the mux.Router // Router is a custom router that encapsulates the mux.Router
type Router struct { type Router struct {
Heros []*deesee.Superhero // The Heros where superheroes are stored store *storage.DeeSee // The store where superheroes are stored
router *mux.Router // Encapsulated mux router router *mux.Router // Encapsulated mux router
logger *log.Logger // The logger to use for this router logger *log.Logger // The logger to use for this router
key int // The key to use for DeeSee encryption key int // The key to use for DeeSee encryption
} }
// NewRouter creates a new encapsulated router. // New creates a new encapsulated router.
// //
// The pattern I use is to define a custom router structure // The pattern I use is to define a custom router structure
// that has a `mux.Router` as the field and also encapsulates things // that has a `mux.Router` as the field and also encapsulates things
// like the JSON Heros to load, encryption-keys, database connection, // like the JSON store to load, encryption-keys, database connection,
// application configuration, and so on. // application configuration, and so on.
// //
// This makes it easy to update routes as they // This makes it easy to update routes as they
// require different resources and development progresses. // require different resources and development progresses.
// //
// And handlers shouldn't have prefix like "handle", course they are methods of the Router // And handlers shouldn't have prefix like "handle", course they are methods of the Router
func NewRouter(heros []*deesee.Superhero, key int, logger *log.Logger) *Router { func New(heros *storage.DeeSee, key int, logger *log.Logger) *Router {
// Set the logger // Set the logger
if logger == nil { if logger == nil {
logger = log.Default() logger = log.Default()
} }
r := &Router{ r := &Router{
Heros: heros, store: heros,
key: key, key: key,
logger: logger, logger: logger,
router: mux.NewRouter(), router: mux.NewRouter(),
@@ -90,7 +90,7 @@ func (r *Router) GetHandler() http.Handler {
func (r *Router) GetSuperHeroes(w http.ResponseWriter, req *http.Request) { func (r *Router) GetSuperHeroes(w http.ResponseWriter, req *http.Request) {
var ( var (
// Resulting // Resulting
heroes = r.Heros heroes = *r.store
// Get query parameter values // Get query parameter values
params = req.URL.Query() params = req.URL.Query()
err error err error
@@ -113,6 +113,7 @@ func (r *Router) GetSuperHeroes(w http.ResponseWriter, req *http.Request) {
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
// Return JSON // Return JSON
if err = json.NewEncoder(w).Encode(heroes); err != nil { if err = json.NewEncoder(w).Encode(heroes); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -122,27 +123,23 @@ func (r *Router) GetSuperHeroes(w http.ResponseWriter, req *http.Request) {
// StoreSuperHero saves one superhero // StoreSuperHero saves one superhero
func (r *Router) StoreSuperHero(w http.ResponseWriter, req *http.Request) { func (r *Router) StoreSuperHero(w http.ResponseWriter, req *http.Request) {
var err error var (
hero := deesee.NewSuperhero() err error
hero = &deesee.Superhero{}
)
// Decode JSON
if err = json.NewDecoder(req.Body).Decode(hero); err != nil { if err = json.NewDecoder(req.Body).Decode(hero); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
// Check if superhero superpower is acceptable // Store superhero
if !hero.IsAcceptable() { if err = r.store.Store(hero); err != nil {
http.Error(w, fmt.Sprintf("Hero power is not acceptable: %s", hero.Name), http.StatusExpectationFailed) http.Error(w, err.Error(), http.StatusConflict)
return return
} }
// Prevent duplicate superheroes
if deesee.FindByName(r.Heros, strings.TrimSpace(hero.Name)) != nil {
http.Error(w, fmt.Sprintf("Hero is already exists: %s", hero.Name), http.StatusConflict)
return
}
r.Heros = append(r.Heros, hero)
// Return JSON // Return JSON
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)

View File

@@ -1,9 +1,10 @@
package main package router
import ( import (
"encoding/json" "encoding/json"
"github.com/eslider/superherohub/api"
"github.com/eslider/superherohub/pkg/deesee" "github.com/eslider/superherohub/pkg/deesee"
"github.com/eslider/superherohub/pkg/deesee/storage"
"github.com/eslider/superherohub/pkg/file"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings" "strings"
@@ -12,14 +13,21 @@ import (
// TestWebserviceGetSuperHeros tests the HandleWebserviceRequest function // TestWebserviceGetSuperHeros tests the HandleWebserviceRequest function
func TestWebserviceGetSuperHeros(t *testing.T) { func TestWebserviceGetSuperHeros(t *testing.T) {
path, err := file.GetModRootPath()
if err != nil {
t.Fatalf("Error getting module root path: %s", err)
}
// Load heros // Load heros
heros, err := deesee.Load(dataPath) heros, err := storage.New(path + "/data/heros.json")
if err != nil { if err != nil {
t.Fatalf("Error loading heros: %heros", err) t.Fatalf("Error loading heros: %heros", err)
} }
// Create router // Create router
router := api.NewRouter(heros, 5, nil) router := New(heros, 5, nil)
// Test cases // Test cases
for _, tc := range []struct { for _, tc := range []struct {
@@ -114,13 +122,20 @@ func TestWebserviceGetSuperHeros(t *testing.T) {
// TODO: Test invalid storing data (e.g. invalid JSON) // TODO: Test invalid storing data (e.g. invalid JSON)
func TestWebserviceStoreSuperhero(t *testing.T) { func TestWebserviceStoreSuperhero(t *testing.T) {
// Load heros // Load heros
heros, err := deesee.Load(dataPath) path, err := file.GetModRootPath()
if err != nil {
t.Fatalf("Error getting module root path: %s", err)
}
// Load heros
heros, err := storage.New(path + "/data/heros.json")
if err != nil { if err != nil {
t.Fatalf("Error loading heros: %heros", err) t.Fatalf("Error loading heros: %heros", err)
} }
// Create router // Create router
router := api.NewRouter(heros, 5, nil) router := New(heros, 5, nil)
// Test with valid domain model // Test with valid domain model
req, err := http.NewRequest(http.MethodGet, "/superheroes", strings.NewReader(`{ req, err := http.NewRequest(http.MethodGet, "/superheroes", strings.NewReader(`{

View File

@@ -3,6 +3,7 @@ package deesee
import ( import (
"encoding/json" "encoding/json"
"github.com/eslider/superherohub/pkg/people" "github.com/eslider/superherohub/pkg/people"
"io"
"os" "os"
"strings" "strings"
) )
@@ -52,6 +53,12 @@ func (s *Superhero) Has(power string) bool {
return s.SuperPowers.Contains(power) return s.SuperPowers.Contains(power)
} }
// Decode JSON superhero
func (s *Superhero) Decode(reader io.Reader) (err error) {
err = json.NewDecoder(reader).Decode(s)
return
}
// IsAcceptable checks if the given power is allowed. // IsAcceptable checks if the given power is allowed.
func IsAcceptable(power string) bool { func IsAcceptable(power string) bool {
for _, p := range allowedSuperPowers { for _, p := range allowedSuperPowers {

View File

@@ -0,0 +1,56 @@
package storage
import (
"encoding/json"
"errors"
"fmt"
"github.com/eslider/superherohub/pkg/deesee"
"os"
"strings"
)
type DeeSee []*deesee.Superhero
// New DeeSee storage.
func New(path string) (d *DeeSee, err error) {
d = &DeeSee{}
err = d.Load(path)
return
}
// Load superheroes from json file
func (s *DeeSee) Load(path string) (err error) {
// Read and unmarshal s from json f path
f, err := os.OpenFile(path, os.O_RDONLY, 0644)
if err != nil {
return fmt.Errorf("unable to read f: %w", err)
}
return json.NewDecoder(f).Decode(s)
}
// Store superhero
func (s *DeeSee) Store(hero *deesee.Superhero) (err error) {
// Check if superhero superpower is acceptable
if !hero.IsAcceptable() {
return errors.New(fmt.Sprintf("Hero power is not acceptable: %s", hero.Name))
}
// Prevent to store duplicate superheroes
if deesee.FindByName(*s, strings.TrimSpace(hero.Name)) != nil {
return errors.New(fmt.Sprintf("Hero already exists: %s", hero.Name))
}
*s = append(*s, hero)
return
}
// FindByName from superheros list
func (s *DeeSee) FindByName(name string) *deesee.Superhero {
for _, hero := range *s {
if strings.EqualFold(name, hero.Name) {
return hero
}
}
return nil
}