Files
web/httputil/handle.go
T

81 lines
2.9 KiB
Go
Raw Normal View History

package httputil
import (
"context"
"encoding/json"
"net/http"
"code.nochebuena.dev/einherjar/contracts/logging"
"code.nochebuena.dev/einherjar/core/valid"
"code.nochebuena.dev/einherjar/core/xerrors"
)
// Handle adapts a typed business function to http.HandlerFunc.
// - Decodes the JSON request body into Req.
// - Validates Req using the provided [valid.Validator].
// - Calls fn with the request context and decoded Req.
// - Encodes Res as JSON on success — HTTP 200 by default, or the code given via
// [WithStatus] (e.g. WithStatus(http.StatusCreated) for a resource-creating POST).
// - On error: logs via [Error] (level derived from HTTP status) and writes the standardized JSON body.
func Handle[Req, Res any](v valid.Validator, logger logging.Logger, fn func(ctx context.Context, req Req) (Res, error), opts ...Option) http.HandlerFunc {
status := resolveStatus(http.StatusOK, opts)
return func(w http.ResponseWriter, r *http.Request) {
var req Req
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Error(logger, w, r, xerrors.New(xerrors.ErrInvalidInput, "invalid JSON: "+err.Error()))
return
}
if err := v.Struct(req); err != nil {
Error(logger, w, r, err)
return
}
res, err := fn(r.Context(), req)
if err != nil {
Error(logger, w, r, err)
return
}
JSON(w, status, res)
}
}
// HandleNoBody adapts a typed function with no request body (GET, HEAD).
// Calls fn with the request context; encodes the result as JSON — HTTP 200 by
// default, or the code given via [WithStatus].
// On error: logs via [Error] and writes the standardized JSON body.
func HandleNoBody[Res any](logger logging.Logger, fn func(ctx context.Context) (Res, error), opts ...Option) http.HandlerFunc {
status := resolveStatus(http.StatusOK, opts)
return func(w http.ResponseWriter, r *http.Request) {
res, err := fn(r.Context())
if err != nil {
Error(logger, w, r, err)
return
}
JSON(w, status, res)
}
}
// HandleEmpty adapts a typed function with a request body but no response body.
// Decodes and validates Req, calls fn, and writes a body-less success — 204 No
// Content by default, or the code given via [WithStatus] (e.g.
// WithStatus(http.StatusAccepted) for async processing).
// On error: logs via [Error] and writes the standardized JSON body.
func HandleEmpty[Req any](v valid.Validator, logger logging.Logger, fn func(ctx context.Context, req Req) error, opts ...Option) http.HandlerFunc {
status := resolveStatus(http.StatusNoContent, opts)
return func(w http.ResponseWriter, r *http.Request) {
var req Req
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Error(logger, w, r, xerrors.New(xerrors.ErrInvalidInput, "invalid JSON: "+err.Error()))
return
}
if err := v.Struct(req); err != nil {
Error(logger, w, r, err)
return
}
if err := fn(r.Context(), req); err != nil {
Error(logger, w, r, err)
return
}
w.WriteHeader(status)
}
}