Files

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