Previously: intro, the basics, error handling, and async data fetching.
An Aside: Small Improvements
Before we go forward, let's make some small quality of life improvements.
Parsing templates from separate files
We've had all of our templates be inlined in strings before. This is fine for testing, but when we're building an app, we'll want to have something in place for loading/embedding our templates diffently.
tempalte FS parsing (source: cde93703)
diff --git a/view.go b/view.go
index 97f02f5..f092749 100644
--- a/view.go
+++ b/view.go
@@ -3,6 +3,7 @@ package veun
import (
"context"
"html/template"
+ "io/fs"
)
type View struct {
@@ -27,10 +28,16 @@ func slotFuncStub(name string) (template.HTML, error) {
return template.HTML(""), nil
}
+func newTemplate(name string) *template.Template {
+ return template.New(name).Funcs(template.FuncMap{
+ "slot": slotFuncStub,
+ })
+}
+
func MustParseTemplate(name, contents string) *template.Template {
- return template.Must(
- template.New(name).
- Funcs(template.FuncMap{"slot": slotFuncStub}).
- Parse(contents),
- )
+ return template.Must(newTemplate(name).Parse(contents))
+}
+
+func MustParseTemplateFS(f fs.FS, ps ...string) *template.Template {
+ return template.Must(newTemplate("ROOT").ParseFS(f, ps...))
}
import "embed"
var (
//go:embed templates
templatesFS embed.FS
templates = MustParseTemplateFS(templatesFS, "templates/*.tpl")
)
If our templates are malformed, this will panic during startup or test time.
View{
// ...
Tpl: templates.Lookup("my_view.tpl"),
}
Others
Slots can be nil views
slot can be nil, we return empty (source: 35883748)
diff --git a/slots.go b/slots.go
index 61f7fb4..8a04e81 100644
--- a/slots.go
+++ b/slots.go
@@ -10,7 +10,7 @@ type Slots map[string]AsRenderable
func (s Slots) renderSlot(ctx context.Context) func(string) (template.HTML, error) {
return func(name string) (template.HTML, error) {
slot, ok := s[name]
- if ok {
+ if ok && slot != nil {
return Render(ctx, slot)
}
This is a design decision, but we allow a view to be empty/nil and then
we don't render it. We might want to move this into Render
, but
this can be here as well.
Documentation
Adding some documentation to our public functions and types.
doc comments (source: 2b70959c)
diff --git a/renderable.go b/renderable.go
index 57f3791..27759f6 100644
--- a/renderable.go
+++ b/renderable.go
@@ -5,17 +5,25 @@ import (
"html/template"
)
+// Renderable represents any struct that can be rendered
+// in the Render function.
type Renderable interface {
+ // Template provides the template object / parsed and compiled,
+ // that Render will execute given a context.
Template(ctx context.Context) (*template.Template, error)
+ // TemplateData provides the data to the template given a context.
TemplateData(ctx context.Context) (any, error)
}
type AsRenderable interface {
+ // Renderable produces a Renderable struct given a context.
Renderable(ctx context.Context) (Renderable, error)
}
+// RenderableFunc is a function that conforms to the Renderable interface.
type RenderableFunc func(context.Context) (Renderable, error)
+// Renderable implements Renderable for RenderableFunc.
func (f RenderableFunc) Renderable(ctx context.Context) (Renderable, error) {
return f(ctx)
}
More meaningful library errors
These should be custom error types but for now this is ok.
error wrapping (source: 7008a944)
diff --git a/renderer.go b/renderer.go
index ebbddef..fe37ba0 100644
--- a/renderer.go
+++ b/renderer.go
@@ -26,7 +26,7 @@ func render(ctx context.Context, r Renderable) (template.HTML, error) {
tpl, err := r.Template(ctx)
if err != nil {
- return empty, err
+ return empty, fmt.Errorf("Template: %w", err)
}
if tpl == nil {
@@ -35,12 +35,12 @@ func render(ctx context.Context, r Renderable) (template.HTML, error) {
data, err := r.TemplateData(ctx)
if err != nil {
- return empty, err
+ return empty, fmt.Errorf("TemplateData: %w", err)
}
var bs bytes.Buffer
if err := tpl.Execute(&bs, data); err != nil {
- return empty, err
+ return empty, fmt.Errorf("tpl.Execute: %w", err)
}
return template.HTML(bs.String()), nil
The HTTP Server
In Go, there is one fundamental interface for serving an HTTP endpoint,
http.Handler
. In practice, you can either make a type
that implements ServeHTTP(...)
or use http.HandlerFunc
to make a
function into a handler. For us, with views and renderables, this is
a bit too low level. We want:
- Something that can produce views based on a request.
- Something that will allow us to do redirects and 404s.
- To integrate well with standard http handlers and middleware.
- To have short meaningful routes.
- To maintain flexibility and composability.
- TO NOT build our own router.
Views are a function of requests and routes
In the simplest case, a view/renderable can be produced by a request. We keep our the views simple and they don't know anything about where their inputs come from.
I can imagine having different kind of constructing functions for the view type as well.
package some_view
import (
"net/http"
"github.com/stanistan/veun"
)
type FromRequest(r *http.Request) (veun.AsRenderable, error) {
return myView{/* */}, nil
}
This is a good start and we can write a handler that works with this type of function.
We're going to panic
everywhere for error handling for the moment
because what that's a problem for future us to solve.
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
view, err := some_view.FromRequest(r)
if err != nil {
panic(err)
}
html, err := Render(r.Context(), view)
if err != nil {
panic(err)
}
_, err = w.Write([]byte(html))
if err != nil {
panic(err)
}
})
The only thing that is specific to that route (that isn't part of
the rendering behavior) is FromRequest
.
And we can extract it into an interface (and function type) that can produce either a view or error out.
type RequestRenderable interface {
RequestRenderable(r *http.Request) (AsRenderable, error)
}
type RequestRenderableFunc func(*http.Request) (AsRenderable, error)
func (f RequestRenderableFunc) RequestRenderable(r *http.Request) (AsRenderable, error) {
return f(r)
}
Adding RequestRenderable & Func (source: bda46e00)
diff --git a/http_request_renderable.go b/http_request_renderable.go
new file mode 100644
index 0000000..66700c8
--- /dev/null
+++ b/http_request_renderable.go
@@ -0,0 +1,21 @@
+package veun
+
+import (
+ "net/http"
+)
+
+// RequestRenderable represents a method that
+// can create a view out of an http.Request.
+type RequestRenderable interface {
+ RequestRenderable(r *http.Request) (AsRenderable, error)
+}
+
+// RequestRenderableFunc is the function representation of a
+// RequestRenderable.
+type RequestRenderableFunc func(*http.Request) (AsRenderable, error)
+
+// RequestRenderable conforms RequestRenderableFunc to
+// RequestRenderable interface.
+func (f RequestRenderableFunc) RequestRenderable(r *http.Request) (AsRenderable, error) {
+ return f(r)
+}
And to make a handler:
func(renderable RequestRenderable) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// snip...
view, err := renderable.RequestRenderable(r)
// snip ...
})
}
We can extract this into a type:
type HTTPHandler struct {
r RequestRenderable
}
func (h HTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
view, err := h.r.RequestRenderable(r)
// ...
}
Initial HTTPHandler (with panics) (source: 4f37b29c)
diff --git a/http_request_renderable.go b/http_request_renderable.go
index 66700c8..8d5f5c0 100644
--- a/http_request_renderable.go
+++ b/http_request_renderable.go
@@ -19,3 +19,26 @@ type RequestRenderableFunc func(*http.Request) (AsRenderable, error)
func (f RequestRenderableFunc) RequestRenderable(r *http.Request) (AsRenderable, error) {
return f(r)
}
+
+// HTTPHandler implements http.Handler for a RequestRenderable.
+type HTTPHandler struct {
+ Renderable RequestRenderable
+}
+
+// ServeHTTP implements http.Handler.
+func (h HTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ renderable, err := h.Renderable.RequestRenderable(r)
+ if err != nil {
+ panic(err)
+ }
+
+ html, err := Render(r.Context(), renderable)
+ if err != nil {
+ panic(err)
+ }
+
+ _, err = w.Write([]byte(html))
+ if err != nil {
+ panic(err)
+ }
+}
And our route definition can look more like this:
mux := http.NewServeMux()
mux.Handle("/empty", HTTPHandler{
RequestRenderableFunc(func(r *http.Request) (AsRenderable, error) {
return nil, nil
}),
})
Testing
Of course we can write an HTTP server using the standard library, but Go also
provides net/http/httptest
, where we can start a server and make requests
to it as if it were remote from our tests.
srv := httptest.NewServer(mux)
// and we can make requests to srv.URL
First test for the http handler (source: cf9d6bbd)
diff --git a/http_request_test.go b/http_request_test.go
new file mode 100644
index 0000000..04f6f6b
--- /dev/null
+++ b/http_request_test.go
@@ -0,0 +1,57 @@
+package veun_test
+
+import (
+ "context"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/alecthomas/assert/v2"
+ "github.com/stanistan/veun"
+)
+
+func TestRequestBasicHandler(t *testing.T) {
+ var handler = veun.HTTPHandler{
+ veun.RequestRenderableFunc(func(r *http.Request) (veun.AsRenderable, error) {
+ return nil, nil
+ }),
+ }
+
+ mux := http.NewServeMux()
+
+ mux.Handle("/empty", handler)
+
+ server := httptest.NewServer(mux)
+ defer server.Close()
+
+ var sendRequest = func(t *testing.T, to string) (string, int, error) {
+ t.Helper()
+
+ req, err := http.NewRequestWithContext(context.TODO(), "GET", server.URL+to, nil)
+ assert.NoError(t, err)
+
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return "", 0, err
+ }
+
+ defer res.Body.Close()
+
+ data, err := ioutil.ReadAll(res.Body)
+ assert.NoError(t, err)
+
+ return string(data), res.StatusCode, nil
+ }
+
+ t.Run("the root path is a real server that 404s", func(t *testing.T) {
+ _, code, _ := sendRequest(t, "/")
+ assert.Equal(t, 404, code)
+ })
+
+ t.Run("empty handler is indeed empty", func(t *testing.T) {
+ body, code, _ := sendRequest(t, "/empty")
+ assert.Equal(t, "", body)
+ assert.Equal(t, 200, code)
+ })
+}
Our example above ended up unearthing a bug where that was not safe to do,
but it should be-- in our error handlers and the slot we allow for a view
to be nil
.
fix: safe to pass nil to renderer (source: eedee592)
diff --git a/renderer.go b/renderer.go
index fe37ba0..8546abc 100644
--- a/renderer.go
+++ b/renderer.go
@@ -8,6 +8,10 @@ import (
)
func Render(ctx context.Context, r AsRenderable) (template.HTML, error) {
+ if r == nil {
+ return template.HTML(""), nil
+ }
+
renderable, err := r.Renderable(ctx)
if err != nil {
return handleRenderError(ctx, err, r)
More Funcs
We can add convenience constructors to the HTTPHandler{RequestRenderable...}
pattern, and this becomes a bit nicer to deal with.
var empty = RequestRenderableFunc(
func(r *http.Request) (AsRenderable, error) {
return nil, nil
},
)
mux.Handle("/empty", HTTPHandler{empty})
Every change we make to how we represent requets/renderables and handlers will be captured in these tests going forward.
convenience function RequestHandlerFunc (source: 791b18a1)
diff --git a/http_request_renderable.go b/http_request_renderable.go
index 8d5f5c0..9a0c7c5 100644
--- a/http_request_renderable.go
+++ b/http_request_renderable.go
@@ -20,6 +20,14 @@ func (f RequestRenderableFunc) RequestRenderable(r *http.Request) (AsRenderable,
return f(r)
}
+func RequestHandlerFunc(r RequestRenderableFunc) http.Handler {
+ return HTTPHandler{Renderable: r}
+}
+
+func RequestHandler(r RequestRenderable) http.Handler {
+ return HTTPHandler{Renderable: r}
+}
+
// HTTPHandler implements http.Handler for a RequestRenderable.
type HTTPHandler struct {
Renderable RequestRenderable
diff --git a/http_request_test.go b/http_request_test.go
index 04f6f6b..8298521 100644
--- a/http_request_test.go
+++ b/http_request_test.go
@@ -12,11 +12,9 @@ import (
)
func TestRequestBasicHandler(t *testing.T) {
- var handler = veun.HTTPHandler{
- veun.RequestRenderableFunc(func(r *http.Request) (veun.AsRenderable, error) {
- return nil, nil
- }),
- }
+ var handler = veun.RequestHandlerFunc(func(r *http.Request) (veun.AsRenderable, error) {
+ return nil, nil
+ })
mux := http.NewServeMux()
Composing RequestRenderable
I'm a big fan of interfaces that work well together and are self-consistent. Views and renderables compose well together (using slots and delegation), we are making trees of views after all. And the tree itself is renderable, just like a node in the tree is-- at some point we don't really have to care too much.
Turns out we have a very similar pattern available to us with RequestRenderable
types.
Why do we even care?
In a real world web application, you are going to end up up with standard a container
view at the top level signifying the <html>...
and whatever application and page chrome
you need.
var htmlTpl = MustParseTemplate("html", `<html><body>{{ slot "body" }}</body></html>`)
type html struct {
Body AsRenderable
}
func (v html) Renderable(_ context.Context) (Renderable, error) {
return View{Tpl: htmlTpl, Slots: Slots{"body": v.Body}}, nil
}
Having each RequestRenderable
be aware of which wrapper view is needed might be
annoying, and ends up making our functions less re-usable across different contexts.
But, we can re-use the interface (similar to the middleware pattern).
func HTML(renderable RequestRenderable) RequestRenderable {
return RequestRenderableFunc(func(r *http.Request) (AsRenderable, error) {
v, err := renderable.RequestRenderable(r)
if err != nil {
return nil, err
}
return html{Body: v}, nil
})
}
Or more clearly:
func HTML(renderable RequestRenderable) http.Handler {
return RequestHandlerFunc( /* ... */ )
}
So we can:
mux.handle("/html/empty", HTML(empty))
Adding tests for HTML() (source: 74eeff3c)
diff --git a/http_request_test.go b/http_request_test.go
index 8298521..7df31ba 100644
--- a/http_request_test.go
+++ b/http_request_test.go
@@ -2,23 +2,55 @@ package veun_test
import (
"context"
+ "fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/alecthomas/assert/v2"
- "github.com/stanistan/veun"
+ . "github.com/stanistan/veun"
)
-func TestRequestBasicHandler(t *testing.T) {
- var handler = veun.RequestHandlerFunc(func(r *http.Request) (veun.AsRenderable, error) {
+var htmlTpl = MustParseTemplate("html", `<html><body>{{ slot "body" }}</body></html>`)
+
+type html struct {
+ Body AsRenderable
+}
+
+func (v html) Renderable(_ context.Context) (Renderable, error) {
+ return View{Tpl: htmlTpl, Slots: Slots{"body": v.Body}}, nil
+}
+
+func HTML(renderable RequestRenderable) http.Handler {
+ return RequestHandlerFunc(func(r *http.Request) (AsRenderable, error) {
+ v, err := renderable.RequestRenderable(r)
+ if err != nil {
+ return nil, err
+ }
+
+ return html{Body: v}, nil
+ })
+}
+
+func TestRequestRequestHandler(t *testing.T) {
+ var empty = RequestRenderableFunc(func(r *http.Request) (AsRenderable, error) {
return nil, nil
})
mux := http.NewServeMux()
- mux.Handle("/empty", handler)
+ mux.Handle("/empty", RequestHandlerFunc(empty))
+ mux.Handle("/html/empty", HTML(empty))
+
+ mux.Handle("/person", RequestHandlerFunc(func(r *http.Request) (AsRenderable, error) {
+ name := r.URL.Query().Get("name")
+ if name == "" {
+ return nil, fmt.Errorf("missing name")
+ }
+
+ return PersonView(Person{Name: name}), nil
+ }))
server := httptest.NewServer(mux)
defer server.Close()
@@ -52,4 +84,22 @@ func TestRequestBasicHandler(t *testing.T) {
assert.Equal(t, "", body)
assert.Equal(t, 200, code)
})
+
+ t.Run("person renders (name=Stan)", func(t *testing.T) {
+ body, code, _ := sendRequest(t, "/person?name=Stan")
+ assert.Equal(t, "<div>Hi, Stan.</div>", body)
+ assert.Equal(t, 200, code)
+ })
+
+ t.Run("person renders (name=someone)", func(t *testing.T) {
+ body, code, _ := sendRequest(t, "/person?name=someone")
+ assert.Equal(t, "<div>Hi, someone.</div>", body)
+ assert.Equal(t, 200, code)
+ })
+
+ t.Run("/html/empty", func(t *testing.T) {
+ body, code, _ := sendRequest(t, "/html/empty")
+ assert.Equal(t, "<html><body></body></html>", body)
+ assert.Equal(t, 200, code)
+ })
}
Fixing the abstraction
While this is nice, we've lost some functionality, and no longer have answers to the questions:
- How will we redirect?
- What if we want to 404?
- What if we want to send back http response headers?
- What is our error handling strategy?
And real applications need answers to these questions.
The current implementation will either fail (with panics, for now), or render a 200
.
I've played around with having this return an Response
struct or something
like that, which would create different levels of composition with usage that
is something like this:
Standard response, 200
with rendered view:
return Response(view), nil
Rendered view with custom status code:
return Response(view, StatusCode(404)), nil
An empty 404
return NotFoundResponse()
Redirects
return RedirectResponse(301, toLocation)
Looking at this, and remembering we want to be compatible with
the standard library, what we're really doing here is building
things that implememnt http.Handler
. This is really powerful.
The problem with the above approach is we lose library composability,
as soon as we are dealing with http.Handler
we can longer extract
view information.
http.Handler
But we can do both! Go obviously has multiple return values, so
let's add one more to our RequestRenderable
.
type RequestRenderable interface {
RequestRenderable(*http.Request) (AsRenderable, http.Handler, error)
}
To go back through our examples above usage would be:
Standard response, 200
with rendered view:
return view, nil, nil
Rendered view with custom status code:
return view, StatusCode(404), nil
An empty 404:
return nil, http.NotFoundHandler(), nil
Redirects:
return nil, http.RedirectHandler(toLocation, 301), nil
Custom Response Headers:
return view, ResponseHeader(...), nil
This means we have the optionality of adding http handlers to our response but also have the types and flexibility to do view composition in our request handers.
Adding http.Handler as a return parameter to RequestRenderable (source: db0d1a58)
diff --git a/http_request_renderable.go b/http_request_renderable.go
index 9a0c7c5..1ff20bc 100644
--- a/http_request_renderable.go
+++ b/http_request_renderable.go
@@ -7,16 +7,16 @@ import (
// RequestRenderable represents a method that
// can create a view out of an http.Request.
type RequestRenderable interface {
- RequestRenderable(r *http.Request) (AsRenderable, error)
+ RequestRenderable(r *http.Request) (AsRenderable, http.Handler, error)
}
// RequestRenderableFunc is the function representation of a
// RequestRenderable.
-type RequestRenderableFunc func(*http.Request) (AsRenderable, error)
+type RequestRenderableFunc func(*http.Request) (AsRenderable, http.Handler, error)
// RequestRenderable conforms RequestRenderableFunc to
// RequestRenderable interface.
-func (f RequestRenderableFunc) RequestRenderable(r *http.Request) (AsRenderable, error) {
+func (f RequestRenderableFunc) RequestRenderable(r *http.Request) (AsRenderable, http.Handler, error) {
return f(r)
}
@@ -35,7 +35,7 @@ type HTTPHandler struct {
// ServeHTTP implements http.Handler.
func (h HTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- renderable, err := h.Renderable.RequestRenderable(r)
+ renderable, _, err := h.Renderable.RequestRenderable(r)
if err != nil {
panic(err)
}
diff --git a/http_request_test.go b/http_request_test.go
index 7df31ba..23051c5 100644
--- a/http_request_test.go
+++ b/http_request_test.go
@@ -23,33 +23,33 @@ func (v html) Renderable(_ context.Context) (Renderable, error) {
}
func HTML(renderable RequestRenderable) http.Handler {
- return RequestHandlerFunc(func(r *http.Request) (AsRenderable, error) {
- v, err := renderable.RequestRenderable(r)
+ return RequestHandlerFunc(func(r *http.Request) (AsRenderable, http.Handler, error) {
+ v, next, err := renderable.RequestRenderable(r)
if err != nil {
- return nil, err
+ return nil, next, err
}
- return html{Body: v}, nil
+ return html{Body: v}, next, nil
})
}
func TestRequestRequestHandler(t *testing.T) {
- var empty = RequestRenderableFunc(func(r *http.Request) (AsRenderable, error) {
- return nil, nil
+ var empty = RequestRenderableFunc(func(r *http.Request) (AsRenderable, http.Handler, error) {
+ return nil, nil, nil
})
mux := http.NewServeMux()
- mux.Handle("/empty", RequestHandlerFunc(empty))
+ mux.Handle("/empty", RequestHandler(empty))
mux.Handle("/html/empty", HTML(empty))
- mux.Handle("/person", RequestHandlerFunc(func(r *http.Request) (AsRenderable, error) {
+ mux.Handle("/person", RequestHandlerFunc(func(r *http.Request) (AsRenderable, http.Handler, error) {
name := r.URL.Query().Get("name")
if name == "" {
- return nil, fmt.Errorf("missing name")
+ return nil, nil, fmt.Errorf("missing name")
}
- return PersonView(Person{Name: name}), nil
+ return PersonView(Person{Name: name}), nil, nil
}))
server := httptest.NewServer(mux)
one-liner implementation for our HTTPHandler (source: 2b9f6914)
diff --git a/http_request_renderable.go b/http_request_renderable.go
index 1ff20bc..5d6bf82 100644
--- a/http_request_renderable.go
+++ b/http_request_renderable.go
@@ -35,7 +35,7 @@ type HTTPHandler struct {
// ServeHTTP implements http.Handler.
func (h HTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- renderable, _, err := h.Renderable.RequestRenderable(r)
+ renderable, next, err := h.Renderable.RequestRenderable(r)
if err != nil {
panic(err)
}
@@ -45,8 +45,13 @@ func (h HTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
panic(err)
}
+ if next != nil {
+ next.ServeHTTP(w, r)
+ }
+
_, err = w.Write([]byte(html))
if err != nil {
panic(err)
}
+
}
diff --git a/http_request_test.go b/http_request_test.go
index 23051c5..237f111 100644
--- a/http_request_test.go
+++ b/http_request_test.go
@@ -34,8 +34,21 @@ func HTML(renderable RequestRenderable) http.Handler {
}
func TestRequestRequestHandler(t *testing.T) {
+ var statusCode = func(code int) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(code)
+ })
+ }
+
var empty = RequestRenderableFunc(func(r *http.Request) (AsRenderable, http.Handler, error) {
- return nil, nil, nil
+ switch r.URL.Query().Get("not_found") {
+ case "default":
+ return nil, http.NotFoundHandler(), nil
+ case "nil_404":
+ return nil, statusCode(http.StatusNotFound), nil
+ default:
+ return nil, nil, nil
+ }
})
mux := http.NewServeMux()
@@ -85,6 +98,18 @@ func TestRequestRequestHandler(t *testing.T) {
assert.Equal(t, 200, code)
})
+ t.Run("empty handler can 404", func(t *testing.T) {
+ body, code, _ := sendRequest(t, "/empty?not_found=default")
+ assert.Equal(t, "404 page not found\n", body)
+ assert.Equal(t, 404, code)
+ })
+
+ t.Run("empty handler can 404 and nil", func(t *testing.T) {
+ body, code, _ := sendRequest(t, "/empty?not_found=nil_404")
+ assert.Equal(t, "", body)
+ assert.Equal(t, 404, code)
+ })
+
t.Run("person renders (name=Stan)", func(t *testing.T) {
body, code, _ := sendRequest(t, "/person?name=Stan")
assert.Equal(t, "<div>Hi, Stan.</div>", body)
This is pretty neat, and allows the person writing their application to only use what they need and when they need it.
Error Handling
Going back to error handling, we always come back to error handling,
our implementation currently has three places where we panic
.
RequestRenderable()
returns an errorRender()
returns an errorWrite
fails
Our library already has hooks two of these to fail...
- RequestRenderable composition can fully handle (1) and (2).
- For (3), we can't really do anything else here-- maybe the connection went away, and we let it fail.
Let's make a really silly error view...
var errorViewTpl = MustParseTemplate("errorView", `Error: {{ . }}`)
type errorView struct {
Error error
}
func (v errorView) Renderable(_ context.Context) (Renderable, error) {
return View{Tpl: errorViewTpl, Data: v.Error}, nil
}
func newErrorView(_ context.Context, err error) (AsRenderable, error) {
return errorView{Error: err}, nil
}
And leverage our ErrorRenderable
interface to do some composition.
func WithErrorHandler(eh ErrorRenderable) func(RequestRenderable) RequestRenderable {
return func(renderable RequestRenderable) RequestRenderable {
return RequestRenderableFunc(func(r *http.Request) (AsRenderable, http.Handler, error) {
v, next, err := renderable.RequestRenderable(r)
if err != nil {
v, err = eh.ErrorRenderable(r.Context(), err)
return v, nil, err
}
html, err := Render(r.Context(), v)
if err != nil {
v, err = eh.ErrorRenderable(r.Context(), err)
return v, nil, err
}
if len(html) == 0 {
return nil, next, nil
}
return nil, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if next != nil {
next.ServeHTTP(w, r)
}
_, _ = w.Write([]byte(html))
}), nil
})
}
}
I don't like it...
-
It illustrates how given the public types and functions in our library, we can pretty quickly build out a solution without breaking our abstraction.
-
It's basically the same thing as our
HTTPHandler
, but with an ErrorRenderable provided (which maybe isn't the right abstraction).
We can put this responsibility in our handler implementation and add some sane defaults,
mainly 500 Internal Server Error
.
First, let's make HTTPHandler
a function instead of a struct, this eliminates the
need for RequestHandlerFunc
.
HTTPHandler is a function, and renaming RequestHandler (source: 8020b8ca)
diff --git a/http_request_renderable.go b/http_request_renderable.go
index 5d6bf82..d62379d 100644
--- a/http_request_renderable.go
+++ b/http_request_renderable.go
@@ -20,21 +20,21 @@ func (f RequestRenderableFunc) RequestRenderable(r *http.Request) (AsRenderable,
return f(r)
}
-func RequestHandlerFunc(r RequestRenderableFunc) http.Handler {
- return HTTPHandler{Renderable: r}
+func HTTPHandlerFunc(r RequestRenderableFunc) http.Handler {
+ return handler{Renderable: r}
}
-func RequestHandler(r RequestRenderable) http.Handler {
- return HTTPHandler{Renderable: r}
+func HTTPHandler(r RequestRenderable) http.Handler {
+ return handler{Renderable: r}
}
-// HTTPHandler implements http.Handler for a RequestRenderable.
-type HTTPHandler struct {
+// handler implements http.Handler for a RequestRenderable.
+type handler struct {
Renderable RequestRenderable
}
// ServeHTTP implements http.Handler.
-func (h HTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
renderable, next, err := h.Renderable.RequestRenderable(r)
if err != nil {
panic(err)
@@ -53,5 +53,4 @@ func (h HTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err != nil {
panic(err)
}
-
}
diff --git a/http_request_test.go b/http_request_test.go
index 237f111..2940f4d 100644
--- a/http_request_test.go
+++ b/http_request_test.go
@@ -23,7 +23,7 @@ func (v html) Renderable(_ context.Context) (Renderable, error) {
}
func HTML(renderable RequestRenderable) http.Handler {
- return RequestHandlerFunc(func(r *http.Request) (AsRenderable, http.Handler, error) {
+ return HTTPHandlerFunc(func(r *http.Request) (AsRenderable, http.Handler, error) {
v, next, err := renderable.RequestRenderable(r)
if err != nil {
return nil, next, err
@@ -53,10 +53,10 @@ func TestRequestRequestHandler(t *testing.T) {
mux := http.NewServeMux()
- mux.Handle("/empty", RequestHandler(empty))
+ mux.Handle("/empty", HTTPHandler(empty))
mux.Handle("/html/empty", HTML(empty))
- mux.Handle("/person", RequestHandlerFunc(func(r *http.Request) (AsRenderable, http.Handler, error) {
+ mux.Handle("/person", HTTPHandlerFunc(func(r *http.Request) (AsRenderable, http.Handler, error) {
name := r.URL.Query().Get("name")
if name == "" {
return nil, nil, fmt.Errorf("missing name")
Adding the error delegate option
Now that our handler is private, we can add options! And our options should be optional. If we end up adding more things to our handler, we can do so with these optional arguments.
type HandlerOption func(h *handler)
func HTTPHandler(r RequestRenderable, opts ...HandlerOption) http.Handler { /* ... */ }
type handler struct {
Renderable RequestRenderable
ErrorHandler ErrorRenderable // <- this is new
}
Replace the panic
calls with handleError(ctx, err, ResponseWriter)
which will either do error degation-- using our handleRenderError
, or
write out at 500 Internal Server Error
, and we're basically done!
handler with error handler (source: 5ae41f09)
diff --git a/error_renderable.go b/error_renderable.go
index 0dcab6c..87f539b 100644
--- a/error_renderable.go
+++ b/error_renderable.go
@@ -17,6 +17,12 @@ type ErrorRenderable interface {
ErrorRenderable(ctx context.Context, err error) (AsRenderable, error)
}
+type ErrorRenderableFunc func(context.Context, error) (AsRenderable, error)
+
+func (f ErrorRenderableFunc) ErrorRenderable(ctx context.Context, err error) (AsRenderable, error) {
+ return f(ctx, err)
+}
+
func handleRenderError(ctx context.Context, err error, with any) (template.HTML, error) {
var empty template.HTML
diff --git a/http_request_renderable.go b/http_request_renderable.go
index d62379d..8ef40b5 100644
--- a/http_request_renderable.go
+++ b/http_request_renderable.go
@@ -1,6 +1,8 @@
package veun
import (
+ "context"
+ "log/slog"
"net/http"
)
@@ -20,29 +22,54 @@ func (f RequestRenderableFunc) RequestRenderable(r *http.Request) (AsRenderable,
return f(r)
}
-func HTTPHandlerFunc(r RequestRenderableFunc) http.Handler {
- return handler{Renderable: r}
+func HTTPHandlerFunc(r RequestRenderableFunc, opts ...HandlerOption) http.Handler {
+ h := handler{Renderable: r}
+ for _, opt := range opts {
+ opt(&h)
+ }
+ return h
+}
+
+func HTTPHandler(r RequestRenderable, opts ...HandlerOption) http.Handler {
+ h := handler{Renderable: r}
+ for _, opt := range opts {
+ opt(&h)
+ }
+ return h
}
-func HTTPHandler(r RequestRenderable) http.Handler {
- return handler{Renderable: r}
+type HandlerOption func(h *handler)
+
+func WithErrorHandler(eh ErrorRenderable) HandlerOption {
+ return func(h *handler) {
+ h.ErrorHandler = eh
+ }
+}
+
+func WithErrorHandlerFunc(eh ErrorRenderableFunc) HandlerOption {
+ return func(h *handler) {
+ h.ErrorHandler = eh
+ }
}
// handler implements http.Handler for a RequestRenderable.
type handler struct {
- Renderable RequestRenderable
+ Renderable RequestRenderable
+ ErrorHandler ErrorRenderable
}
// ServeHTTP implements http.Handler.
func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
renderable, next, err := h.Renderable.RequestRenderable(r)
if err != nil {
- panic(err)
+ h.handleError(r.Context(), w, err)
+ return
}
html, err := Render(r.Context(), renderable)
if err != nil {
- panic(err)
+ h.handleError(r.Context(), w, err)
+ return
}
if next != nil {
@@ -54,3 +81,17 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
panic(err)
}
}
+
+func (h handler) handleError(ctx context.Context, w http.ResponseWriter, err error) {
+ html, rErr := handleRenderError(ctx, err, h.ErrorHandler)
+ if rErr == nil && len(html) > 0 {
+ w.WriteHeader(http.StatusInternalServerError)
+ _, _ = w.Write([]byte(html))
+ return
+ }
+
+ // TODO: grab the logger from the context
+ slog.Error("handler failed", "err", err)
+ code := http.StatusInternalServerError
+ http.Error(w, http.StatusText(code), code)
+}
diff --git a/http_request_test.go b/http_request_test.go
index 2940f4d..fa0f800 100644
--- a/http_request_test.go
+++ b/http_request_test.go
@@ -26,14 +26,31 @@ func HTML(renderable RequestRenderable) http.Handler {
return HTTPHandlerFunc(func(r *http.Request) (AsRenderable, http.Handler, error) {
v, next, err := renderable.RequestRenderable(r)
if err != nil {
- return nil, next, err
+ return nil, nil, err
+ } else if v == nil {
+ return nil, next, nil
}
return html{Body: v}, next, nil
})
}
+var errorViewTpl = MustParseTemplate("errorView", `Error: {{ . }}`)
+
+type errorView struct {
+ Error error
+}
+
+func (v errorView) Renderable(_ context.Context) (Renderable, error) {
+ return View{Tpl: errorViewTpl, Data: v.Error}, nil
+}
+
+func newErrorView(_ context.Context, err error) (AsRenderable, error) {
+ return errorView{Error: err}, nil
+}
+
func TestRequestRequestHandler(t *testing.T) {
+
var statusCode = func(code int) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(code)
@@ -51,19 +68,22 @@ func TestRequestRequestHandler(t *testing.T) {
}
})
- mux := http.NewServeMux()
-
- mux.Handle("/empty", HTTPHandler(empty))
- mux.Handle("/html/empty", HTML(empty))
-
- mux.Handle("/person", HTTPHandlerFunc(func(r *http.Request) (AsRenderable, http.Handler, error) {
+ var person = RequestRenderableFunc(func(r *http.Request) (AsRenderable, http.Handler, error) {
name := r.URL.Query().Get("name")
if name == "" {
return nil, nil, fmt.Errorf("missing name")
}
return PersonView(Person{Name: name}), nil, nil
- }))
+ })
+
+ mux := http.NewServeMux()
+
+ mux.Handle("/empty", HTTPHandler(empty))
+ mux.Handle("/html/empty", HTML(empty))
+
+ mux.Handle("/person", HTTPHandler(person, WithErrorHandlerFunc(newErrorView)))
+ mux.Handle("/html/person", HTML(person))
server := httptest.NewServer(mux)
defer server.Close()
@@ -116,6 +136,12 @@ func TestRequestRequestHandler(t *testing.T) {
assert.Equal(t, 200, code)
})
+ t.Run("person (name=)", func(t *testing.T) {
+ body, code, _ := sendRequest(t, "/person?name=")
+ assert.Equal(t, 500, code)
+ assert.Equal(t, "Error: missing name", body)
+ })
+
t.Run("person renders (name=someone)", func(t *testing.T) {
body, code, _ := sendRequest(t, "/person?name=someone")
assert.Equal(t, "<div>Hi, someone.</div>", body)
@@ -124,7 +150,19 @@ func TestRequestRequestHandler(t *testing.T) {
t.Run("/html/empty", func(t *testing.T) {
body, code, _ := sendRequest(t, "/html/empty")
- assert.Equal(t, "<html><body></body></html>", body)
+ assert.Equal(t, "", body)
assert.Equal(t, 200, code)
})
+
+ t.Run("/html/person (name=Stan)", func(t *testing.T) {
+ body, code, _ := sendRequest(t, "/html/person?name=Stan")
+ assert.Equal(t, "<html><body><div>Hi, Stan.</div></body></html>", body)
+ assert.Equal(t, 200, code)
+ })
+
+ t.Run("/html/person (name=)", func(t *testing.T) {
+ body, code, _ := sendRequest(t, "/html/person?name=")
+ assert.Equal(t, "Internal Server Error\n", body)
+ assert.Equal(t, 500, code)
+ })
}
Putting it together
From the tests in the patches, we can see that making a handler is now pretty simple, by building up the few pieces we've put together, we have a pretty robust little library.
import (
"net/http"
. "github.com/stanistan/veun"
)
type myServer struct {
// some db contections and contexts
}
func (s *myServer) SomePageHandler(r *http.Request) (AsRenderable, http.Handler, error) {
// Stuff...
}
func main() {
http.Handle("/some-page", HTML(s.SomePageHandler))
}
This is compatible with middleware, any router that works with http.Handler
functions, and can do response headers, redirects, custom error pages,
and cancellation/deadlines, and really any kind of way that someone would
want to structure their HTTP server.
moving HTTPHandler to http_handler (source: 9e996124)
diff --git a/http_handler.go b/http_handler.go
new file mode 100644
index 0000000..dd725ff
--- /dev/null
+++ b/http_handler.go
@@ -0,0 +1,87 @@
+package veun
+
+import (
+ "context"
+ "log/slog"
+ "net/http"
+)
+
+// HTTPHandler constructs an http.HTTPHandler given the RequestRenderable.
+func HTTPHandler(r RequestRenderable, opts ...HandlerOption) http.Handler {
+ h := handler{Renderable: r}
+ for _, opt := range opts {
+ opt(&h)
+ }
+ return h
+}
+
+// HTTPHandler constructs an http.HTTPHandler given the RequestRenderableFunc.
+func HTTPHandlerFunc(r RequestRenderableFunc, opts ...HandlerOption) http.Handler {
+ h := handler{Renderable: r}
+ for _, opt := range opts {
+ opt(&h)
+ }
+ return h
+}
+
+// HandlerOption is an option that can be provided to the handler.
+type HandlerOption func(h *handler)
+
+// WithErrorHandler creates a HandlerOption that can be provided to HTTPHandler
+// or HTTPHandlerFunc.
+//
+// This can change the default error handling behavior of the handler.
+func WithErrorHandler(eh ErrorRenderable) HandlerOption {
+ return func(h *handler) {
+ h.ErrorHandler = eh
+ }
+}
+
+// WithErrorHandlerFunc is the same as WithErrorHandler.
+func WithErrorHandlerFunc(eh ErrorRenderableFunc) HandlerOption {
+ return WithErrorHandler(eh)
+}
+
+// handler implements http.Handler for a RequestRenderable.
+type handler struct {
+ Renderable RequestRenderable
+ ErrorHandler ErrorRenderable
+}
+
+// ServeHTTP implements http.Handler.
+func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ renderable, next, err := h.Renderable.RequestRenderable(r)
+ if err != nil {
+ h.handleError(r.Context(), w, err)
+ return
+ }
+
+ html, err := Render(r.Context(), renderable)
+ if err != nil {
+ h.handleError(r.Context(), w, err)
+ return
+ }
+
+ if next != nil {
+ next.ServeHTTP(w, r)
+ }
+
+ _, err = w.Write([]byte(html))
+ if err != nil {
+ panic(err)
+ }
+}
+
+func (h handler) handleError(ctx context.Context, w http.ResponseWriter, err error) {
+ html, rErr := handleRenderError(ctx, err, h.ErrorHandler)
+ if rErr == nil && len(html) > 0 {
+ w.WriteHeader(http.StatusInternalServerError)
+ _, _ = w.Write([]byte(html))
+ return
+ }
+
+ // TODO: grab the logger from the context
+ slog.Error("handler failed", "err", err)
+ code := http.StatusInternalServerError
+ http.Error(w, http.StatusText(code), code)
+}
diff --git a/http_request_renderable.go b/http_request_renderable.go
index 8ef40b5..267a293 100644
--- a/http_request_renderable.go
+++ b/http_request_renderable.go
@@ -1,8 +1,6 @@
package veun
import (
- "context"
- "log/slog"
"net/http"
)
@@ -21,77 +19,3 @@ type RequestRenderableFunc func(*http.Request) (AsRenderable, http.Handler, erro
func (f RequestRenderableFunc) RequestRenderable(r *http.Request) (AsRenderable, http.Handler, error) {
return f(r)
}
-
-func HTTPHandlerFunc(r RequestRenderableFunc, opts ...HandlerOption) http.Handler {
- h := handler{Renderable: r}
- for _, opt := range opts {
- opt(&h)
- }
- return h
-}
-
-func HTTPHandler(r RequestRenderable, opts ...HandlerOption) http.Handler {
- h := handler{Renderable: r}
- for _, opt := range opts {
- opt(&h)
- }
- return h
-}
-
-type HandlerOption func(h *handler)
-
-func WithErrorHandler(eh ErrorRenderable) HandlerOption {
- return func(h *handler) {
- h.ErrorHandler = eh
- }
-}
-
-func WithErrorHandlerFunc(eh ErrorRenderableFunc) HandlerOption {
- return func(h *handler) {
- h.ErrorHandler = eh
- }
-}
-
-// handler implements http.Handler for a RequestRenderable.
-type handler struct {
- Renderable RequestRenderable
- ErrorHandler ErrorRenderable
-}
-
-// ServeHTTP implements http.Handler.
-func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- renderable, next, err := h.Renderable.RequestRenderable(r)
- if err != nil {
- h.handleError(r.Context(), w, err)
- return
- }
-
- html, err := Render(r.Context(), renderable)
- if err != nil {
- h.handleError(r.Context(), w, err)
- return
- }
-
- if next != nil {
- next.ServeHTTP(w, r)
- }
-
- _, err = w.Write([]byte(html))
- if err != nil {
- panic(err)
- }
-}
-
-func (h handler) handleError(ctx context.Context, w http.ResponseWriter, err error) {
- html, rErr := handleRenderError(ctx, err, h.ErrorHandler)
- if rErr == nil && len(html) > 0 {
- w.WriteHeader(http.StatusInternalServerError)
- _, _ = w.Write([]byte(html))
- return
- }
-
- // TODO: grab the logger from the context
- slog.Error("handler failed", "err", err)
- code := http.StatusInternalServerError
- http.Error(w, http.StatusText(code), code)
-}