251 lines
6.1 KiB
Go
251 lines
6.1 KiB
Go
package response
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/asaskevich/govalidator"
|
|
"github.com/labstack/echo/v4"
|
|
)
|
|
|
|
// APIResponse represents a standard API response structure
|
|
type APIResponse struct {
|
|
Success bool `json:"success"`
|
|
Message string `json:"message,omitempty"`
|
|
Data interface{} `json:"data,omitempty"`
|
|
Error *APIError `json:"error,omitempty"`
|
|
Meta *Meta `json:"meta,omitempty"`
|
|
}
|
|
|
|
// APIError represents error details in API response
|
|
type APIError struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
Details string `json:"details,omitempty"`
|
|
}
|
|
|
|
// Meta represents metadata for paginated responses
|
|
type Meta struct {
|
|
Total int `json:"total,omitempty"`
|
|
Limit int `json:"limit"`
|
|
Offset int `json:"offset"`
|
|
Page int `json:"page,omitempty"`
|
|
Pages int `json:"pages,omitempty"`
|
|
NextCursor string `json:"next_cursor,omitempty"`
|
|
HasMore bool `json:"has_more,omitempty"`
|
|
}
|
|
|
|
type Pagination struct {
|
|
Limit int `query:"limit" valid:"optional,range(1|100)" default:"20"`
|
|
Offset int `query:"offset" valid:"optional,range(0|1000000)" default:"0"`
|
|
Cursor string `query:"cursor" valid:"optional"`
|
|
SortBy string `query:"sort_by" valid:"optional"`
|
|
SortOrder string `query:"sort_order" valid:"optional,in(asc|desc)"`
|
|
}
|
|
|
|
// Success returns a successful response
|
|
func Success(c echo.Context, data interface{}, message string) error {
|
|
return c.JSON(http.StatusOK, APIResponse{
|
|
Success: true,
|
|
Message: message,
|
|
Data: data,
|
|
})
|
|
}
|
|
|
|
// SuccessWithMeta returns a successful response with metadata
|
|
func SuccessWithMeta(c echo.Context, data interface{}, meta *Meta, message string) error {
|
|
return c.JSON(http.StatusOK, APIResponse{
|
|
Success: true,
|
|
Message: message,
|
|
Data: data,
|
|
Meta: meta,
|
|
})
|
|
}
|
|
|
|
// Created returns a 201 Created response
|
|
func Created(c echo.Context, data interface{}, message string) error {
|
|
return c.JSON(http.StatusCreated, APIResponse{
|
|
Success: true,
|
|
Message: message,
|
|
Data: data,
|
|
})
|
|
}
|
|
|
|
// Accepted returns a 202 Accepted response (for async operations)
|
|
func Accepted(c echo.Context, data interface{}, message string) error {
|
|
return c.JSON(http.StatusAccepted, APIResponse{
|
|
Success: true,
|
|
Message: message,
|
|
Data: data,
|
|
})
|
|
}
|
|
|
|
// BadRequest returns a 400 Bad Request response
|
|
func BadRequest(c echo.Context, message, details string) error {
|
|
return c.JSON(http.StatusBadRequest, APIResponse{
|
|
Success: false,
|
|
Message: "Bad Request",
|
|
Error: &APIError{
|
|
Code: "BAD_REQUEST",
|
|
Message: message,
|
|
Details: details,
|
|
},
|
|
})
|
|
}
|
|
|
|
// Unauthorized returns a 401 Unauthorized response
|
|
func Unauthorized(c echo.Context, message string) error {
|
|
return c.JSON(http.StatusUnauthorized, APIResponse{
|
|
Success: false,
|
|
Message: "Unauthorized",
|
|
Error: &APIError{
|
|
Code: "UNAUTHORIZED",
|
|
Message: message,
|
|
},
|
|
})
|
|
}
|
|
|
|
// Forbidden returns a 403 Forbidden response
|
|
func Forbidden(c echo.Context, message string) error {
|
|
return c.JSON(http.StatusForbidden, APIResponse{
|
|
Success: false,
|
|
Message: "Forbidden",
|
|
Error: &APIError{
|
|
Code: "FORBIDDEN",
|
|
Message: message,
|
|
},
|
|
})
|
|
}
|
|
|
|
// NotFound returns a 404 Not Found response
|
|
func NotFound(c echo.Context, message string) error {
|
|
return c.JSON(http.StatusNotFound, APIResponse{
|
|
Success: false,
|
|
Message: "Not Found",
|
|
Error: &APIError{
|
|
Code: "NOT_FOUND",
|
|
Message: message,
|
|
},
|
|
})
|
|
}
|
|
|
|
// Conflict returns a 409 Conflict response
|
|
func Conflict(c echo.Context, message string) error {
|
|
return c.JSON(http.StatusConflict, APIResponse{
|
|
Success: false,
|
|
Message: "Conflict",
|
|
Error: &APIError{
|
|
Code: "CONFLICT",
|
|
Message: message,
|
|
},
|
|
})
|
|
}
|
|
|
|
// ValidationError returns a 422 Unprocessable Entity response
|
|
func ValidationError(c echo.Context, message, details string) error {
|
|
return c.JSON(http.StatusUnprocessableEntity, APIResponse{
|
|
Success: false,
|
|
Message: "Validation Error",
|
|
Error: &APIError{
|
|
Code: "VALIDATION_ERROR",
|
|
Message: message,
|
|
Details: details,
|
|
},
|
|
})
|
|
}
|
|
|
|
// InternalServerError returns a 500 Internal Server Error response
|
|
func InternalServerError(c echo.Context, message string) error {
|
|
return c.JSON(http.StatusInternalServerError, APIResponse{
|
|
Success: false,
|
|
Message: "Internal Server Error",
|
|
Error: &APIError{
|
|
Code: "INTERNAL_SERVER_ERROR",
|
|
Message: message,
|
|
},
|
|
})
|
|
}
|
|
|
|
// ServiceUnavailable returns a 503 Service Unavailable response
|
|
func ServiceUnavailable(c echo.Context, message string) error {
|
|
return c.JSON(http.StatusServiceUnavailable, APIResponse{
|
|
Success: false,
|
|
Message: "Service Unavailable",
|
|
Error: &APIError{
|
|
Code: "SERVICE_UNAVAILABLE",
|
|
Message: message,
|
|
},
|
|
})
|
|
}
|
|
|
|
// Parse parses the form and validates it
|
|
func Parse[T any](c echo.Context) (*T, error) {
|
|
var form T
|
|
if err := c.Bind(&form); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, err := govalidator.ValidateStruct(form); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &form, nil
|
|
}
|
|
|
|
func NewPagination(c echo.Context) *Pagination {
|
|
limit, err := strconv.Atoi(c.QueryParam("limit"))
|
|
if err != nil {
|
|
limit = 20
|
|
}
|
|
|
|
offset, err := strconv.Atoi(c.QueryParam("offset"))
|
|
if err != nil {
|
|
offset = 0
|
|
}
|
|
|
|
sortBy := c.QueryParam("sort_by")
|
|
if sortBy == "" {
|
|
sortBy = "created_at"
|
|
}
|
|
|
|
sortOrder := c.QueryParam("sort_order")
|
|
if sortOrder == "" {
|
|
sortOrder = "desc"
|
|
}
|
|
|
|
cursor := c.QueryParam("cursor")
|
|
|
|
return &Pagination{
|
|
Limit: limit,
|
|
Offset: offset,
|
|
Cursor: cursor,
|
|
SortBy: sortBy,
|
|
SortOrder: sortOrder,
|
|
}
|
|
}
|
|
|
|
// Response calculates offset-based pagination metadata.
|
|
func (p *Pagination) Response(total int64) *Meta {
|
|
return p.ListMeta(total, "", false)
|
|
}
|
|
|
|
// ListMeta builds pagination metadata for list endpoints (offset and/or cursor).
|
|
// When total is negative, total/page/pages are omitted (count was skipped).
|
|
func (p *Pagination) ListMeta(total int64, nextCursor string, hasMore bool) *Meta {
|
|
meta := &Meta{
|
|
Limit: p.Limit,
|
|
Offset: p.Offset,
|
|
NextCursor: nextCursor,
|
|
HasMore: hasMore,
|
|
}
|
|
if total >= 0 {
|
|
pages := (total + int64(p.Limit) - 1) / int64(p.Limit)
|
|
meta.Total = int(total)
|
|
if p.Limit > 0 {
|
|
meta.Page = p.Offset/p.Limit + 1
|
|
}
|
|
meta.Pages = int(pages)
|
|
}
|
|
return meta
|
|
}
|