Previously: intro, the basics, error handling, async data fetching, http.Handler, and more.
hmmm
Ok, so I'm starting here not knowing exactly where I want to go, and then using this to move forward. Going forward can mean staying where we are with our design.
Following up on the last post, there still is something that doesn't feel right with the design of the library.
Mainly, the difference between the following interface functions:
Renderable(context.Context) (Renderable, error)
RequestRenderable(*http.Request) (AsRenderable, error)
ErrorRenderable(context.Context, error) (AsRenderable, error)
For the first one, we covered that it's useful for us to have something that is representalbe this way– fine. For the second and third however, we have two things that seem different but are closely related.
- We don't need to pass a context for
RequestRenderable
becuase aRequest
has a context, otherwise I'd be passingfunc(ctx, r)
. - We might not need to pass an
err
toErrorRenderable
since a context can have aCancelCause
, but either way we come from a place ofcontext
andvalue
.
It brings up the question, and one of the reasons I punted on errors after solving errors: Should they be unified?
-
Should the library follow the principle: everything only gets a
context.Context
, implying the existence of a*veun.Error
which has aContext
interface similar to that of*http.Request
? -
Should the library follow the principle: only pass a context and you use that to determine your current state to know what kind of renderable you are producing ala
ctx.Err()
andctx.Cause()
? One can attach arbitrary values toContext
and/or create different ones for different use cases.
Exploration
Writing our own context.Context
wrapper would mean that the interface
implementor would have to dispatch on some kind of switch statement...
func(ctx veun.Context) (AsRenderable, error) {
switch ctx.Type() {
case veun.Request:
// ... extract the request
}
}
The above feels like code that would be fairly error prone. There's another way to represent a similar thing, and it's really not bad at all.
func(ctx veun.Context) (veun.AsRenderable, error) {
return ctx.Renderable(veun.R{
Request: func(r *http.Request) (veun.AsRenderable, http.Handler, error) {
// ctx, and r are available here
},
Error: func(err error) (veun.AsRenderable, error) {
// ctx and err are available here
},
})
}
I'd need to explore the calling code and how rendering and composition
would work in practice actually. Another thing to keep in mind is is if
veun.R
(for renderable) is also an AsRenderable
?
A thing that's nice is if we add more factory types then it's easy to extend the struct. A thing that isn't so nice is that it encodes the kinds of things you need to do in the library and doesn't give more flexibility to try other ways of executing it.
veun.Error
tho
Let's try out a hypothetical *veun.Error
that's similar in structure
and interface to *http.Request
.
struct Error { Err error /* ... */ }
func (e *Error) Context() context.Context { /* ... */ }
func (e *Error) WithContext(ctx context.Context) *Error { /* ... */ }
func (e *Error) Error() string { return e.Err.Error() }
func (e *Error) Unwrap() error { return errors.Unwrap(e.Err) }
I'm not really sure we'd need the WithContext
here, but why not, let's
keep it consistent.
Also we are fulfilling the Error
and Unwrap
interfaces for errors.
🤔 Does any of this actually help?
While looking at errors, in my implmentations and tests, I kept coming back
to a couple of things. Renaming RequestRenderable
, ErrorRenderable
, Renderable
,
View
, etc.
Re-View, a pivot
At first when starting to write this post, I wanted to explore errors and contexts.
A couple of different things I tried were interesting but not good enough or not
useful enough, or not intuitive enough. And just repeating Renderable
really was the
thing to fix.
There's an adage in go that is something like: return structs and accept interfaces, and in our prior situation we were just throwing around interfaces, this meant for concrete implementations, there was always wrapping and unwrapping.
In the search for the right ergnomic and naming I've moved around the and renamed the library code a whole bunch.
Template
In our original implementation, we were returning a View
struct which was
Renderable
. And in a lot of the writing, I was referring to View
and Renderable
as interchangeable concepts.
I've since separated that out for things to be renderable to HTML here,
and there's also the Div
functions we can construct using veun.Raw
.
Concepts:
View
ViewForError
ViewForRequest
These are the interface functions we're building, Template
is an implmentation detail
of directly using a html/template
.
Views
type MyView struct { /* fields elided */ }
func (v MyView) View(ctx context.Context) (*veun.View, error) {
return veun.V(veun.Template{
Tpl: someTpl,
Data: nil,
Slots: veun.Slots{ /* ... */ },
}).WithErrorHandler(someErrorHandler), nil
}
A few things jump out from the new implementation of the (now called)
AsView
interface: veun.V
, veun.Template
, and *veun.View
.
*veun.View
is an opaque type, and can only be constructed (in a useful way),
by veun.V
. This constructor combines HTMLRenderable
and ErrorHandler
.
We're not doing duck-typing by whether or not the error handler interface
is attached to MyView
, we're doing it based on wether or not an error
handler was explicitly attached to the *View
constructed.
This allows us to conitnue to return nil
(also ergonomic for construction).
Aside: We are doing duck-typing inside of V
but afterwards we get a
concrete implementation.
View constructors
Other types, like ViewForRequest
, and ViewForError
return an AsView
.
Rendering
I made an explicit decition to change Render
to a function that accepts an AsView
,
and the rendering to be opaque behind and HTMLRenderable
encapsulated by
a View
.
In the prior version it was actually a bit confusing on what you can call render on and what you can't, where you'd get error handling and where you wouldn't. I wanted to remove that kind of ambiguity and make it simpler to do more.
"veun/vhttp"
I moved all of the http related types and functions to the vhttp
package. It's
called vhttp
since you're using it in conjunction with the net/http
standard
library and otherwise you'd be import/aliasing it.
We've got vhttp.Handler
, request.Handler
, and a package of middleware that
can be useful for use with standard mux
.
What it looks like
Given a MyView
like we wrote above that renders something, we can have it be
created by an HTTP request.
import (
"context"
"net/http"
"github.com/stanistan/veun"
"github.com/stanistan/veun/vhttp"
"github.com/stanistan/veun/vhttp/request"
)
func (v MyView) View(_ context.Context) (*veun.View, error) {
return veun.View(/*...*/), nil
}
func MyViewRequestHandler() request.Handler {
return request.HandlerFunc(func(r *http.Request) (veun.AsView, http.Handler, error) {
// - We can extract data from the request.
// - We can push up an error
// - or we can do something with the response, like a 404, or anything.
return MyView{}, nil, nil
})
}
func main() {
// ...
mux.Handle("/some/path", vhttp.Handler(MyViewRequestHandler()))
// ...
}
Demo
I'm working on a demo webserver where there are examples of different ways of doing composition, routing, redirects, errors, etc, and the kinds of patterns that become possible and useful when you have all of this in one place.
- website: veun-http-demo.stanistan.com
- source: github/veun-http-demo
In the future, I'd like to actually build (or rebuild) something using the library as well as better document the different components that are part of the demo server.