diff --git a/Makefile b/Makefile index b91d245..ec8271d 100644 --- a/Makefile +++ b/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: diff --git a/main.go b/cmd/hub/main.go similarity index 74% rename from main.go rename to cmd/hub/main.go index 13f4105..3460e79 100644 --- a/main.go +++ b/cmd/hub/main.go @@ -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,16 +12,13 @@ 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 ( - key = 5 // DeeSee encryption key - err error + dataPath = "data/heros.json" // store to JSON file + port = 8080 // Port to serve + key = 5 // DeeSee encryption key + err error ) // Create logger @@ -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) diff --git a/dist/.gitignore b/dist/.gitignore deleted file mode 100644 index 72e8ffc..0000000 --- a/dist/.gitignore +++ /dev/null @@ -1 +0,0 @@ -* diff --git a/api/router.go b/internal/router/router.go similarity index 81% rename from api/router.go rename to internal/router/router.go index c530c65..79256f0 100644 --- a/api/router.go +++ b/internal/router/router.go @@ -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 - 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 + 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) diff --git a/router_test.go b/internal/router/router_test.go similarity index 88% rename from router_test.go rename to internal/router/router_test.go index 94b5e64..374442a 100644 --- a/router_test.go +++ b/internal/router/router_test.go @@ -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(`{ diff --git a/pkg/deesee/model.go b/pkg/deesee/model.go index 3dcd266..54381ad 100644 --- a/pkg/deesee/model.go +++ b/pkg/deesee/model.go @@ -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 { diff --git a/pkg/deesee/storage/storage.go b/pkg/deesee/storage/storage.go new file mode 100644 index 0000000..5f9d989 --- /dev/null +++ b/pkg/deesee/storage/storage.go @@ -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 +}