Commit f596f3f6 authored by Dustin L. Howett's avatar Dustin L. Howett
Browse files

Implement package views.

Package views provides Ghostbin's view model. Views are templates loaded
from a set of globbed files, and provide four template functions:
* global <var>
  Fetches a variable from the global view data provider.
* local <var>
  Fetches a variable from the local view data provider.
* now
  Returns the current time.
* subtemplate <name>
  Renders a sibling view and returns its HTML.

A view model alone is not useful; it can contain any number of
templates but provides no way to use them. This is where binding comes
in.

Once a single view is bound via model.Bind(name, dataProvider), it can
be used in perpetuity as a handle to the open template with its bound
name. It will look up `local` variables from its bound data provider,
and `global` variables from its parent's bound data provider.

Views can be bound with string names, in which case they are simply
executed by name (and subtemplate will not function properly), or with
the special type views.PageID. A view bound by way of views.PageID will
render the special `tmpl_page` template, passing the bound name along in
{{.page}}. {{.page}} is crucial to the operation of subtemplate; for a
given page "A", {{subtemplate "X"}} will render the template named
"A_X".
parents
package views
import "net/http"
// DataProvider represents a View's variable storage. It provides
// read-only access to a view-scoped variables. View executions are
// differentiated by way of the http.Request for which they were
// initiated.
type DataProvider interface {
ViewValue(r *http.Request, name string) interface{}
}
func varFromDataProvider(dp DataProvider) func(vctx viewContext, name string) interface{} {
return func(vctx viewContext, name string) interface{} {
return dp.ViewValue(vctx.r, name)
}
}
/*
Package views provides Ghostbin's view model. Views are templates loaded
from a set of globbed files.
The view model provides four predefined template functions, and disallows
the creation of additional bound functions.
global . <var>
Fetches a variable from the global view data provider.
local . <var>
Fetches a variable from the local view data provider.
now
Returns the current time.
subtemplate . <name>
Renders a sibling view and returns its HTML.
A view model alone is not useful; it can contain any number of
templates but provides no way to use them. This is where binding comes
in.
Once a single view is bound via model.Bind(name, dataProvider), it can
be used in perpetuity as a handle to the open template with its bound
name. It will look up `local` variables from its bound data provider,
and `global` variables from its parent's bound data provider.
Views can be bound with string names, in which case they are simply
executed by name (and subtemplate will not function properly), or with
the special type views.PageID. A view bound by way of views.PageID will
render the special `tmpl_page` template, passing the bound name along in
{{.page}}. {{.page}} is crucial to the operation of subtemplate; for a
given page "A", {{subtemplate "X"}} will render the template named
"A_X".
*/
package views
package views
import (
"errors"
"fmt"
"html/template"
"sync"
"time"
)
// Model represents a view model loaded from a set of files. Its
// behavior is documented in the package-level documentation above.
type Model struct {
mu sync.Mutex
glob string
baseTemplate *template.Template
tmpl *template.Template
bound []*View
}
// Bind combines a view model, a view ID, and a data provider into a
// single, durable reference to a template. The supplied data provider
// will be used for all `local` variable lookups for the durartion of the
// View's life.
func (m *Model) Bind(id interface{}, dp DataProvider) (*View, error) {
m.mu.Lock()
defer m.mu.Unlock()
var vid viewID
switch tid := id.(type) {
case string:
vid = stringViewID(tid)
case viewID:
vid = tid
default:
return nil, fmt.Errorf("unintelligible view ID passed to Bind: %v", id)
}
view := &View{
id: vid,
dp: dp,
}
err := view.rebind(m.tmpl)
if err != nil {
return nil, err
}
m.bound = append(m.bound, view)
return view, nil
}
// Reload reloads the model's view templates from disk, reconstructing
// all bound views and template functions. No views are re-evaluated or
// re-rendered.
func (m *Model) Reload() error {
m.mu.Lock()
defer m.mu.Unlock()
tmpl, err := m.baseTemplate.Clone()
if err != nil {
return err
}
tmpl, err = tmpl.ParseGlob(m.glob)
if err != nil {
return err
}
m.tmpl = tmpl
// rebind all bound views to the new template
// this supports the load/bind/reload scenario.
for _, bv := range m.bound {
err := bv.rebind(tmpl)
if err != nil {
return err
}
}
return nil
}
// New returns a new Model bound to the supplied data provider. The data
// provider will be used for all `global` variable lookups.
func New(glob string, globalDataProvider DataProvider) (*Model, error) {
m := &Model{
glob: glob,
}
tmpl := template.New(".base").Funcs(template.FuncMap{
// all provided functions must be defined here,
// otherwise the global parse will fail.
"global": varFromDataProvider(globalDataProvider),
"now": time.Now,
// rebind in subviews.
"subtemplate": func(args ...interface{}) interface{} {
// subtemplate is rebound for all bound views.
panic(errors.New("unbound use of subtemplate"))
},
"local": func(args ...interface{}) interface{} {
panic(errors.New("unbound use of local"))
},
})
m.baseTemplate = tmpl
return m, m.Reload()
}
package views
import (
"bytes"
"html/template"
"net/http"
"sync"
)
// viewContext represents the template context passed to each render.
type viewContext struct {
page string
r *http.Request
}
// viewID is the internal interface that allows us to differentiate page-based IDs from string-based IDs
type viewID interface {
// template returns the name of the template used to render the view with this ID
template() string
// baseContext creates a new viewContext with pre-seeded values.
baseContext() *viewContext
}
// PageID represents a view identified by its page name. Views with page
// name identifiers will be treated differently from string-bound views:
//
// * The template rendered for every Exec will be `tmpl_page`.
// * The page name will be provided to the view's rendering context.
// * The `subtemplate` function will render a sibling template prefixed
// with the page's name.
type PageID string
func (p PageID) template() string {
return "tmpl_page"
}
func (p PageID) baseContext() *viewContext {
return &viewContext{page: string(p)}
}
type stringViewID string
func (s stringViewID) template() string {
return string(s)
}
func (s stringViewID) baseContext() *viewContext {
return &viewContext{}
}
// View represents an ID bound to a data provider and Model. Its behavior
// is documented in the package-level documentation above.
type View struct {
mu sync.RWMutex
// immutable
id viewID
dp DataProvider
// mutable under mu
tmpl *template.Template
}
func (v *View) subtemplate(vctx *viewContext, name string) template.HTML {
buf := &bytes.Buffer{}
err := v.tmpl.ExecuteTemplate(buf, vctx.page+"_"+name, vctx)
if err != nil {
// We return an empty snippet here, as a subtemplate failing to exist is non-fatal.
return template.HTML("")
}
return template.HTML(buf.String())
}
func (v *View) rebind(root *template.Template) error {
v.mu.Lock()
defer v.mu.Unlock()
tmpl, err := root.Clone()
if err != nil {
return err
}
tmpl.Funcs(template.FuncMap{
"local": varFromDataProvider(v.dp),
"subtemplate": v.subtemplate,
})
v.tmpl = tmpl
return nil
}
// Exec executes a view given a ResponseWriter and a Request. The Request
// is used as the primary key for every variable lookup during the
// template's execution.
func (v *View) Exec(w http.ResponseWriter, r *http.Request) error {
v.mu.RLock()
t := v.tmpl
v.mu.RUnlock()
vctx := v.id.baseContext()
vctx.r = r
return t.ExecuteTemplate(w, v.id.template(), vctx)
}
// ServeHTTP exists to provide conformance with http.Handler, allowing a
// view to be bound and used directly as a response handler.
func (v *View) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// TODO(DH) Error handle this.
v.Exec(w, r)
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment