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