Previously: intro, the basics, and error handling.
There are cases where we'll fetch data for something up front, and pass it down the view tree, meaning the views are basically "pure functions", all done; and there are times when a view will request data for itself, meaning a view constructor could be waiting and blocking.
The below example is of a view constructor kicking off a simulated fetch in a goroutine, based on how its configured the view either fails or succeeds.
ExpensiveViewData
is a placeholder for something that could
have come from some JSON API.
type ExpensiveViewData struct {
Title string `json:"title"`
}
The view holds data for success or error and Renderable
checks for
which channel gets data first (this will block), either getting data,
or an error, and then completing the Renderable
interface with which
ever.
type ExpensiveView struct {
Data chan ExpensiveViewData
Err chan error
}
func (v *ExpensiveView) Renderable() (Renderable, error) {
select {
case err := <-v.Err:
return nil, err
case data := <-v.Data:
return View{/* */}, nil
}
}
The constructor (function) for our view, because we're simulating
work, is parameterized by shouldErr
and we kick off a goroutine
that waits for 1 second and then sends the message.
func NewExpensiveView(shouldErr bool) *ExpensiveView {
errCh := make(chan err)
dataCh := make(chan ExpensiveViewData)
go func() {
defer close(errCh)
defer close(dataCh)
// do data fetching and either write to one thing or the other
time.Sleep(1 * time.Second)
if shouldErr {
errCh <- fmt.Errorf("fetch failed")
} else {
dataCh <- ExpensiveViewData{Title: "hi"}
}
}()
return &ExpensiveView{Data: dataCh, Err: errCh}
}
initial channel data fetch test (source: 3b46e0cb)
diff --git a/render_with_data_fetch_test.go b/render_with_data_fetch_test.go
new file mode 100644
index 0000000..963242e
--- /dev/null
+++ b/render_with_data_fetch_test.go
@@ -0,0 +1,68 @@
+package veun_test
+
+import (
+ "fmt"
+ "html/template"
+ "testing"
+ "time"
+
+ "github.com/alecthomas/assert/v2"
+
+ . "github.com/stanistan/veun"
+)
+
+type ExpensiveViewData struct {
+ Title string `json:"title"`
+}
+
+var expensiveViewTpl = MustParseTemplate("expensiveView", `{{ .Title }} success`)
+
+type ExpensiveView struct {
+ Data chan ExpensiveViewData
+ Err chan error
+}
+
+func NewExpensiveView(shouldErr bool) *ExpensiveView {
+ errCh := make(chan error)
+ dataCh := make(chan ExpensiveViewData)
+
+ go func() {
+ defer func() {
+ close(errCh)
+ close(dataCh)
+ }()
+
+ // do data fetching and either write to
+ // one thing or the other
+ time.Sleep(1 * time.Millisecond)
+ if shouldErr {
+ errCh <- fmt.Errorf("fetch failed")
+ } else {
+ dataCh <- ExpensiveViewData{Title: "hi"}
+ }
+ }()
+
+ return &ExpensiveView{Data: dataCh, Err: errCh}
+}
+
+func (v *ExpensiveView) Renderable() (Renderable, error) {
+ select {
+ case err := <-v.Err:
+ return nil, err
+ case data := <-v.Data:
+ return View{Tpl: expensiveViewTpl, Data: data}, nil
+ }
+}
+
+func TestViewWithChannels(t *testing.T) {
+ t.Run("successful", func(t *testing.T) {
+ html, err := Render(NewExpensiveView(false))
+ assert.NoError(t, err)
+ assert.Equal(t, template.HTML(`hi success`), html)
+ })
+
+ t.Run("failed", func(t *testing.T) {
+ _, err := Render(NewExpensiveView(true))
+ assert.Error(t, err)
+ })
+}
This works just fine, but what if we don't want this to be waiting for a second, what if we have 10ms to do the work?
Context and Cancellation
We need something that could do cancellation in case of a timeout.
We're missing context.Context
.
Where this is mostly going to be used, in the span of an HTTP request, we can grab this from the request itself, but our API has no method of propagating it down through construction, render, and data fetching.
What we want is for our select
to look like this:
select {
case <-ctx.Done():
return nil, ctx.Err()
case err := <-v.Err:
return nil, err
case data := <-v.Data:
return View{/* */}, nil
}
And eventually, our API to look like this:
func (v *ExpensiveView) Renderable(ctx context.Context) (Renderable, error) {
// ...
}
So we have some changes to make.
Before | After |
---|---|
Renderable() | Renderable(Context) |
Template() | Template(Context) |
TemplateData() | TemplateData(Context) |
Render(AsRenderable) | Render(Context, AsRenderable) |
Adding context.Context to all interface methods (source: 81d7c2de)
diff --git a/error_renderable.go b/error_renderable.go
index 63159e8..0dcab6c 100644
--- a/error_renderable.go
+++ b/error_renderable.go
@@ -1,6 +1,9 @@
package veun
-import "html/template"
+import (
+ "context"
+ "html/template"
+)
type ErrorRenderable interface {
// ErrorRenderable can return bubble the error
@@ -11,10 +14,10 @@ type ErrorRenderable interface {
// which will ignore the error entirely.
//
// Otherwise we will attempt to render next one.
- ErrorRenderable(err error) (AsRenderable, error)
+ ErrorRenderable(ctx context.Context, err error) (AsRenderable, error)
}
-func handleRenderError(err error, with any) (template.HTML, error) {
+func handleRenderError(ctx context.Context, err error, with any) (template.HTML, error) {
var empty template.HTML
if with == nil {
@@ -26,7 +29,7 @@ func handleRenderError(err error, with any) (template.HTML, error) {
return empty, err
}
- r, err := errRenderable.ErrorRenderable(err)
+ r, err := errRenderable.ErrorRenderable(ctx, err)
if err != nil {
return empty, err
}
@@ -35,5 +38,5 @@ func handleRenderError(err error, with any) (template.HTML, error) {
return empty, nil
}
- return Render(r)
+ return Render(ctx, r)
}
diff --git a/render_container_as_view_test.go b/render_container_as_view_test.go
index 9af1ad0..b83ff1a 100644
--- a/render_container_as_view_test.go
+++ b/render_container_as_view_test.go
@@ -1,6 +1,7 @@
package veun_test
import (
+ "context"
"html/template"
"testing"
@@ -14,7 +15,7 @@ type ContainerView2 struct {
Body AsRenderable
}
-func (v ContainerView2) Renderable() (Renderable, error) {
+func (v ContainerView2) Renderable(ctx context.Context) (Renderable, error) {
return View{
Tpl: containerViewTpl,
Slots: Slots{"heading": v.Heading, "body": v.Body},
@@ -22,7 +23,7 @@ func (v ContainerView2) Renderable() (Renderable, error) {
}
func TestRenderContainerAsView(t *testing.T) {
- html, err := Render(ContainerView2{
+ html, err := Render(context.Background(), ContainerView2{
Heading: ChildView1{},
Body: ChildView2{},
})
diff --git a/render_container_error_test.go b/render_container_error_test.go
index 8052a8d..abedcca 100644
--- a/render_container_error_test.go
+++ b/render_container_error_test.go
@@ -1,6 +1,7 @@
package veun_test
import (
+ "context"
"errors"
"fmt"
"html/template"
@@ -15,7 +16,7 @@ type FailingView struct {
Err error
}
-func (v FailingView) Renderable() (Renderable, error) {
+func (v FailingView) Renderable(_ context.Context) (Renderable, error) {
return nil, fmt.Errorf("FailingView.Renderable(): %w", v.Err)
}
@@ -24,11 +25,11 @@ type FallibleView struct {
Child AsRenderable
}
-func (v FallibleView) Renderable() (Renderable, error) {
- return v.Child.Renderable()
+func (v FallibleView) Renderable(ctx context.Context) (Renderable, error) {
+ return v.Child.Renderable(ctx)
}
-func (v FallibleView) ErrorRenderable(err error) (AsRenderable, error) {
+func (v FallibleView) ErrorRenderable(ctx context.Context, err error) (AsRenderable, error) {
if v.CapturesErr == nil {
return nil, err
}
@@ -41,7 +42,7 @@ func (v FallibleView) ErrorRenderable(err error) (AsRenderable, error) {
}
func TestRenderContainerWithFailingView(t *testing.T) {
- _, err := Render(ContainerView2{
+ _, err := Render(context.Background(), ContainerView2{
Heading: ChildView1{},
Body: FailingView{
Err: fmt.Errorf("construction: %w", errSomethingFailed),
@@ -52,7 +53,7 @@ func TestRenderContainerWithFailingView(t *testing.T) {
func TestRenderContainerWithCapturedError(t *testing.T) {
t.Run("errors_bubble_out", func(t *testing.T) {
- _, err := Render(ContainerView2{
+ _, err := Render(context.Background(), ContainerView2{
Heading: ChildView1{},
Body: FallibleView{
Child: FailingView{Err: errSomethingFailed},
@@ -62,7 +63,7 @@ func TestRenderContainerWithCapturedError(t *testing.T) {
})
t.Run("errors_can_push_replacement_views", func(t *testing.T) {
- html, err := Render(ContainerView2{
+ html, err := Render(context.Background(), ContainerView2{
Heading: ChildView1{},
Body: FallibleView{
Child: FailingView{Err: errSomethingFailed},
@@ -77,7 +78,7 @@ func TestRenderContainerWithCapturedError(t *testing.T) {
})
t.Run("errors_can_return_nil_views", func(t *testing.T) {
- html, err := Render(ContainerView2{
+ html, err := Render(context.Background(), ContainerView2{
Heading: ChildView1{},
Body: FallibleView{
Child: FailingView{Err: errors.New("hi")},
diff --git a/render_container_test.go b/render_container_test.go
index 162f0d2..aef4e68 100644
--- a/render_container_test.go
+++ b/render_container_test.go
@@ -1,6 +1,7 @@
package veun_test
import (
+ "context"
"html/template"
"testing"
@@ -19,30 +20,30 @@ var containerViewTpl = MustParseTemplate("containerView", `<div>
<div class="body">{{ slot "body" }}</div>
</div>`)
-func tplWithRealSlotFunc(tpl *template.Template, slots map[string]AsRenderable) *template.Template {
+func tplWithRealSlotFunc(ctx context.Context, tpl *template.Template, slots map[string]AsRenderable) *template.Template {
return tpl.Funcs(template.FuncMap{
"slot": func(name string) (template.HTML, error) {
slot, ok := slots[name]
if ok {
- return Render(slot)
+ return Render(ctx, slot)
}
return template.HTML(""), nil
},
})
}
-func (v ContainerView) Template() (*template.Template, error) {
- return tplWithRealSlotFunc(containerViewTpl, map[string]AsRenderable{
+func (v ContainerView) Template(ctx context.Context) (*template.Template, error) {
+ return tplWithRealSlotFunc(ctx, containerViewTpl, map[string]AsRenderable{
"heading": v.Heading,
"body": v.Body,
}), nil
}
-func (v ContainerView) TemplateData() (any, error) {
+func (v ContainerView) TemplateData(_ context.Context) (any, error) {
return nil, nil
}
-func (v ContainerView) Renderable() (Renderable, error) {
+func (v ContainerView) Renderable(_ context.Context) (Renderable, error) {
return v, nil
}
@@ -52,18 +53,18 @@ var childViewTemplate = template.Must(
type ChildView1 struct{}
-func (v ChildView1) Renderable() (Renderable, error) {
+func (v ChildView1) Renderable(_ context.Context) (Renderable, error) {
return View{Tpl: childViewTemplate, Data: "HEADING"}, nil
}
type ChildView2 struct{}
-func (v ChildView2) Renderable() (Renderable, error) {
+func (v ChildView2) Renderable(_ context.Context) (Renderable, error) {
return View{Tpl: childViewTemplate, Data: "BODY"}, nil
}
func TestRenderContainer(t *testing.T) {
- html, err := Render(&ContainerView{
+ html, err := Render(context.Background(), &ContainerView{
Heading: ChildView1{},
Body: ChildView2{},
})
diff --git a/render_person_test.go b/render_person_test.go
index e2a99de..5de4182 100644
--- a/render_person_test.go
+++ b/render_person_test.go
@@ -1,6 +1,7 @@
package veun_test
import (
+ "context"
"html/template"
"testing"
@@ -24,12 +25,12 @@ var personViewTpl = template.Must(
template.New("PersonView").Parse(`<div>Hi, {{ .Name }}.</div>`),
)
-func (v *personView) Renderable() (Renderable, error) {
+func (v *personView) Renderable(_ context.Context) (Renderable, error) {
return View{Tpl: personViewTpl, Data: v.Person}, nil
}
func TestRenderPerson(t *testing.T) {
- html, err := Render(PersonView(Person{Name: "Stan"}))
+ html, err := Render(context.Background(), PersonView(Person{Name: "Stan"}))
assert.NoError(t, err)
assert.Equal(t, html, template.HTML(`<div>Hi, Stan.</div>`))
}
diff --git a/render_with_data_fetch_test.go b/render_with_data_fetch_test.go
index 963242e..e6166ed 100644
--- a/render_with_data_fetch_test.go
+++ b/render_with_data_fetch_test.go
@@ -1,6 +1,7 @@
package veun_test
import (
+ "context"
"fmt"
"html/template"
"testing"
@@ -45,7 +46,7 @@ func NewExpensiveView(shouldErr bool) *ExpensiveView {
return &ExpensiveView{Data: dataCh, Err: errCh}
}
-func (v *ExpensiveView) Renderable() (Renderable, error) {
+func (v *ExpensiveView) Renderable(_ context.Context) (Renderable, error) {
select {
case err := <-v.Err:
return nil, err
@@ -56,13 +57,13 @@ func (v *ExpensiveView) Renderable() (Renderable, error) {
func TestViewWithChannels(t *testing.T) {
t.Run("successful", func(t *testing.T) {
- html, err := Render(NewExpensiveView(false))
+ html, err := Render(context.Background(), NewExpensiveView(false))
assert.NoError(t, err)
assert.Equal(t, template.HTML(`hi success`), html)
})
t.Run("failed", func(t *testing.T) {
- _, err := Render(NewExpensiveView(true))
+ _, err := Render(context.Background(), NewExpensiveView(true))
assert.Error(t, err)
})
}
diff --git a/renderer.go b/renderer.go
index 27d3abc..33af885 100644
--- a/renderer.go
+++ b/renderer.go
@@ -2,37 +2,38 @@ package veun
import (
"bytes"
+ "context"
"fmt"
"html/template"
)
type Renderable interface {
- Template() (*template.Template, error)
- TemplateData() (any, error)
+ Template(ctx context.Context) (*template.Template, error)
+ TemplateData(ctx context.Context) (any, error)
}
type AsRenderable interface {
- Renderable() (Renderable, error)
+ Renderable(ctx context.Context) (Renderable, error)
}
-func Render(r AsRenderable) (template.HTML, error) {
- renderable, err := r.Renderable()
+func Render(ctx context.Context, r AsRenderable) (template.HTML, error) {
+ renderable, err := r.Renderable(ctx)
if err != nil {
- return handleRenderError(err, r)
+ return handleRenderError(ctx, err, r)
}
- out, err := render(renderable)
+ out, err := render(ctx, renderable)
if err != nil {
- return handleRenderError(err, r)
+ return handleRenderError(ctx, err, r)
}
return out, nil
}
-func render(r Renderable) (template.HTML, error) {
+func render(ctx context.Context, r Renderable) (template.HTML, error) {
var empty template.HTML
- tpl, err := r.Template()
+ tpl, err := r.Template(ctx)
if err != nil {
return empty, err
}
@@ -41,7 +42,7 @@ func render(r Renderable) (template.HTML, error) {
return empty, fmt.Errorf("missing template")
}
- data, err := r.TemplateData()
+ data, err := r.TemplateData(ctx)
if err != nil {
return empty, err
}
diff --git a/slots.go b/slots.go
index ef55359..61f7fb4 100644
--- a/slots.go
+++ b/slots.go
@@ -1,19 +1,24 @@
package veun
-import "html/template"
+import (
+ "context"
+ "html/template"
+)
type Slots map[string]AsRenderable
-func (s Slots) renderSlot(name string) (template.HTML, error) {
- slot, ok := s[name]
- if ok {
- return Render(slot)
- }
+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 {
+ return Render(ctx, slot)
+ }
- var empty template.HTML
- return empty, nil
+ var empty template.HTML
+ return empty, nil
+ }
}
-func (s Slots) addToTemplate(t *template.Template) *template.Template {
- return t.Funcs(template.FuncMap{"slot": s.renderSlot})
+func (s Slots) addToTemplate(ctx context.Context, t *template.Template) *template.Template {
+ return t.Funcs(template.FuncMap{"slot": s.renderSlot(ctx)})
}
diff --git a/view.go b/view.go
index 2bfc217..97f02f5 100644
--- a/view.go
+++ b/view.go
@@ -1,6 +1,9 @@
package veun
-import "html/template"
+import (
+ "context"
+ "html/template"
+)
type View struct {
Tpl *template.Template
@@ -8,15 +11,15 @@ type View struct {
Data any
}
-func (v View) Template() (*template.Template, error) {
- return v.Slots.addToTemplate(v.Tpl), nil
+func (v View) Template(ctx context.Context) (*template.Template, error) {
+ return v.Slots.addToTemplate(ctx, v.Tpl), nil
}
-func (v View) TemplateData() (any, error) {
+func (v View) TemplateData(_ context.Context) (any, error) {
return v.Data, nil
}
-func (v View) Renderable() (Renderable, error) {
+func (v View) Renderable(_ context.Context) (Renderable, error) {
return v, nil
}
testing for cancelled context (source: a6a523ee)
diff --git a/render_with_data_fetch_test.go b/render_with_data_fetch_test.go
index e6166ed..4d93650 100644
--- a/render_with_data_fetch_test.go
+++ b/render_with_data_fetch_test.go
@@ -23,7 +23,7 @@ type ExpensiveView struct {
Err chan error
}
-func NewExpensiveView(shouldErr bool) *ExpensiveView {
+func NewExpensiveView(shouldErr bool, sleepFor time.Duration) *ExpensiveView {
errCh := make(chan error)
dataCh := make(chan ExpensiveViewData)
@@ -35,7 +35,7 @@ func NewExpensiveView(shouldErr bool) *ExpensiveView {
// do data fetching and either write to
// one thing or the other
- time.Sleep(1 * time.Millisecond)
+ time.Sleep(sleepFor)
if shouldErr {
errCh <- fmt.Errorf("fetch failed")
} else {
@@ -46,8 +46,10 @@ func NewExpensiveView(shouldErr bool) *ExpensiveView {
return &ExpensiveView{Data: dataCh, Err: errCh}
}
-func (v *ExpensiveView) Renderable(_ context.Context) (Renderable, error) {
+func (v *ExpensiveView) Renderable(ctx context.Context) (Renderable, error) {
select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
case err := <-v.Err:
return nil, err
case data := <-v.Data:
@@ -57,13 +59,25 @@ func (v *ExpensiveView) Renderable(_ context.Context) (Renderable, error) {
func TestViewWithChannels(t *testing.T) {
t.Run("successful", func(t *testing.T) {
- html, err := Render(context.Background(), NewExpensiveView(false))
+ html, err := Render(context.Background(), NewExpensiveView(false, 1*time.Millisecond))
assert.NoError(t, err)
assert.Equal(t, template.HTML(`hi success`), html)
})
t.Run("failed", func(t *testing.T) {
- _, err := Render(context.Background(), NewExpensiveView(true))
+ _, err := Render(context.Background(), NewExpensiveView(true, 1*time.Millisecond))
assert.Error(t, err)
})
+
+ t.Run("context timed out", func(t *testing.T) {
+ ctx, _ := context.WithTimeout(context.Background(), 1*time.Millisecond)
+ _, err := Render(ctx, NewExpensiveView(false, 2*time.Millisecond))
+ assert.Error(t, err)
+ })
+
+ t.Run("context timeout not reached", func(t *testing.T) {
+ ctx, _ := context.WithTimeout(context.Background(), 5*time.Millisecond)
+ _, err := Render(ctx, NewExpensiveView(false, 2*time.Millisecond))
+ assert.NoError(t, err)
+ })
}
Fallible and WithTimeout
Because we can do call delegation in our views and render, we can force a situation where a subtree always stops rendering by creating a view which explicitly cancels a subcontext and passes it to the its subview.
type ViewWithTimeout struct {
Delegate AsRenderable
Timeout time.Duration
}
func (v ViewWithTimeout) Renderable(ctx context.Context) (Renderable, error) {
ctx, _ = context.WithTimeout(ctx, v.Timeout)
return v.Delegate.Renderable(ctx)
}
And using the FallibleView
from part-2 we can make sure that something
fetches and renders within a given time frame.
view := FallibleView{
Contents: ViewWithTimeout{
Delegate: expensiveView(),
Timeout: 100 * time.Millisecond,
},
ErrorRenderable: func(_ context.Context, err error) (AsRenderable, error) {
return View{/* */}, nil
},
}
testing context and timeouts and fallbacks (source: 6f95e9bb)
diff --git a/render_with_data_fetch_test.go b/render_with_data_fetch_test.go
index 4d93650..593784c 100644
--- a/render_with_data_fetch_test.go
+++ b/render_with_data_fetch_test.go
@@ -57,6 +57,16 @@ func (v *ExpensiveView) Renderable(ctx context.Context) (Renderable, error) {
}
}
+type ViewWithTimeout struct {
+ Delegate AsRenderable
+ Timeout time.Duration
+}
+
+func (v ViewWithTimeout) Renderable(ctx context.Context) (Renderable, error) {
+ ctx, _ = context.WithTimeout(ctx, v.Timeout)
+ return v.Delegate.Renderable(ctx)
+}
+
func TestViewWithChannels(t *testing.T) {
t.Run("successful", func(t *testing.T) {
html, err := Render(context.Background(), NewExpensiveView(false, 1*time.Millisecond))
@@ -80,4 +90,16 @@ func TestViewWithChannels(t *testing.T) {
_, err := Render(ctx, NewExpensiveView(false, 2*time.Millisecond))
assert.NoError(t, err)
})
+
+ t.Run("with timeout and fallible", func(t *testing.T) {
+ html, err := Render(context.Background(), FallibleView{
+ Child: ViewWithTimeout{
+ Delegate: NewExpensiveView(false, 10*time.Millisecond),
+ Timeout: 2 * time.Millisecond,
+ },
+ CapturesErr: context.DeadlineExceeded,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, template.HTML(`HEADING`), html)
+ })
}
Cleaning up, and examples of Context composition
We can make our own very similar WithTimeout
function.
func WithTimeout(r AsRenderable, timeout time.Duration) AsRenderable {
// ...
}
This is just a function signature, but seeing this immediately makes me think of ways that something can be extracted into a different kind of pattern.
HTTP middleware has the signature: func(http.Handler) http.Handler
.
We can update our function to look like this:
func WithTimeout(timeout time.Duration) func(AsRenderable) AsRenderable {
return func(r AsRenderable) AsRenderable {
// ...
}
}
Is this actually useful? How would this be used in practice?
Probably not by doing: WithTimeout(timeout)(view)
, but if we had some way
of applying these, like: Compose(view, WithTimeout(timeout))
, this might be ok.
Let's just save this idea for later...
A Renderable function
Something that is interesting here though is the part we gloss
over, (// ...
). Sometimes we don't need a full struct, sometimes
we only need a closure, and the resulting code can be clearer and
simpler to reason about.
type RenderableFunc func(context.Context) (Renderable, error)
func (f RenderableFunc) Renderable(ctx context.Context) (Renderable, error) {
return f(ctx)
}
moving renderable to renderable.go, adding RenderableFunc (source: 8c05c665)
diff --git a/renderable.go b/renderable.go
new file mode 100644
index 0000000..57f3791
--- /dev/null
+++ b/renderable.go
@@ -0,0 +1,21 @@
+package veun
+
+import (
+ "context"
+ "html/template"
+)
+
+type Renderable interface {
+ Template(ctx context.Context) (*template.Template, error)
+ TemplateData(ctx context.Context) (any, error)
+}
+
+type AsRenderable interface {
+ Renderable(ctx context.Context) (Renderable, error)
+}
+
+type RenderableFunc func(context.Context) (Renderable, error)
+
+func (f RenderableFunc) Renderable(ctx context.Context) (Renderable, error) {
+ return f(ctx)
+}
diff --git a/renderer.go b/renderer.go
index 33af885..ebbddef 100644
--- a/renderer.go
+++ b/renderer.go
@@ -7,15 +7,6 @@ import (
"html/template"
)
-type Renderable interface {
- Template(ctx context.Context) (*template.Template, error)
- TemplateData(ctx context.Context) (any, error)
-}
-
-type AsRenderable interface {
- Renderable(ctx context.Context) (Renderable, error)
-}
-
func Render(ctx context.Context, r AsRenderable) (template.HTML, error) {
renderable, err := r.Renderable(ctx)
if err != nil {
And now to use it:
func WithTimeout(timeout time.Duration) func(AsRenderable) AsRenderable {
return func(r AsRenderable) AsRenderable {
return RenderableFunc(func(ctx context.Context) (Renderable, error) {
ctx, _ = context.WithTimeout(timeout)
return r.Renderable(ctx)
})
}
}
N.B. Because go contexts are copies, cancelling subtree renders MUST BE done through delegating.
func WithErrorHandler(eh ErrorRenderable) func(AsRenderable) AsRenderable {
return func(r AsRenderable) AsRenderable {
return FallibleView{Contents: r, ErrorRenderable: eh}
}
}
Can we put it together?
func Compose(r AsRenderable, fs ...func(AsRenderable) AsRenderable) AsRenderable {
for _, f := range fs {
r = f(r)
}
return r
}
r := Compose(r, WithTimeout(timeout), WithErrorHandler(eh))
html, err := Render(ctx, r)
Except for writing AsRenderable
over and over and over again, that's not so bad,
and the usage is nice.