Clean up JSON responses with responder library

zikaeroh created

Change summary

helpers.go                    |   5 -
internal/responder/respond.go |  65 +++++++++++++++++++++++
main.go                       | 101 ++++++++++++++++++++----------------
3 files changed, 120 insertions(+), 51 deletions(-)

Detailed changes

helpers.go 🔗

@@ -1,17 +1,12 @@
 package main
 
 import (
-	"net/http"
 	"reflect"
 
 	"github.com/gofrs/uuid"
 	"github.com/tomwright/queryparam/v4"
 )
 
-func httpErr(w http.ResponseWriter, code int) {
-	http.Error(w, http.StatusText(code), code)
-}
-
 func stringPtr(s string) *string {
 	return &s
 }

internal/responder/respond.go 🔗

@@ -0,0 +1,65 @@
+package responder
+
+import (
+	"encoding/json"
+	"net/http"
+)
+
+type defaultResponse struct {
+	Status  int    `json:"status"`
+	Message string `json:"message"`
+}
+
+type response struct {
+	value  *interface{}
+	status int
+	pretty bool
+}
+
+type Option func(r *response)
+
+func Pretty(pretty bool) Option {
+	return func(r *response) {
+		r.pretty = pretty
+	}
+}
+
+func Status(status int) Option {
+	return func(r *response) {
+		r.status = status
+	}
+}
+
+func Body(v interface{}) Option {
+	return func(r *response) {
+		r.value = &v
+	}
+}
+
+func Respond(w http.ResponseWriter, opts ...Option) {
+	r := &response{
+		status: http.StatusOK,
+	}
+
+	for _, opt := range opts {
+		opt(r)
+	}
+
+	enc := json.NewEncoder(w)
+
+	if r.pretty {
+		enc.SetIndent("    ", "")
+	}
+
+	w.Header().Add("Content-Type", "application/json")
+	w.WriteHeader(r.status)
+
+	if v := r.value; v != nil {
+		_ = enc.Encode(*v)
+	} else {
+		_ = enc.Encode(&defaultResponse{
+			Status:  r.status,
+			Message: http.StatusText(r.status),
+		})
+	}
+}

main.go 🔗

