184 lines
5.0 KiB
Go
184 lines
5.0 KiB
Go
package router
|
|
|
|
import (
|
|
"encoding/json"
|
|
"github.com/eslider/superherohub/pkg/deesee"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gorilla/mux"
|
|
)
|
|
|
|
// Router is a custom router that encapsulates the mux.Router
|
|
type Router struct {
|
|
store Storage // 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
|
|
}
|
|
|
|
type Storage interface {
|
|
// List of superheroes
|
|
List() []*deesee.Superhero
|
|
|
|
// Put stores a superhero in the store.
|
|
Put(*deesee.Superhero) error
|
|
}
|
|
|
|
// 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 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 New(store Storage, key int, logger *log.Logger) *Router {
|
|
// Set the logger
|
|
if logger == nil {
|
|
logger = log.Default()
|
|
}
|
|
|
|
r := &Router{
|
|
store: store,
|
|
key: key,
|
|
logger: logger,
|
|
router: mux.NewRouter(),
|
|
}
|
|
|
|
r.router.Use(r.loggingMiddleware)
|
|
|
|
// Set a custom 404 handler
|
|
r.router.NotFoundHandler = http.HandlerFunc(r.HandleNotFound)
|
|
|
|
// Set `getSuperHeroes` as the handler for the route
|
|
r.router.HandleFunc("/superheroes", r.GetSuperHeroes).Methods(http.MethodGet)
|
|
|
|
// Set `putSuperHero` as the handler for the route
|
|
r.router.HandleFunc("/superheroes", r.StoreSuperHero).Methods(http.MethodPut)
|
|
|
|
// Enable CORS by uncommenting the following line
|
|
// router.Use(mux.CORSMethodMiddleware(router))
|
|
|
|
// Maybe better to use /api prefix for all routes?
|
|
// subrouter := router.PathPrefix("/api").Subrouter()
|
|
return r
|
|
}
|
|
|
|
// GetHandler returns the encapsulated router
|
|
func (r *Router) GetHandler() http.Handler {
|
|
return r.router
|
|
}
|
|
|
|
// GetSuperHeroes returns a list of superheroes
|
|
// with optional filtering and encryption of identities.
|
|
//
|
|
// Usage:
|
|
//
|
|
// - Retrieve all superheroes
|
|
//
|
|
// GET /superheroes
|
|
//
|
|
// - Retrieve all superheroes with encrypted identities:
|
|
//
|
|
// GET /superheroes?encrypted=true
|
|
//
|
|
// - Retrieve superheroes that match given superpower(s)
|
|
//
|
|
// GET /superheroes?superpowers=flight,super-strength
|
|
//
|
|
// - Retrieve superheroes that match given superpower(s) with encrypted identities
|
|
//
|
|
// GET /superheroes?superpowers=flight,super-strength&encrypted=true
|
|
func (r *Router) GetSuperHeroes(w http.ResponseWriter, req *http.Request) {
|
|
var (
|
|
// Resulting
|
|
heroes = r.store.List()
|
|
// Get query parameter values
|
|
params = req.URL.Query()
|
|
err error
|
|
)
|
|
|
|
// Filter superheroes by superpowers?
|
|
// The name of the power should be long enough to search for, at least 3 chars.
|
|
if powerFilter := params.Get("superpowers"); len(powerFilter) > 2 {
|
|
|
|
// Here we use the `strings` package to split comma-separated power names.
|
|
// Maybe it's better to prevent send special chars by remove with regular expression.
|
|
powers := strings.Split(powerFilter, ",")
|
|
heroes = deesee.SearchByPowers(heroes, powers)
|
|
}
|
|
|
|
// Encrypt identities with DeeSee encryption algorithm?
|
|
if strings.EqualFold(params.Get("encrypted"), "true") {
|
|
// Encrypt identities
|
|
heroes = deesee.EncryptHerosIdentities(heroes, r.key)
|
|
}
|
|
|
|
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)
|
|
return
|
|
}
|
|
}
|
|
|
|
// StoreSuperHero saves one superhero
|
|
func (r *Router) StoreSuperHero(w http.ResponseWriter, req *http.Request) {
|
|
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
|
|
}
|
|
|
|
// Put superhero
|
|
if err = r.store.Put(hero); err != nil {
|
|
http.Error(w, err.Error(), http.StatusConflict)
|
|
return
|
|
}
|
|
|
|
// Return JSON
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
|
|
// Response body
|
|
_, err = w.Write([]byte(`{
|
|
"message": "Superhero stored successfully.",
|
|
"status": "success"
|
|
}`))
|
|
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
// HandleNotFound is a custom 404 handler
|
|
func (r *Router) HandleNotFound(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusNotFound)
|
|
_, err := w.Write([]byte(`{"error": "not found"}`))
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
// loggingMiddleware is a middleware that logs all requests
|
|
func (r *Router) loggingMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
r.logger.Printf("Request: %s %s", req.Method, req.RequestURI)
|
|
next.ServeHTTP(w, req)
|
|
})
|
|
}
|