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_PATH=dist
PACKAGE_PATH=github.com/eslider/superherohub
DIST_NAME ?=deesee-superhero-service
DIST_PATH ?=build/package
PORT ?=8080
KEY ?=5
objects =$(patsubst %.go,%.o,$(wildcard *.go))
BINARY_PATH=${DIST_PATH}/${DIST_NAME}
PACKAGE_PATH=github.com/eslider/superherohub
# Build the project
# Build the project service
build:
go build -o ${BINARY_PATH} ${PACKAGE_PATH}
go build -o ${BINARY_PATH} cmd/hub/main.go
# Run the project
run:
go build -o ${BINARY_PATH} ${PACKAGE_PATH}
go build -o ${BINARY_PATH} cmd/hub/main.go
${BINARY_PATH}
# Test and build the project
# Test the project
test:
go test -v -cover ${PACKAGE_PATH}/pkg/deesee
go test -v -cover ${PACKAGE_PATH}/pkg/people
go test -v -cover -count 10 ${PACKAGE_PATH}
go test -v -cover ./...
echo ${PORT}
# This test ork only by developer
# Please ignore this
@@ -29,7 +31,7 @@ test-store:
-H 'Cache-Control: no-cache' \
-H "Content-Type: application/json" \
-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:

View File

@@ -2,8 +2,8 @@ package main
import (
"fmt"
"github.com/eslider/superherohub/api"
"github.com/eslider/superherohub/pkg/deesee"
"github.com/eslider/superherohub/internal/router"
"github.com/eslider/superherohub/pkg/deesee/storage"
"github.com/eslider/superherohub/pkg/file"
"log"
"net/http"
@@ -12,14 +12,11 @@ import (
"time"
)
var (
dataPath = "data/heros.json" // Heros to JSON file
port = 8080 // Port to serve
)
// main is the entry point of the application
func main() {
var (
dataPath = "data/heros.json" // store to JSON file
port = 8080 // Port to serve
key = 5 // DeeSee encryption key
err error
)
@@ -49,9 +46,10 @@ func main() {
// logger.Print("Key number should be between 1 and 25")
// 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
if os.Getenv("DATA_PATH") != "" {
@@ -63,7 +61,9 @@ func main() {
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 {
logger.Fatalf("Invalid data format: %s", dataPath)
}
@@ -71,12 +71,15 @@ func main() {
// Create a new server
srv := &http.Server{
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,
ReadTimeout: 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
if err := srv.ListenAndServe(); err != nil {
logger.Fatal(err)

1
dist/.gitignore vendored
View File

@@ -1 +0,0 @@
*

View File

@@ -1,9 +1,9 @@
package api
package router
import (
"encoding/json"
"fmt"
"github.com/eslider/superherohub/pkg/deesee"
"github.com/eslider/superherohub/pkg/deesee/storage"
"log"
"net/http"
"strings"
@@ -13,31 +13,31 @@ import (
// Router is a custom router that encapsulates the mux.Router
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
logger *log.Logger // The logger to use for this router
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
// 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.
//
// This makes it easy to update routes as they
// require different resources and development progresses.
//
// 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
if logger == nil {
logger = log.Default()
}
r := &Router{
Heros: heros,
store: heros,
key: key,
logger: logger,
router: mux.NewRouter(),
@@ -90,7 +90,7 @@ func (r *Router) GetHandler() http.Handler {
func (r *Router) GetSuperHeroes(w http.ResponseWriter, req *http.Request) {
var (
// Resulting
heroes = r.Heros
heroes = *r.store
// Get query parameter values
params = req.URL.Query()
err error
@@ -113,6 +113,7 @@ func (r *Router) GetSuperHeroes(w http.ResponseWriter, req *http.Request) {
}
w.Header().Set("Content-Type", "application/json")
// Return JSON
if err = json.NewEncoder(w).Encode(heroes); err != nil {
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
func (r *Router) StoreSuperHero(w http.ResponseWriter, req *http.Request) {
var err error
hero := deesee.NewSuperhero()
var (
err error
hero = &deesee.Superhero{}
)
// Decode JSON
if err = json.NewDecoder(req.Body).Decode(hero); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Check if superhero superpower is acceptable
if !hero.IsAcceptable() {
http.Error(w, fmt.Sprintf("Hero power is not acceptable: %s", hero.Name), http.StatusExpectationFailed)
// Store superhero
if err = r.store.Store(hero); err != nil {
http.Error(w, err.Error(), http.StatusConflict)
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
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)

View File

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

View File

@@ -3,6 +3,7 @@ package deesee
import (
"encoding/json"
"github.com/eslider/superherohub/pkg/people"
"io"
"os"
"strings"
)
@@ -52,6 +53,12 @@ func (s *Superhero) Has(power string) bool {
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.
func IsAcceptable(power string) bool {
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
}