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) }