From f455c1bc4954aa0b558dd42042b28fd07520cb2e Mon Sep 17 00:00:00 2001 From: AlejandroHerr Date: Sat, 17 May 2025 17:02:50 +0200 Subject: [PATCH 1/2] feat(api): ai pkg with helpers for chi --- go.mod | 3 + go.sum | 4 ++ pkg/api/error.go | 67 +++++++++++++++++++++ pkg/api/handler.go | 35 +++++++++++ pkg/{chi/middlewares => api}/middlewares.go | 6 +- 5 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 pkg/api/error.go create mode 100644 pkg/api/handler.go rename pkg/{chi/middlewares => api}/middlewares.go (93%) diff --git a/go.mod b/go.mod index 57d19e1..d778a51 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,9 @@ go 1.24.2 require ( github.com/go-chi/chi/v5 v5.2.1 + github.com/go-chi/render v1.0.3 github.com/golang-cz/devslog v0.0.13 github.com/google/uuid v1.6.0 ) + +require github.com/ajg/form v1.5.1 // indirect diff --git a/go.sum b/go.sum index 586159e..1d48e3e 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,9 @@ +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= +github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= github.com/golang-cz/devslog v0.0.13 h1:JkJ6PPNSOCBpYyU03v3xw7WgpChQ3AYFqgRbYBhUk/Y= github.com/golang-cz/devslog v0.0.13/go.mod h1:bSe5bm0A7Nyfqtijf1OMNgVJHlWEuVSXnkuASiE1vV8= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= diff --git a/pkg/api/error.go b/pkg/api/error.go new file mode 100644 index 0000000..f6f5218 --- /dev/null +++ b/pkg/api/error.go @@ -0,0 +1,67 @@ +package api + +import ( + "log/slog" + "net/http" + + "github.com/go-chi/render" +) + +type ErrorResponse struct { + Err error `json:"-"` // low-level runtime error + HTTPStatusCode int `json:"-"` // http response status code + + StatusText string `json:"status"` // user-level status message + ErrorText string `json:"error,omitempty"` // application-level error message, for debugging + Details interface{} `json:"details,omitempty"` +} + +func NewErrorResponse(err error, statusCode int, statusText string, errorText string, details interface{}) *ErrorResponse { + return &ErrorResponse{ + Err: err, + HTTPStatusCode: statusCode, + StatusText: statusText, + ErrorText: errorText, + Details: details, + } +} + +func (e *ErrorResponse) Error() string { + if e.Err != nil { + return e.Err.Error() + } + if e.ErrorText != "" { + return e.ErrorText + } + if e.StatusText != "" { + return e.StatusText + } + + return "" +} + +func (e *ErrorResponse) LogValue() slog.Value { + return slog.GroupValue( + slog.String("error", e.Error()), + slog.Int("http_status_code", e.HTTPStatusCode), + slog.String("status_text", e.StatusText), + slog.String("error_text", e.ErrorText), + slog.Any("details", e.Details), + ) +} + +func (e *ErrorResponse) Render(_ http.ResponseWriter, r *http.Request) error { + render.Status(r, e.HTTPStatusCode) + + return nil +} + +func RenderErrorResponse(err error) *ErrorResponse { + return &ErrorResponse{ + Err: err, + HTTPStatusCode: http.StatusUnprocessableEntity, + StatusText: "Error rendering response.", + ErrorText: err.Error(), + Details: nil, + } +} diff --git a/pkg/api/handler.go b/pkg/api/handler.go new file mode 100644 index 0000000..383ac38 --- /dev/null +++ b/pkg/api/handler.go @@ -0,0 +1,35 @@ +package api + +import ( + "log/slog" + "net/http" + + "github.com/go-chi/render" +) + +type RendererFunc func(http.ResponseWriter, *http.Request) (render.Renderer, *ErrorResponse) + +func HandleRendererFunc(fn RendererFunc, logger *slog.Logger) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var resp render.Renderer + + okResp, errResp := fn(w, r) + if errResp != nil { + logger.ErrorContext( + r.Context(), + "error in handler", + slog.Any("error", errResp), + ) + + resp = errResp + } else { + resp = okResp + } + + if err := render.Render(w, r, resp); err != nil { + logger.ErrorContext(r.Context(), "error rendering response", slog.Any("error", err)) + + render.Render(w, r, RenderErrorResponse(err)) //nolint: errcheck,gosec // ignore error + } + } +} diff --git a/pkg/chi/middlewares/middlewares.go b/pkg/api/middlewares.go similarity index 93% rename from pkg/chi/middlewares/middlewares.go rename to pkg/api/middlewares.go index d82703d..f3d096a 100644 --- a/pkg/chi/middlewares/middlewares.go +++ b/pkg/api/middlewares.go @@ -7,7 +7,7 @@ import ( "time" "github.com/go-chi/chi/v5" - chimiddleware "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/chi/v5/middleware" "github.com/google/uuid" ) @@ -16,7 +16,7 @@ type RequestIDContextKey struct{} func RequestIDMiddleware() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ww := chimiddleware.NewWrapResponseWriter(w, r.ProtoMajor) + ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) requestID := r.Header.Get("X-Request-ID") if requestID == "" { @@ -39,7 +39,7 @@ func RequestLoggerMiddleware(logger *slog.Logger) func(http.Handler) http.Handle start := time.Now() // Create a response writer wrapper to capture status and size - ww := chimiddleware.NewWrapResponseWriter(w, r.ProtoMajor) + ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) requestID, ok := r.Context().Value(RequestIDContextKey{}).(string) if !ok { From 4cb76b4c83bfeab0a212619c4f5166e5677c1974 Mon Sep 17 00:00:00 2001 From: AlejandroHerr Date: Sat, 17 May 2025 17:06:10 +0200 Subject: [PATCH 2/2] style(api): fix lint issues --- pkg/api/error.go | 36 ++++++++++++++---------------------- pkg/api/handler.go | 2 +- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/pkg/api/error.go b/pkg/api/error.go index f6f5218..99b8f77 100644 --- a/pkg/api/error.go +++ b/pkg/api/error.go @@ -7,7 +7,7 @@ import ( "github.com/go-chi/render" ) -type ErrorResponse struct { +type ErrRepsonse struct { Err error `json:"-"` // low-level runtime error HTTPStatusCode int `json:"-"` // http response status code @@ -16,8 +16,14 @@ type ErrorResponse struct { Details interface{} `json:"details,omitempty"` } -func NewErrorResponse(err error, statusCode int, statusText string, errorText string, details interface{}) *ErrorResponse { - return &ErrorResponse{ +func NewErrorResponse( + err error, + statusCode int, + statusText string, + errorText string, + details interface{}, +) *ErrRepsonse { + return &ErrRepsonse{ Err: err, HTTPStatusCode: statusCode, StatusText: statusText, @@ -26,23 +32,9 @@ func NewErrorResponse(err error, statusCode int, statusText string, errorText st } } -func (e *ErrorResponse) Error() string { - if e.Err != nil { - return e.Err.Error() - } - if e.ErrorText != "" { - return e.ErrorText - } - if e.StatusText != "" { - return e.StatusText - } - - return "" -} - -func (e *ErrorResponse) LogValue() slog.Value { +func (e *ErrRepsonse) LogValue() slog.Value { return slog.GroupValue( - slog.String("error", e.Error()), + slog.String("error", e.Err.Error()), slog.Int("http_status_code", e.HTTPStatusCode), slog.String("status_text", e.StatusText), slog.String("error_text", e.ErrorText), @@ -50,14 +42,14 @@ func (e *ErrorResponse) LogValue() slog.Value { ) } -func (e *ErrorResponse) Render(_ http.ResponseWriter, r *http.Request) error { +func (e *ErrRepsonse) Render(_ http.ResponseWriter, r *http.Request) error { render.Status(r, e.HTTPStatusCode) return nil } -func RenderErrorResponse(err error) *ErrorResponse { - return &ErrorResponse{ +func RenderErrorResponse(err error) *ErrRepsonse { + return &ErrRepsonse{ Err: err, HTTPStatusCode: http.StatusUnprocessableEntity, StatusText: "Error rendering response.", diff --git a/pkg/api/handler.go b/pkg/api/handler.go index 383ac38..f0804c3 100644 --- a/pkg/api/handler.go +++ b/pkg/api/handler.go @@ -7,7 +7,7 @@ import ( "github.com/go-chi/render" ) -type RendererFunc func(http.ResponseWriter, *http.Request) (render.Renderer, *ErrorResponse) +type RendererFunc func(http.ResponseWriter, *http.Request) (render.Renderer, *ErrRepsonse) func HandleRendererFunc(fn RendererFunc, logger *slog.Logger) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) {