diff --git a/go.mod b/go.mod index 004e331..26607cd 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/gin-gonic/gin v1.10.1 github.com/go-playground/validator/v10 v10.27.0 github.com/itsLeonB/ezutil/v2 v2.4.0 - github.com/itsLeonB/ungerr v0.3.0 + github.com/itsLeonB/ungerr v0.4.0 github.com/stretchr/testify v1.11.1 go.opentelemetry.io/otel v1.40.0 go.opentelemetry.io/otel/trace v1.40.0 diff --git a/go.sum b/go.sum index c6a393c..010fa7d 100644 --- a/go.sum +++ b/go.sum @@ -43,8 +43,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/itsLeonB/ezutil/v2 v2.4.0 h1:ylhQWF0yoBGltfuU4zi0wPj+JCoVv9jPoNvZsNONstg= github.com/itsLeonB/ezutil/v2 v2.4.0/go.mod h1:h30JTcbfmdbMXfgc9ARGlqoudR2UMG2EV49dpIZ60Os= -github.com/itsLeonB/ungerr v0.3.0 h1:lSQGyQTtoYk31FUweSfmBXKpk0KioCbjg7e7mAchpBY= -github.com/itsLeonB/ungerr v0.3.0/go.mod h1:6zc0blpoIkdqkq90Q9rqCYFjyxR1uOn5n+5z7KSgWxM= +github.com/itsLeonB/ungerr v0.4.0 h1:unXts8ahcD7mTnlzJgem5twGcQLMPzo3Vn35PbORito= +github.com/itsLeonB/ungerr v0.4.0/go.mod h1:6zc0blpoIkdqkq90Q9rqCYFjyxR1uOn5n+5z7KSgWxM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= diff --git a/pkg/middleware/error.go b/pkg/middleware/error.go index d95b731..8070797 100644 --- a/pkg/middleware/error.go +++ b/pkg/middleware/error.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "io" + "net" "runtime/debug" "strings" @@ -134,17 +135,27 @@ func (em *errorMiddleware) identifyKnownError(err error) ungerr.AppError { case *json.UnmarshalTypeError: return ungerr.BadRequestError(fmt.Sprintf("invalid value for field %s", e.Field)) - default: - errStr := e.Error() - if e == io.EOF || errStr == "EOF" { - return ungerr.BadRequestError("missing request body") - } - if strings.Contains(errStr, "connection reset by peer") || - strings.Contains(errStr, "broken pipe") { - return ungerr.BadRequestError("connection error") + case *net.OpError: + if e.Timeout() { + return ungerr.TimeoutError("your connection may be slow, please retry") } - return nil + return identifyOtherErrors(e) + + default: + return identifyOtherErrors(e) + } +} + +func identifyOtherErrors(e error) ungerr.AppError { + errStr := e.Error() + if e == io.EOF || errStr == "EOF" { + return ungerr.BadRequestError("missing request body") + } + if strings.Contains(errStr, "connection reset by peer") || + strings.Contains(errStr, "broken pipe") { + return ungerr.BadRequestError("connection error") } + return nil } func (em *errorMiddleware) handlePanic(r any, ctx *gin.Context, span trace.Span) { diff --git a/pkg/response/responses.go b/pkg/response/responses.go index 761f61b..76b38f6 100644 --- a/pkg/response/responses.go +++ b/pkg/response/responses.go @@ -3,14 +3,12 @@ package response import "math" // QueryOptions represents common pagination query parameters for HTTP requests. -// It includes validation tags to ensure proper values for page and limit parameters. type QueryOptions struct { Page int `form:"page" binding:"required,min=1"` Limit int `form:"limit" binding:"required,min=1"` } // Pagination contains metadata about paginated results. -// It provides information about the current page, total pages, and navigation flags. type Pagination struct { TotalData int `json:"totalData"` CurrentPage int `json:"currentPage"` @@ -20,39 +18,29 @@ type Pagination struct { } // IsZero checks if all pagination fields are at their zero values. -// Returns true if the pagination data is uninitialized or empty. func (p Pagination) IsZero() bool { return p.TotalData == 0 && p.CurrentPage == 0 && p.TotalPages == 0 && !p.HasNextPage && !p.HasPrevPage } // JSONResponse represents a standardized HTTP JSON response structure. -// It can include a message, data payload, error information, and pagination metadata. -type JSONResponse struct { - Data any `json:"data,omitzero"` +type JSONResponse[T any] struct { + Data T `json:"data,omitzero"` Errors []error `json:"errors,omitempty"` Pagination Pagination `json:"pagination,omitzero"` } -// NewResponse creates a basic JSONResponse with the specified message. -// Additional data, errors, or pagination can be added using the With* methods. -func NewResponse(data any) JSONResponse { - return JSONResponse{ - Data: data, - } +// NewResponse creates a JSONResponse with the specified data. +func NewResponse[T any](data T) JSONResponse[T] { + return JSONResponse[T]{Data: data} } // NewErrorResponse creates a JSONResponse for error cases. -// It populates the Errors field with the provided errors. -func NewErrorResponse(err ...error) JSONResponse { - return JSONResponse{ - Errors: err, - } +func NewErrorResponse(err ...error) JSONResponse[any] { + return JSONResponse[any]{Errors: err} } // WithPagination calculates and adds pagination metadata to the JSONResponse. -// It computes total pages and next/previous flags based on query options and total data count. -// Returns a new JSONResponse with pagination metadata included. -func (jr JSONResponse) WithPagination(queryOptions QueryOptions, totalData int) JSONResponse { +func (jr JSONResponse[T]) WithPagination(queryOptions QueryOptions, totalData int) JSONResponse[T] { if queryOptions.Limit <= 0 { return jr } diff --git a/pkg/response/responses_test.go b/pkg/response/responses_test.go index db5b59f..674ccaa 100644 --- a/pkg/response/responses_test.go +++ b/pkg/response/responses_test.go @@ -31,7 +31,7 @@ func TestNewErrorResponse(t *testing.T) { func TestPagination(t *testing.T) { t.Run("WithPagination", func(t *testing.T) { - jr := NewResponse(nil) + jr := NewResponse[any](nil) opts := QueryOptions{Page: 2, Limit: 10} totalData := 25 @@ -46,7 +46,7 @@ func TestPagination(t *testing.T) { }) t.Run("WithPagination zero limit", func(t *testing.T) { - jr := NewResponse(nil) + jr := NewResponse[any](nil) opts := QueryOptions{Page: 1, Limit: 0} jr = jr.WithPagination(opts, 100) @@ -55,7 +55,7 @@ func TestPagination(t *testing.T) { }) t.Run("WithPagination last page", func(t *testing.T) { - jr := NewResponse(nil) + jr := NewResponse[any](nil) opts := QueryOptions{Page: 3, Limit: 10} totalData := 25 diff --git a/pkg/server/utils.go b/pkg/server/utils.go index b05cb90..ba503c8 100644 --- a/pkg/server/utils.go +++ b/pkg/server/utils.go @@ -97,7 +97,7 @@ func GetAndParseFromContext[T any](ctx *gin.Context, key string) (T, error) { return ezutil.Parse[T](asserted) } -func Handler(handlerName string, successCode int, handler func(ctx *gin.Context) (any, error)) gin.HandlerFunc { +func Handler[T any](handlerName string, successCode int, handler func(ctx *gin.Context) (T, error)) gin.HandlerFunc { tracer := otel.GetTracerProvider().Tracer(packageName) return func(ctx *gin.Context) { c, span := tracer.Start(ctx.Request.Context(), handlerName) @@ -105,7 +105,7 @@ func Handler(handlerName string, successCode int, handler func(ctx *gin.Context) defer span.End() if resp, err := handler(ctx); err == nil { - ctx.JSON(successCode, response.JSONResponse{Data: resp}) + ctx.JSON(successCode, response.JSONResponse[T]{Data: resp}) } else { _ = ctx.Error(err) }