From 0001b1d60942b8add6879796be7262da8db5772f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20W=C3=BCstenberg?= Date: Fri, 8 Oct 2021 15:00:20 +0200 Subject: [PATCH] Add http.Adapt function (#92) In the new package `http`, an `Adapt` function converts a `Handler` into a `http.HandlerFunc` from the `http` stdlib package. --- http/handler.go | 42 ++++++++++++++++ http/handler_test.go | 114 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 http/handler.go create mode 100644 http/handler_test.go diff --git a/http/handler.go b/http/handler.go new file mode 100644 index 0000000..9f47bff --- /dev/null +++ b/http/handler.go @@ -0,0 +1,42 @@ +// Package http provides adapters to render gomponents in http handlers. +package http + +import ( + "net/http" + + g "github.com/maragudk/gomponents" +) + +// Handler is like http.Handler but returns a Node and an error. +// See Adapt for how errors are translated to HTTP responses. +type Handler = func(http.ResponseWriter, *http.Request) (g.Node, error) + +type errorWithStatusCode interface { + StatusCode() int +} + +// Adapt a Handler to a http.Handlerfunc. +// The returned Node is rendered to the ResponseWriter, in both normal and error cases. +// If the Handler returns an error, and it implements a "StatusCode() int" method, that HTTP status code is sent +// in the response header. Otherwise, the status code http.StatusInternalServerError (500) is used. +func Adapt(h Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + n, err := h(w, r) + if err != nil { + switch v := err.(type) { + case errorWithStatusCode: + w.WriteHeader(v.StatusCode()) + default: + w.WriteHeader(http.StatusInternalServerError) + } + } + + if n == nil { + return + } + + if err := n.Render(w); err != nil { + http.Error(w, "error rendering node: "+err.Error(), http.StatusInternalServerError) + } + } +} diff --git a/http/handler_test.go b/http/handler_test.go new file mode 100644 index 0000000..822d69a --- /dev/null +++ b/http/handler_test.go @@ -0,0 +1,114 @@ +package http_test + +import ( + "errors" + "io" + "net/http" + "net/http/httptest" + "testing" + + g "github.com/maragudk/gomponents" + ghttp "github.com/maragudk/gomponents/http" +) + +func TestAdapt(t *testing.T) { + t.Run("renders a node to the response writer", func(t *testing.T) { + h := ghttp.Adapt(func(w http.ResponseWriter, r *http.Request) (g.Node, error) { + return g.El("div"), nil + }) + code, body := get(t, h) + if code != http.StatusOK { + t.Fatal("status code is", code) + } + if body != "
" { + t.Fatal("body is", body) + } + }) + + t.Run("renders nothing when returning nil node", func(t *testing.T) { + h := ghttp.Adapt(func(w http.ResponseWriter, r *http.Request) (g.Node, error) { + return nil, nil + }) + code, body := get(t, h) + if code != http.StatusOK { + t.Fatal("status code is", code) + } + if body != "" { + t.Fatal(`body is`, body) + } + }) + + t.Run("errors with 500 if node cannot render", func(t *testing.T) { + h := ghttp.Adapt(func(w http.ResponseWriter, r *http.Request) (g.Node, error) { + return erroringNode{}, nil + }) + code, body := get(t, h) + if code != http.StatusInternalServerError { + t.Fatal("status code is", code) + } + if body != "error rendering node: don't want to\n" { + t.Fatal(`body is`, body) + } + }) + + t.Run("errors with status code if error implements StatusCode method and renders node", func(t *testing.T) { + h := ghttp.Adapt(func(w http.ResponseWriter, r *http.Request) (g.Node, error) { + return g.El("div"), statusCodeError{http.StatusTeapot} + }) + code, body := get(t, h) + if code != http.StatusTeapot { + t.Fatal("status code is", code) + } + if body != "
" { + t.Fatal(`body is`, body) + } + }) + + t.Run("errors with 500 if other error and renders node", func(t *testing.T) { + h := ghttp.Adapt(func(w http.ResponseWriter, r *http.Request) (g.Node, error) { + return g.El("div"), errors.New("") + }) + code, body := get(t, h) + if code != http.StatusInternalServerError { + t.Fatal("status code is", code) + } + if body != "
" { + t.Fatal(`body is`, body) + } + }) +} + +type erroringNode struct{} + +func (n erroringNode) Render(io.Writer) error { + return errors.New("don't want to") +} + +type statusCodeError struct { + code int +} + +func (e statusCodeError) Error() string { + return http.StatusText(e.code) +} + +func (e statusCodeError) StatusCode() int { + return e.code +} + +func get(t *testing.T, h http.Handler) (int, string) { + t.Helper() + + recorder := httptest.NewRecorder() + request, err := http.NewRequest(http.MethodGet, "/", nil) + if err != nil { + t.Fatal(err) + } + h.ServeHTTP(recorder, request) + result := recorder.Result() + body, err := io.ReadAll(result.Body) + if err != nil { + t.Fatal(err) + } + return result.StatusCode, string(body) +}