diff --git a/handler.go b/handler.go index 63c20c5..128711a 100644 --- a/handler.go +++ b/handler.go @@ -62,3 +62,27 @@ func (fn JSONBodyHandler[V]) ServeHTTP(w http.ResponseWriter, r *http.Request) e return fn(w, r, body) } + +// JSONResponder can be used as a [Handler] which automatically parses the json body into a value, which is passed to the function. +// It also returns a [ResponseHelper], instead of passing an [http.ResponseWriter]. +type JSONResponder[V any] func(r *http.Request, body V) (ResponseHelper, error) + +var _ Handler = JSONResponder[map[string]any](nil) + +func (fn JSONResponder[V]) ServeHTTP(w http.ResponseWriter, r *http.Request) error { + reqBody := r.Body + //nolint:errcheck // This is usually fine + defer reqBody.Close() + + dec := json.NewDecoder(reqBody) + var body V + if err := dec.Decode(&body); err != nil { + return err + } + + var rh ResponseHandler = func(req *http.Request) (ResponseHelper, error) { + return fn(req, body) + } + + return rh.ServeHTTP(w, r) +} diff --git a/handler_test.go b/handler_test.go index 66c3aca..9734bc6 100644 --- a/handler_test.go +++ b/handler_test.go @@ -1,6 +1,7 @@ package ezhandler_test import ( + "encoding/json" "io" "net/http" "net/http/httptest" @@ -144,3 +145,80 @@ func runJSONBodyHandlerTest(tt *testing.T, tc jsonBodyTest) { }) } + +type jsonBodyResponderTest struct { + name string + requestBody string + handler ezhandler.JSONResponder[testBody] + expectedStatus int + expectedBody testBody + expectedErr error + expectedErrContains string +} + +var jsonBodyResponderTests = []jsonBodyResponderTest{ + { + name: "successful decoding and response", + requestBody: `{"name":"John Doe","age":30}`, + handler: ezhandler.JSONResponder[testBody](func(r *http.Request, body testBody) (ezhandler.ResponseHelper, error) { + return ezhandler.JSONResponse(body), nil + }), + expectedStatus: http.StatusOK, + expectedBody: testBody{Name: "John Doe", Age: 30}, + expectedErr: nil, + }, + { + name: "invalid JSON body", + requestBody: `{"name":"John Doe","age":}`, + handler: ezhandler.JSONResponder[testBody](func(r *http.Request, body testBody) (ezhandler.ResponseHelper, error) { + return nil, nil // Should not be called + }), + expectedErrContains: "invalid character", + }, + { + name: "empty body for struct type", + requestBody: "", + handler: ezhandler.JSONResponder[testBody](func(r *http.Request, body testBody) (ezhandler.ResponseHelper, error) { + return nil, nil // Should not be called + }), + expectedErr: io.EOF, + }, + { + name: "handler function returns error", + requestBody: `{"name":"Jane Doe","age":25}`, + handler: ezhandler.JSONResponder[testBody](func(r *http.Request, body testBody) (ezhandler.ResponseHelper, error) { + return nil, errTest + }), + expectedErr: errTest, + }, +} + +func TestJSONResponder_ServeHTTP(t *testing.T) { + for _, tc := range jsonBodyResponderTests { + runJSONResponderTest(t, tc) + } +} + +func runJSONResponderTest(tt *testing.T, tc jsonBodyResponderTest) { + tt.Run(tc.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tc.requestBody)) + rec := httptest.NewRecorder() + + err := tc.handler.ServeHTTP(rec, req) + + switch { + case tc.expectedErr != nil: + assert.Equal(t, tc.expectedErr, err) + case tc.expectedErrContains != "": + assert.ErrorContains(t, err, tc.expectedErrContains) + default: + assert.NoError(t, err) + assert.Equal(t, tc.expectedStatus, rec.Code) + // Marshal expectedBody to JSON string for comparison + expectedBodyBytes, marshalErr := json.Marshal(tc.expectedBody) + assert.NoError(t, marshalErr) + assert.JSONEq(t, string(expectedBodyBytes), rec.Body.String()) + assert.Equal(t, "application/json", rec.Header().Get("Content-Type")) + } + }) +}