package rest
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"github.com/PacktPublishing/Hands-On-Dependency-Injection-in-Go/ch12/acme/internal/logging"
"github.com/PacktPublishing/Hands-On-Dependency-Injection-in-Go/ch12/acme/internal/modules/get"
"github.com/gorilla/mux"
)
const (
// default person id (returned on error)
defaultPersonID = 0
// key in the mux where the ID is stored
muxVarID = "id"
)
// GetModel will load a registration
//go:generate mockery -name=GetModel -case underscore -testonly -inpkg -note @generated
type GetModel interface {
Do(ID int) (*get.Person, error)
}
// GetConfig is the config for the Get Handler
type GetConfig interface {
Logger() logging.Logger
}
// NewGetHandler is the constructor for GetHandler
func NewGetHandler(cfg GetConfig, model GetModel) *GetHandler {
return &GetHandler{
cfg: cfg,
getter: model,
}
}
// GetHandler is the HTTP handler for the "Get Person" endpoint
// In this simplified example we are assuming all possible errors are user errors and returning "bad request" HTTP 400
// or "not found" HTTP 404
// There are some programmer errors possible but hopefully these will be caught in testing.
type GetHandler struct {
cfg GetConfig
getter GetModel
}
// ServeHTTP implements http.Handler
func (h *GetHandler) ServeHTTP(response http.ResponseWriter, request *http.Request) {
// extract person id from request
id, err := h.extractID(request)
if err != nil {
// output error
response.WriteHeader(http.StatusBadRequest)
return
}
// attempt get
person, err := h.getter.Do(id)
if err != nil {
// not need to log here as we can expect other layers to do so
response.WriteHeader(http.StatusNotFound)
return
}
// happy path
err = h.writeJSON(response, person)
if err != nil {
// this error should not happen but if it does there is nothing we can do to recover
response.WriteHeader(http.StatusInternalServerError)
}
}
// extract the person ID from the request
func (h *GetHandler) extractID(request *http.Request) (int, error) {
// ID is part of the URL, so we extract it from there
vars := mux.Vars(request)
idAsString, exists := vars[muxVarID]
if !exists {
// log and return error
err := errors.New("[get] person id missing from request")
h.cfg.Logger().Warn(err.Error())
return defaultPersonID, err
}
// convert ID to int
id, err := strconv.Atoi(idAsString)
if err != nil {
// log and return error
err = fmt.Errorf("[get] failed to convert person id into a number. err: %s", err)
h.cfg.Logger().Error(err.Error())
return defaultPersonID, err
}
return id, nil
}
// output the supplied person as JSON
func (h *GetHandler) writeJSON(writer io.Writer, person *get.Person) error {
output := &getResponseFormat{
ID: person.ID,
FullName: person.FullName,
Phone: person.Phone,
Currency: person.Currency,
Price: person.Price,
}
// call to http.ResponseWriter.Write() will cause HTTP OK (200) to be output as well
return json.NewEncoder(writer).Encode(output)
}
// the JSON response format
type getResponseFormat struct {
ID int `json:"id"`
FullName string `json:"name"`
Phone string `json:"phone"`
Currency string `json:"currency"`
Price float64 `json:"price"`
}
package rest
import (
"encoding/json"
"io"
"net/http"
"github.com/PacktPublishing/Hands-On-Dependency-Injection-in-Go/ch12/acme/internal/modules/list"
)
// ListModel will load all registrations
//go:generate mockery -name=ListModel -case underscore -testonly -inpkg -note @generated
type ListModel interface {
Do() ([]*list.Person, error)
}
// NewLister is the constructor for ListHandler
func NewListHandler(model ListModel) *ListHandler {
return &ListHandler{
lister: model,
}
}
// ListHandler is the HTTP handler for the "List Do people" endpoint
// In this simplified example we are assuming all possible errors are system errors (HTTP 500)
type ListHandler struct {
lister ListModel
}
// ServeHTTP implements http.Handler
func (h *ListHandler) ServeHTTP(response http.ResponseWriter, request *http.Request) {
// attempt loadAll
people, err := h.lister.Do()
if err != nil {
// not need to log here as we can expect other layers to do so
response.WriteHeader(http.StatusNotFound)
return
}
// happy path
err = h.writeJSON(response, people)
if err != nil {
// this error should not happen but if it does there is nothing we can do to recover
response.WriteHeader(http.StatusInternalServerError)
}
}
// output the result as JSON
func (h *ListHandler) writeJSON(writer io.Writer, people []*list.Person) error {
output := &listResponseFormat{
People: make([]*listResponseItemFormat, len(people)),
}
for index, record := range people {
output.People[index] = &listResponseItemFormat{
ID: record.ID,
FullName: record.FullName,
Phone: record.Phone,
}
}
// call to http.ResponseWriter.Write() will cause HTTP OK (200) to be output as well
return json.NewEncoder(writer).Encode(output)
}
type listResponseFormat struct {
People []*listResponseItemFormat `json:"people"`
}
type listResponseItemFormat struct {
ID int `json:"id"`
FullName string `json:"name"`
Phone string `json:"phone"`
}
package rest
import (
"net/http"
)
func notFoundHandler(response http.ResponseWriter, _ *http.Request) {
response.WriteHeader(http.StatusNotFound)
_, _ = response.Write([]byte(`Not found`))
}
package rest
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/PacktPublishing/Hands-On-Dependency-Injection-in-Go/ch12/acme/internal/modules/register"
)
// RegisterModel will validate and save a registration
//go:generate mockery -name=RegisterModel -case underscore -testonly -inpkg -note @generated
type RegisterModel interface {
Do(ctx context.Context, in *register.Person) (int, error)
}
// NewRegisterHandler is the constructor for RegisterHandler
func NewRegisterHandler(model RegisterModel) *RegisterHandler {
return &RegisterHandler{
registerer: model,
}
}
// RegisterHandler is the HTTP handler for the "Register" endpoint
// In this simplified example we are assuming all possible errors are user errors and returning "bad request" HTTP 400.
// There are some programmer errors possible but hopefully these will be caught in testing.
type RegisterHandler struct {
registerer RegisterModel
}
// ServeHTTP implements http.Handler
func (h *RegisterHandler) ServeHTTP(response http.ResponseWriter, request *http.Request) {
// set latency budget for this API
subCtx, cancel := context.WithTimeout(request.Context(), 1500*time.Millisecond)
defer cancel()
// extract payload from request
requestPayload, err := h.extractPayload(request)
if err != nil {
// output error
response.WriteHeader(http.StatusBadRequest)
return
}
// call the business logic using the request data and context
id, err := h.register(subCtx, requestPayload)
if err != nil {
// not need to log here as we can expect other layers to do so
response.WriteHeader(http.StatusBadRequest)
return
}
// happy path
response.Header().Add("Location", fmt.Sprintf("/person/%d/", id))
response.WriteHeader(http.StatusCreated)
}
// extract payload from request
func (h *RegisterHandler) extractPayload(request *http.Request) (*registerRequest, error) {
requestPayload := ®isterRequest{}
decoder := json.NewDecoder(request.Body)
err := decoder.Decode(requestPayload)
if err != nil {
return nil, err
}
return requestPayload, nil
}
// call the logic layer
func (h *RegisterHandler) register(ctx context.Context, requestPayload *registerRequest) (int, error) {
person := ®ister.Person{
FullName: requestPayload.FullName,
Phone: requestPayload.Phone,
Currency: requestPayload.Currency,
}
return h.registerer.Do(ctx, person)
}
// register endpoint request format
type registerRequest struct {
// FullName of the person
FullName string `json:"fullName"`
// Phone of the person
Phone string `json:"phone"`
// Currency the wish to register in
Currency string `json:"currency"`
}
package rest
import (
"net/http"
"github.com/PacktPublishing/Hands-On-Dependency-Injection-in-Go/ch12/acme/internal/logging"
"github.com/gorilla/mux"
)
// Config is the config for the REST package
type Config interface {
Logger() logging.Logger
BindAddress() string
}
// New will create and initialize the server
func New(cfg Config,
getModel GetModel,
listModel ListModel,
registerModel RegisterModel) *Server {
return &Server{
address: cfg.BindAddress(),
handlerGet: NewGetHandler(cfg, getModel),
handlerList: NewListHandler(listModel),
handlerNotFound: notFoundHandler,
handlerRegister: NewRegisterHandler(registerModel),
}
}
// Server is the HTTP REST server
type Server struct {
address string
server *http.Server
handlerGet http.Handler
handlerList http.Handler
handlerNotFound http.HandlerFunc
handlerRegister http.Handler
}
// Listen will start a HTTP rest for this service
func (s *Server) Listen(stop <-chan struct{}) {
router := s.buildRouter()
// create the HTTP server
s.server = &http.Server{
Handler: router,
Addr: s.address,
}
// listen for shutdown
go func() {
// wait for shutdown signal
<-stop
_ = s.server.Close()
}()
// start the HTTP server
_ = s.server.ListenAndServe()
}
// configure the endpoints to handlers
func (s *Server) buildRouter() http.Handler {
router := mux.NewRouter()
// map URL endpoints to HTTP handlers
router.Handle("/person/{id}/", s.handlerGet).Methods("GET")
router.Handle("/person/list", s.handlerList).Methods("GET")
router.Handle("/person/register", s.handlerRegister).Methods("POST")
// convert a "catch all" not found handler
router.NotFoundHandler = s.handlerNotFound
return router
}