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:
26
Makefile
26
Makefile
@@ -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:
|
||||
|
||||
@@ -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
1
dist/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
*
|
||||
@@ -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)
|
||||
@@ -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(`{
|
||||
@@ -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 {
|
||||
|
||||
56
pkg/deesee/storage/storage.go
Normal file
56
pkg/deesee/storage/storage.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user