@@ -17,6 +17,7 @@ import (
 	"github.com/prometheus/client_golang/prometheus/promhttp"
 	"github.com/tomwright/queryparam/v4"
 	"github.com/zikaeroh/codies/internal/protocol"
+	"github.com/zikaeroh/codies/internal/responder"
 	"github.com/zikaeroh/codies/internal/server"
 	"github.com/zikaeroh/codies/internal/version"
 	"golang.org/x/sync/errgroup"
@@ -83,19 +84,18 @@ func main() {
 		r.Use(middleware.NoCache)
 
 		r.Get("/api/time", func(w http.ResponseWriter, r *http.Request) {
-			w.Header().Add("Content-Type", "application/json")
-			_ = json.NewEncoder(w).Encode(&protocol.TimeResponse{Time: time.Now()})
+			responder.Respond(w, responder.Body(&protocol.TimeResponse{Time: time.Now()}))
 		})
 
 		r.Get("/api/stats", func(w http.ResponseWriter, r *http.Request) {
 			rooms, clients := srv.Stats()
-
-			enc := json.NewEncoder(w)
-			enc.SetIndent("", "    ")
-			_ = enc.Encode(&protocol.StatsResponse{
-				Rooms:   rooms,
-				Clients: clients,
-			})
+			responder.Respond(w,
+				responder.Body(&protocol.StatsResponse{
+					Rooms:   rooms,
+					Clients: clients,
+				}),
+				responder.Pretty(true),
+			)
 		})
 
 		r.Group(func(r chi.Router) {
@@ -106,18 +106,16 @@ func main() {
 			r.Get("/api/exists", func(w http.ResponseWriter, r *http.Request) {
 				query := &protocol.ExistsQuery{}
 				if err := queryparam.Parse(r.URL.Query(), query); err != nil {
-					httpErr(w, http.StatusBadRequest)
+					responder.Respond(w, responder.Status(http.StatusBadRequest))
 					return
 				}
 
 				room := srv.FindRoomByID(query.RoomID)
 				if room == nil {
-					w.WriteHeader(http.StatusNotFound)
+					responder.Respond(w, responder.Status(http.StatusNotFound))
 				} else {
-					w.WriteHeader(http.StatusOK)
+					responder.Respond(w, responder.Status(http.StatusOK))
 				}
-
-				_, _ = w.Write([]byte("."))
 			})
 
 			r.Post("/api/room", func(w http.ResponseWriter, r *http.Request) {
@@ -125,76 +123,88 @@ func main() {
 
 				req := &protocol.RoomRequest{}
 				if err := json.NewDecoder(r.Body).Decode(req); err != nil {
-					httpErr(w, http.StatusBadRequest)
+					responder.Respond(w, responder.Status(http.StatusBadRequest))
 					return
 				}
 
-				w.Header().Add("Content-Type", "application/json")
-
 				if msg, valid := req.Valid(); !valid {
-					resp := &protocol.RoomResponse{
-						Error: stringPtr(msg),
-					}
-					w.WriteHeader(http.StatusBadRequest)
-					_ = json.NewEncoder(w).Encode(resp)
+					responder.Respond(w,
+						responder.Status(http.StatusBadRequest),
+						responder.Body(&protocol.RoomResponse{
+							Error: stringPtr(msg),
+						}),
+					)
 					return
 				}
 
-				resp := &protocol.RoomResponse{}
-
+				var room *server.Room
 				if req.Create {
-					room, err := srv.CreateRoom(req.RoomName, req.RoomPass)
+					var err error
+					room, err = srv.CreateRoom(req.RoomName, req.RoomPass)
 					if err != nil {
 						switch err {
 						case server.ErrRoomExists:
-							resp.Error = stringPtr("Room already exists.")
-							w.WriteHeader(http.StatusBadRequest)
+							responder.Respond(w,
+								responder.Status(http.StatusBadRequest),
+								responder.Body(&protocol.RoomResponse{
+									Error: stringPtr("Room already exists."),
+								}),
+							)
 						case server.ErrTooManyRooms:
-							resp.Error = stringPtr("Too many rooms.")
-							w.WriteHeader(http.StatusServiceUnavailable)
+							responder.Respond(w,
+								responder.Status(http.StatusServiceUnavailable),
+								responder.Body(&protocol.RoomResponse{
+									Error: stringPtr("Too many rooms."),
+								}),
+							)
 						default:
-							resp.Error = stringPtr("An unknown error occurred.")
-							w.WriteHeader(http.StatusInternalServerError)
+							responder.Respond(w,
+								responder.Status(http.StatusInternalServerError),
+								responder.Body(&protocol.RoomResponse{
+									Error: stringPtr("An unknown error occurred."),
+								}),
+							)
 						}
-					} else {
-						resp.ID = &room.ID
-						w.WriteHeader(http.StatusOK)
+						return
 					}
 				} else {
-					room := srv.FindRoom(req.RoomName)
+					room = srv.FindRoom(req.RoomName)
 					if room == nil || room.Password != req.RoomPass {
-						resp.Error = stringPtr("Room not found or password does not match.")
-						w.WriteHeader(http.StatusNotFound)
-					} else {
-						resp.ID = &room.ID
-						w.WriteHeader(http.StatusOK)
+						responder.Respond(w,
+							responder.Status(http.StatusNotFound),
+							responder.Body(&protocol.RoomResponse{
+								Error: stringPtr("Room not found or password does not match."),
+							}),
+						)
+						return
 					}
 				}
 
-				_ = json.NewEncoder(w).Encode(resp)
+				responder.Respond(w, responder.Body(&protocol.RoomResponse{
+					ID: &room.ID,
+				}))
 			})
 
 			r.Get("/api/ws", func(w http.ResponseWriter, r *http.Request) {
 				query := &protocol.WSQuery{}
 				if err := queryparam.Parse(r.URL.Query(), query); err != nil {
-					httpErr(w, http.StatusBadRequest)
+					responder.Respond(w, responder.Status(http.StatusBadRequest))
 					return
 				}
 
 				if _, valid := query.Valid(); !valid {
-					httpErr(w, http.StatusBadRequest)
+					responder.Respond(w, responder.Status(http.StatusBadRequest))
 					return
 				}
 
 				room := srv.FindRoomByID(query.RoomID)
 				if room == nil {
-					httpErr(w, http.StatusNotFound)
+					responder.Respond(w, responder.Status(http.StatusBadRequest))
 					return
 				}
 
 				c, err := websocket.Accept(w, r, wsOpts)
 				if err != nil {
-					log.Println(err)
 					return
 				}
 
@@ -258,7 +268,6 @@ func checkVersion(next http.Handler) http.Handler {
 		if r.Header.Get("Upgrade") == "websocket" {
 			c, err := websocket.Accept(w, r, wsOpts)
 			if err != nil {
-				log.Println(err)
 				return
 			}
 			c.Close(4418, reason)