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_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:
|
||||||
|
|||||||
@@ -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,16 +12,13 @@ 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 (
|
||||||
key = 5 // DeeSee encryption key
|
dataPath = "data/heros.json" // store to JSON file
|
||||||
err error
|
port = 8080 // Port to serve
|
||||||
|
key = 5 // DeeSee encryption key
|
||||||
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create logger
|
// Create logger
|
||||||
@@ -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
1
dist/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
*
|
|
||||||
@@ -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)
|
||||||
@@ -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(`{
|
||||||
@@ -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 {
|
||||||
|
|||||||
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