package router import ( "encoding/json" "github.com/eslider/superherohub/pkg/deesee" "github.com/eslider/superherohub/pkg/deesee/storage" "log" "net/http" "strings" "github.com/gorilla/mux" ) // Router is a custom router that encapsulates the mux.Router type Router struct { 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 } // 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(heros *storage.DeeSee, key int, logger *log.Logger) *Router { // Set the logger if logger == nil { logger = log.Default() } r := &Router{ store: heros, 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 // 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 } // Store superhero if err = r.store.Store(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) }) }