add API boilerplate + /accounts/{accountID} and /accounts/@me endpoints
This commit is contained in:
parent
0fa769a248
commit
dfc116d828
7 changed files with 335 additions and 0 deletions
166
web/api/error.go
Normal file
166
web/api/error.go
Normal file
|
@ -0,0 +1,166 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"emperror.dev/errors"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
// NoValue should be returned by functions that either:
|
||||
// - don't return any content for one or more code paths
|
||||
// - handle rendering content themselves
|
||||
const NoValue = errors.Sentinel("no content")
|
||||
|
||||
// WrapHandlerT wraps a modified http.HandlerFunc into a stdlib-compatible one.
|
||||
// The inner HandlerFunc additionally returns a JSON response and an error.
|
||||
func WrapHandlerT[T any](hn func(w http.ResponseWriter, r *http.Request) (T, error)) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
v, err := hn(w, r)
|
||||
if err == nil {
|
||||
render.JSON(w, r, v)
|
||||
return
|
||||
} else if err == NoValue {
|
||||
return
|
||||
}
|
||||
|
||||
// if the function returned an API error, just render that verbatim
|
||||
if apiErr, ok := err.(Error); ok {
|
||||
apiErr.prepare()
|
||||
|
||||
render.Status(r, apiErr.Status)
|
||||
render.JSON(w, r, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
// otherwise, we return an internal server error message
|
||||
apiErr := Error{Code: ErrInternalServerError}
|
||||
apiErr.prepare()
|
||||
|
||||
render.Status(r, apiErr.Status)
|
||||
render.JSON(w, r, apiErr)
|
||||
}
|
||||
}
|
||||
|
||||
// WrapHandler wraps a modified http.HandlerFunc into a stdlib-compatible one.
|
||||
// The inner HandlerFunc additionally returns an error.
|
||||
func WrapHandler(hn func(w http.ResponseWriter, r *http.Request) error) http.HandlerFunc {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) (any, error) {
|
||||
err := hn(w, r)
|
||||
if err == nil {
|
||||
return nil, NoValue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return WrapHandlerT(fn)
|
||||
|
||||
// return func(w http.ResponseWriter, r *http.Request) {
|
||||
// err := hn(w, r)
|
||||
// if err != nil {
|
||||
// // if the function returned an API error, just render that verbatim
|
||||
// // we can assume that it also logged the error (if that was needed)
|
||||
// if apiErr, ok := err.(Error); ok {
|
||||
// apiErr.prepare()
|
||||
|
||||
// render.Status(r, apiErr.Status)
|
||||
// render.JSON(w, r, apiErr)
|
||||
// return
|
||||
// }
|
||||
|
||||
// // otherwise, we log the error and return an internal server error message
|
||||
// log.Err(err).Msg("error in http handler")
|
||||
|
||||
// apiErr := Error{Code: ErrInternalServerError}
|
||||
// apiErr.prepare()
|
||||
|
||||
// render.Status(r, apiErr.Status)
|
||||
// render.JSON(w, r, apiErr)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Details string `json:"details,omitempty"`
|
||||
|
||||
// Status is set as the HTTP status code.
|
||||
Status int `json:"-"`
|
||||
}
|
||||
|
||||
func (e Error) Error() string {
|
||||
if e.Message == "" {
|
||||
e.Message = errCodeMessages[e.Code]
|
||||
}
|
||||
|
||||
if e.Details != "" {
|
||||
return fmt.Sprintf("%s (code: %d) (%s)", e.Message, e.Code, e.Details)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s (code: %d)", e.Message, e.Code)
|
||||
}
|
||||
|
||||
func (e *Error) prepare() {
|
||||
if e.Status == 0 {
|
||||
e.Status = errCodeStatuses[e.Code]
|
||||
}
|
||||
|
||||
if e.Message == "" {
|
||||
e.Message = errCodeMessages[e.Code]
|
||||
}
|
||||
}
|
||||
|
||||
// Error code constants
|
||||
const (
|
||||
ErrBadRequest = 400
|
||||
ErrForbidden = 403
|
||||
ErrNotFound = 404
|
||||
ErrMethodNotAllowed = 405
|
||||
ErrTooManyRequests = 429
|
||||
ErrInternalServerError = 500 // catch-all code for unknown errors
|
||||
|
||||
// Auth related
|
||||
ErrInvalidToken = 1001
|
||||
ErrMissingScope = 1002
|
||||
|
||||
// Account related
|
||||
ErrAccountNotFound = 1003
|
||||
)
|
||||
|
||||
func ErrCodeMessage(code int) string {
|
||||
return errCodeMessages[code]
|
||||
}
|
||||
|
||||
var errCodeMessages = map[int]string{
|
||||
ErrBadRequest: "Bad request",
|
||||
ErrForbidden: "Forbidden",
|
||||
ErrInternalServerError: "Internal server error",
|
||||
ErrNotFound: "Not found",
|
||||
ErrTooManyRequests: "Rate limit reached",
|
||||
ErrMethodNotAllowed: "Method not allowed",
|
||||
|
||||
ErrInvalidToken: "No token supplied, or token is invalid",
|
||||
ErrMissingScope: "Token is missing required scope for this endpoint",
|
||||
|
||||
ErrAccountNotFound: "Account not found",
|
||||
}
|
||||
|
||||
func ErrCodeStatus(code int) int {
|
||||
return errCodeStatuses[code]
|
||||
}
|
||||
|
||||
var errCodeStatuses = map[int]int{
|
||||
ErrBadRequest: http.StatusBadRequest,
|
||||
ErrForbidden: http.StatusForbidden,
|
||||
ErrInternalServerError: http.StatusInternalServerError,
|
||||
ErrNotFound: http.StatusNotFound,
|
||||
ErrTooManyRequests: http.StatusTooManyRequests,
|
||||
ErrMethodNotAllowed: http.StatusMethodNotAllowed,
|
||||
|
||||
ErrInvalidToken: http.StatusUnauthorized,
|
||||
ErrMissingScope: http.StatusForbidden,
|
||||
|
||||
ErrAccountNotFound: http.StatusNotFound,
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue