87 lines
2.5 KiB
Go
87 lines
2.5 KiB
Go
|
|
package httputil
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"net/http"
|
||
|
|
|
||
|
|
"code.nochebuena.dev/go/valid"
|
||
|
|
"code.nochebuena.dev/go/xerrors"
|
||
|
|
)
|
||
|
|
|
||
|
|
// HandlerFunc is an http.Handler that can return an error.
|
||
|
|
// On non-nil error the error is mapped to the appropriate HTTP response via Error.
|
||
|
|
// Useful for manual handlers that don't need the typed adapter.
|
||
|
|
type HandlerFunc func(w http.ResponseWriter, r *http.Request) error
|
||
|
|
|
||
|
|
func (h HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if err := h(w, r); err != nil {
|
||
|
|
Error(w, err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle adapts a typed business function to http.HandlerFunc.
|
||
|
|
// - Decodes JSON request body into Req.
|
||
|
|
// - Validates Req using the provided Validator.
|
||
|
|
// - Calls fn with the request context and decoded Req.
|
||
|
|
// - Encodes Res as JSON with HTTP 200.
|
||
|
|
// - Maps any returned error to the appropriate HTTP status via xerrors code.
|
||
|
|
func Handle[Req, Res any](v valid.Validator, fn func(ctx context.Context, req Req) (Res, error)) http.HandlerFunc {
|
||
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
var req Req
|
||
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
|
|
Error(w, badRequest("invalid JSON: "+err.Error()))
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if err := v.Struct(req); err != nil {
|
||
|
|
Error(w, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
res, err := fn(r.Context(), req)
|
||
|
|
if err != nil {
|
||
|
|
Error(w, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusOK, res)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// HandleNoBody adapts a typed function with no request body (GET, DELETE).
|
||
|
|
// Calls fn with request context; encodes result as JSON with HTTP 200.
|
||
|
|
func HandleNoBody[Res any](fn func(ctx context.Context) (Res, error)) http.HandlerFunc {
|
||
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
res, err := fn(r.Context())
|
||
|
|
if err != nil {
|
||
|
|
Error(w, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusOK, res)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// HandleEmpty adapts a typed function with a body but no response body (returns 204).
|
||
|
|
// Decodes JSON body into Req, validates, calls fn. Returns 204 on success.
|
||
|
|
func HandleEmpty[Req any](v valid.Validator, fn func(ctx context.Context, req Req) error) http.HandlerFunc {
|
||
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
var req Req
|
||
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
|
|
Error(w, badRequest("invalid JSON: "+err.Error()))
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if err := v.Struct(req); err != nil {
|
||
|
|
Error(w, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if err := fn(r.Context(), req); err != nil {
|
||
|
|
Error(w, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
NoContent(w)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// badRequest wraps a message in an ErrInvalidInput error.
|
||
|
|
func badRequest(msg string) error {
|
||
|
|
return xerrors.New(xerrors.ErrInvalidInput, msg)
|
||
|
|
}
|