mirror of
https://github.com/hanzo-fi/go-libs.git
synced 2026-10-30 20:53:44 +00:00
194 lines
4.9 KiB
Go
194 lines
4.9 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
logging "github.com/hanzo-fi/go-libs/v5/pkg/observe/log"
|
|
bunpaginate "github.com/hanzo-fi/go-libs/v5/pkg/storage/bun/paginate"
|
|
)
|
|
|
|
const (
|
|
defaultLimit = 15
|
|
|
|
// MaxPageSize is the maximum value accepted for the `pageSize` query parameter.
|
|
// Larger values are clamped to it.
|
|
MaxPageSize = bunpaginate.MaxPageSize
|
|
|
|
ErrorCodeNotFound = "NOT_FOUND"
|
|
ErrorInternal = "INTERNAL"
|
|
ErrorCodeForbidden = "FORBIDDEN"
|
|
ErrorCodeValidation = "VALIDATION"
|
|
|
|
internalServerErrorMessage = "internal server error"
|
|
)
|
|
|
|
func writeJSON(w http.ResponseWriter, statusCode int, v any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(statusCode)
|
|
if v != nil {
|
|
if err := json.NewEncoder(w).Encode(v); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func WriteErrorResponse(w http.ResponseWriter, statusCode int, errorCode string, err error) {
|
|
writeErrorResponse(w, statusCode, errorCode, err.Error())
|
|
}
|
|
|
|
func writeErrorResponse(w http.ResponseWriter, statusCode int, errorCode string, errorMessage string) {
|
|
writeJSON(w, statusCode, ErrorResponse{
|
|
ErrorCode: errorCode,
|
|
ErrorMessage: errorMessage,
|
|
})
|
|
}
|
|
|
|
func NotFound(w http.ResponseWriter, err error) {
|
|
WriteErrorResponse(w, http.StatusNotFound, ErrorCodeNotFound, err)
|
|
}
|
|
|
|
func NoContent(w http.ResponseWriter) {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func Forbidden(w http.ResponseWriter, code string, err error) {
|
|
WriteErrorResponse(w, http.StatusForbidden, code, err)
|
|
}
|
|
|
|
func BadRequest(w http.ResponseWriter, code string, err error) {
|
|
WriteErrorResponse(w, http.StatusBadRequest, code, err)
|
|
}
|
|
|
|
func BadRequestWithDetails(w http.ResponseWriter, code string, err error, details string) {
|
|
writeJSON(w, http.StatusBadRequest, ErrorResponse{
|
|
ErrorCode: code,
|
|
ErrorMessage: err.Error(),
|
|
Details: details,
|
|
})
|
|
}
|
|
|
|
func InternalServerError(w http.ResponseWriter, r *http.Request, err error) {
|
|
logging.FromContext(r.Context()).Error(err)
|
|
writeErrorResponse(w, http.StatusInternalServerError, ErrorInternal, internalServerErrorMessage)
|
|
}
|
|
|
|
func Accepted(w http.ResponseWriter, v any) {
|
|
writeJSON(w, http.StatusAccepted, BaseResponse[any]{
|
|
Data: &v,
|
|
})
|
|
}
|
|
func Created(w http.ResponseWriter, v any) {
|
|
writeJSON(w, http.StatusCreated, BaseResponse[any]{
|
|
Data: &v,
|
|
})
|
|
}
|
|
|
|
func RawOk(w http.ResponseWriter, v any) {
|
|
writeJSON(w, http.StatusOK, v)
|
|
}
|
|
|
|
func Ok(w http.ResponseWriter, v any) {
|
|
writeJSON(w, http.StatusOK, BaseResponse[any]{
|
|
Data: &v,
|
|
})
|
|
}
|
|
|
|
func RenderCursor[T any](w http.ResponseWriter, v bunpaginate.Cursor[T]) {
|
|
writeJSON(w, http.StatusOK, BaseResponse[T]{
|
|
Cursor: &v,
|
|
})
|
|
}
|
|
|
|
func WriteResponse(w http.ResponseWriter, status int, body []byte) {
|
|
w.WriteHeader(status)
|
|
if _, err := w.Write(body); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func CursorFromListResponse[T any, V any](w http.ResponseWriter, query ListQuery[V], response *ListResponse[T]) {
|
|
RenderCursor(w, bunpaginate.Cursor[T]{
|
|
PageSize: query.Limit,
|
|
HasMore: response.HasMore,
|
|
Previous: response.Previous,
|
|
Next: response.Next,
|
|
Data: response.Data,
|
|
})
|
|
}
|
|
|
|
func ParsePaginationToken(r *http.Request) string {
|
|
return r.URL.Query().Get("paginationToken")
|
|
}
|
|
|
|
// ParsePageSize parses the `pageSize` query parameter. It never fails:
|
|
// - absent or empty parameter: returns defaultLimit
|
|
// - invalid value (non-numeric, overflow, <= 0): logs and returns defaultLimit
|
|
// - value above MaxPageSize: logs and returns MaxPageSize
|
|
func ParsePageSize(r *http.Request) int {
|
|
pageSize := r.URL.Query().Get("pageSize")
|
|
if pageSize == "" {
|
|
return defaultLimit
|
|
}
|
|
|
|
v, err := strconv.ParseInt(pageSize, 10, 32)
|
|
if err != nil || v <= 0 {
|
|
// note: the logger has no warning level, Infof is the closest
|
|
logging.FromContext(r.Context()).
|
|
Infof("invalid 'pageSize' query param %q, falling back to default page size %d", pageSize, defaultLimit)
|
|
return defaultLimit
|
|
}
|
|
if v > MaxPageSize {
|
|
logging.FromContext(r.Context()).
|
|
Infof("'pageSize' query param %d exceeds maximum, clamped to %d", v, MaxPageSize)
|
|
return MaxPageSize
|
|
}
|
|
return int(v)
|
|
}
|
|
|
|
func ReadPaginatedRequest[T any](r *http.Request, f func(r *http.Request) T) ListQuery[T] {
|
|
var payload T
|
|
if f != nil {
|
|
payload = f(r)
|
|
}
|
|
return ListQuery[T]{
|
|
Pagination: Pagination{
|
|
Limit: ParsePageSize(r),
|
|
PaginationToken: ParsePaginationToken(r),
|
|
},
|
|
Payload: payload,
|
|
}
|
|
}
|
|
|
|
func GetQueryMap(m map[string][]string, key string) map[string]string {
|
|
dicts := make(map[string]string)
|
|
for k, v := range m {
|
|
if i := strings.IndexByte(k, '['); i >= 1 && k[0:i] == key {
|
|
if j := strings.IndexByte(k[i+1:], ']'); j >= 1 {
|
|
dicts[k[i+1:][:j]] = v[0]
|
|
}
|
|
}
|
|
}
|
|
return dicts
|
|
}
|
|
|
|
type ListResponse[T any] struct {
|
|
Data []T
|
|
Next, Previous string
|
|
HasMore bool
|
|
}
|
|
|
|
type Pagination struct {
|
|
Limit int
|
|
PaginationToken string
|
|
}
|
|
|
|
type ListQuery[T any] struct {
|
|
Pagination
|
|
Payload T
|
|
}
|
|
|
|
type Mapper[SRC any, DST any] func(src SRC) DST
|