diff --git a/close17.go b/close17.go deleted file mode 100644 index 7a7cf69..0000000 --- a/close17.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (C) 2016 JT Olds -// See LICENSE for copying information - -// +build !go1.8 - -package webhelp - -import ( - "net/http" - - "golang.org/x/net/context" -) - -// CloseNotify causes a handler to have its request.Context() get canceled the -// second the client goes away, by hooking the http.CloseNotifier logic -// into the context. Without this addition, the context will still close when -// the handler completes. This will no longer be necessary with Go1.8. -func CloseNotify(h http.Handler) http.Handler { - return RouteHandlerFunc(h, func(w http.ResponseWriter, r *http.Request) { - if cnw, ok := w.(http.CloseNotifier); ok { - doneChan := make(chan bool) - defer close(doneChan) - - closeChan := cnw.CloseNotify() - ctx, cancelFunc := context.WithCancel(Context(r)) - r = WithContext(r, ctx) - - go func() { - select { - case <-doneChan: - cancelFunc() - case <-closeChan: - cancelFunc() - } - }() - } - h.ServeHTTP(w, r) - }) -} diff --git a/close18.go b/close18.go deleted file mode 100644 index c0d4690..0000000 --- a/close18.go +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (C) 2016 JT Olds -// See LICENSE for copying information - -// +build go1.8 - -package webhelp - -import ( - "net/http" - - "golang.org/x/net/context" -) - -// CloseNotify causes a handler to have its request.Context() get canceled the -// second the client goes away, by hooking the http.CloseNotifier logic -// into the context. Without this addition, the context will still close when -// the handler completes. This will no longer be necessary with Go1.8. -func CloseNotify(h http.Handler) http.Handler { - return h -} diff --git a/compat17.go b/compat17.go deleted file mode 100644 index 9de64d9..0000000 --- a/compat17.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (C) 2016 JT Olds -// See LICENSE for copying information - -// This file can go once everything uses go1.7 context semantics - -// +build go1.7 - -package webhelp - -import ( - "context" - "net/http" -) - -// Context is a light wrapper around the behavior of Go 1.7's -// (*http.Request).Context method, except this version works with Go 1.6 too. -func Context(r *http.Request) context.Context { - return r.Context() -} - -// WithContext is a light wrapper around the behavior of Go 1.7's -// (*http.Request).WithContext method, except this version works with Go 1.6 -// too. IMPORTANT CAVEAT: to get this to work for Go 1.6, a few tricks are -// pulled, such as expecting the returned r.URL to never change what object it -// points to, and a finalizer is set on the returned request. -func WithContext(r *http.Request, ctx context.Context) *http.Request { - return r.WithContext(ctx) -} - -// ContextBase is a back-compat handler for Go1.7 context features in Go1.6. -// You'll need to have this at the base of your handler stack. You don't need -// to use this if you're using webhelp.ListenAndServe. -func ContextBase(h http.Handler) http.Handler { - return h -} diff --git a/errors.go b/errors.go deleted file mode 100644 index c9d7faa..0000000 --- a/errors.go +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (C) 2016 JT Olds -// See LICENSE for copying information - -package webhelp - -import ( - "encoding/json" - "fmt" - "net/http" - - "github.com/spacemonkeygo/errors" - "github.com/spacemonkeygo/errors/errhttp" - "golang.org/x/net/context" -) - -var ( - HTTPError = errors.NewClass("HTTP Error", errors.NoCaptureStack()) - ErrBadRequest = HTTPError.NewClass("Bad request", - errhttp.SetStatusCode(http.StatusBadRequest)) - ErrNotFound = ErrBadRequest.NewClass("Not found", - errhttp.SetStatusCode(http.StatusNotFound)) - ErrMethodNotAllowed = ErrBadRequest.NewClass("Method not allowed", - errhttp.SetStatusCode(http.StatusMethodNotAllowed)) - ErrInternalServerError = HTTPError.NewClass("Internal server error", - errhttp.SetStatusCode(http.StatusInternalServerError)) - ErrUnauthorized = HTTPError.NewClass("Unauthorized", - errhttp.SetStatusCode(http.StatusUnauthorized)) - - errHandler = errors.GenSym() -) - -// Redirect is just http.Redirect with http.StatusSeeOther which I always -// forget. -func Redirect(w http.ResponseWriter, r *http.Request, redirectTo string) { - http.Redirect(w, r, redirectTo, http.StatusSeeOther) -} - -// HandleError uses the provided error handler given via HandleErrorsWith -// to handle the error, falling back to a built in default if not provided. -func HandleError(w http.ResponseWriter, r *http.Request, err error) { - if handler, ok := Context(r).Value(errHandler).(ErrorHandler); ok { - handler.HandleError(w, r, err) - return - } - logger.Errorf("error: %v", err) - http.Error(w, errhttp.GetErrorBody(err), - errhttp.GetStatusCode(err, http.StatusInternalServerError)) -} - -type ErrorHandler interface { - HandleError(w http.ResponseWriter, r *http.Request, err error) -} - -func HandleErrorsWith(eh ErrorHandler, h http.Handler) http.Handler { - return RouteHandlerFunc(h, func(w http.ResponseWriter, r *http.Request) { - ctx := context.WithValue(Context(r), errHandler, eh) - h.ServeHTTP(w, WithContext(r, ctx)) - }) -} - -type ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) - -func (f ErrorHandlerFunc) HandleError(w http.ResponseWriter, r *http.Request, - err error) { - f(w, r, err) -} - -var ( - JSONErrorHandler = ErrorHandlerFunc(jsonErrorHandler) -) - -func jsonErrorHandler(w http.ResponseWriter, r *http.Request, err error) { - logger.Errorf("error: %v", err) - data, err := json.MarshalIndent(map[string]string{ - "err": errhttp.GetErrorBody(err)}, "", " ") - if err != nil { - logger.Critf("failed serializing error: %v", err) - data = []byte(`{"err": "Internal Server Error"}`) - } - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Content-Length", fmt.Sprint(len(data))) - w.WriteHeader(errhttp.GetStatusCode(err, http.StatusInternalServerError)) - w.Write(data) -} - -func RenderJSON(w http.ResponseWriter, r *http.Request, value interface{}) { - data, err := json.MarshalIndent( - map[string]interface{}{"resp": value}, "", " ") - if err != nil { - if handler, ok := Context(r).Value(errHandler).(ErrorHandler); ok { - handler.HandleError(w, r, err) - return - } - jsonErrorHandler(w, r, err) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Content-Length", fmt.Sprint(len(data))) - w.Write(data) -} diff --git a/examples_test.go b/examples_test.go index 550ee12..1f73e89 100644 --- a/examples_test.go +++ b/examples_test.go @@ -6,20 +6,27 @@ package webhelp_test import ( "fmt" "net/http" - "testing" - "github.com/jtolds/webhelp" + "github.com/jtolds/webhelp/whcompat" + "github.com/jtolds/webhelp/whlog" + "github.com/jtolds/webhelp/whmux" ) -func Example(t *testing.T) { - pageName := webhelp.NewStringArgMux() - handler := webhelp.DirMux{ - "wiki": pageName.Shift(webhelp.Exact(http.HandlerFunc( - func(w http.ResponseWriter, r *http.Request) { - name := pageName.Get(r.Context()) - w.Header().Set("Content-Type", "text/plain") - fmt.Fprintf(w, "Welcome to %s", name) - })))} - - webhelp.ListenAndServe(":0", handler) +var ( + pageName = whmux.NewStringArg() +) + +func page(w http.ResponseWriter, r *http.Request) { + name := pageName.Get(whcompat.Context(r)) + + w.Header().Set("Content-Type", "text/plain") + fmt.Fprintf(w, "Welcome to %s", name) +} + +func Example() { + pageHandler := pageName.Shift(whmux.Exact(http.HandlerFunc(page))) + + whlog.ListenAndServe(":0", whmux.Dir{ + "wiki": pageHandler, + }) } diff --git a/fatal.go b/fatal.go deleted file mode 100644 index 790abb7..0000000 --- a/fatal.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (C) 2016 JT Olds -// See LICENSE for copying information - -package webhelp - -import ( - "net/http" -) - -type fatalBehavior func(w http.ResponseWriter, r *http.Request) - -// FatalHandler takes a Handler and returns a new one that works with -// Fatal, FatalRedirect, and FatalError. FatalHandler should be placed *inside* -// a LoggingHandler, HandleErrorsWith, and a few other handlers, otherwise -// the wrapper will be one of the things interrupted by Fatal calls. -func FatalHandler(h http.Handler) http.Handler { - return RouteHandlerFunc(h, func(w http.ResponseWriter, r *http.Request) { - rw := wrapResponseWriter(w) - defer func() { - rec := recover() - if rec == nil { - return - } - behavior, ok := rec.(fatalBehavior) - if !ok { - panic(rec) - } - if behavior != nil { - behavior(rw, r) - } - if !rw.WroteHeader() { - rw.WriteHeader(http.StatusInternalServerError) - } - }() - h.ServeHTTP(rw, r) - }) -} - -// FatalRedirect is like Redirect but panics so all additional request -// processing terminates. Implemented with Fatal(). -// -// IMPORTANT: must be used with FatalHandler, or else the http.ResponseWriter -// won't be able to be obtained. Because this requires FatalHandler, if -// you're writing a library intended to be used by others, please avoid this -// and other Fatal* methods. If you are writing a library intended to be used -// by yourself, you should probably avoid these methods anyway. -func FatalRedirect(redirectTo string) { - Fatal(func(w http.ResponseWriter, r *http.Request) { - Redirect(w, r, redirectTo) - }) -} - -// FatalError is like HandleError but panics so that all additional request -// processing terminates. Implemented with Fatal() -// -// IMPORTANT: must be used with FatalHandler, or else the http.ResponseWriter -// won't be able to be obtained. Because this requires FatalHandler, if -// you're writing a library intended to be used by others, please avoid this -// and other Fatal* methods. If you are writing a library intended to be used -// by yourself, you should probably avoid these methods anyway. -func FatalError(err error) { - Fatal(func(w http.ResponseWriter, r *http.Request) { - HandleError(w, r, err) - }) -} - -// Fatal panics in a way that FatalHandler understands to abort all additional -// request processing. Once request processing has been aborted, handler is -// called, if not nil. If handler doesn't write a response, a 500 will -// automatically be returned. FatalError and FatalRedirect are implemented -// using this method. -// -// IMPORTANT: must be used with FatalHandler, or else the http.ResponseWriter -// won't be able to be obtained. Because this requires FatalHandler, if -// you're writing a library intended to be used by others, please avoid this -// and other Fatal* methods. If you are writing a library intended to be used -// by yourself, you should probably avoid these methods anyway. -func Fatal(handler func(w http.ResponseWriter, r *http.Request)) { - // even if handler == nil, this is NOT the same as panic(nil), so we're okay. - panic(fatalBehavior(handler)) -} diff --git a/gensym.go b/gensym.go index c9906c6..d9d261a 100644 --- a/gensym.go +++ b/gensym.go @@ -4,38 +4,25 @@ package webhelp import ( - "github.com/spacemonkeygo/errors" + "sync" ) -// GenSym generates a brand new, never-before-seen symbol for use as a -// Context.WithValue key. -// -// Example usage: -// -// var UserKey = webhelp.GenSym() -// -// func myWrapper(h http.Handler) http.Handler { -// return webhelp.RouteHandlerFunc(h, -// func(w http.ResponseWriter, r *http.Request) { -// user, err := loadUser(r) -// if err != nil { -// webhelp.HandleError(w, r, err) -// return -// } -// h.ServeHTTP(w, webhelp.WithContext(r, -// context.WithValue(webhelp.Context(r), UserKey, user))) -// }) -// } -// -// func myHandler(w http.ResponseWriter, r *http.Request) { -// ctx := webhelp.Context(r) -// if user, ok := ctx.Value(UserKey).(*User); ok { -// // do something with the user -// } -// } -// -// func Routes() http.Handler { -// return myWrapper(http.HandlerFunc(myHandler)) -// } -// -func GenSym() interface{} { return errors.GenSym() } +var ( + keyMtx sync.Mutex + keyCounter uint64 +) + +// ContextKey is only useful via the GenSym() constructor. See GenSym() for +// more documentation +type ContextKey struct { + id uint64 +} + +// GenSym generates a brand new, never-before-seen ContextKey for use as a +// Context.WithValue key. Please see the example. +func GenSym() ContextKey { + keyMtx.Lock() + defer keyMtx.Unlock() + keyCounter += 1 + return ContextKey{id: keyCounter} +} diff --git a/gensym_test.go b/gensym_test.go new file mode 100644 index 0000000..1ef1798 --- /dev/null +++ b/gensym_test.go @@ -0,0 +1,66 @@ +// Copyright (C) 2016 JT Olds +// See LICENSE for copying information + +package webhelp_test + +import ( + "fmt" + "net/http" + + "github.com/jtolds/webhelp" + "github.com/jtolds/webhelp/whcompat" + "github.com/jtolds/webhelp/wherr" + "github.com/jtolds/webhelp/whlog" + "github.com/jtolds/webhelp/whroute" + "golang.org/x/net/context" +) + +var ( + UserKey = webhelp.GenSym() +) + +type User struct { + Name string +} + +func loadUser(r *http.Request) (user *User, err error) { + return nil, wherr.InternalServerError.New("not implemented yet") +} + +// myWrapper will load the user from a request, serving any detected errors, +// and otherwise passing the request along to the wrapped handler with the +// user bound inside the context. +func myWrapper(h http.Handler) http.Handler { + return whroute.HandlerFunc(h, + func(w http.ResponseWriter, r *http.Request) { + + user, err := loadUser(r) + if err != nil { + wherr.Handle(w, r, err) + return + } + + h.ServeHTTP(w, whcompat.WithContext(r, + context.WithValue(whcompat.Context(r), UserKey, user))) + }) +} + +// myHandler is a standard http.HandlerFunc that expects to be able to load +// a user out of the request context. +func myHandler(w http.ResponseWriter, r *http.Request) { + ctx := whcompat.Context(r) + if user, ok := ctx.Value(UserKey).(*User); ok { + // do something with the user + fmt.Fprint(w, user.Name) + } +} + +// Routes returns an http.Handler. You might have a whmux.Dir or something +// in here. +func Routes() http.Handler { + return myWrapper(http.HandlerFunc(myHandler)) +} + +func ExampleGenSym() { + whlog.ListenAndServe(":0", Routes()) +} diff --git a/logging.go b/logging.go deleted file mode 100644 index 00e259b..0000000 --- a/logging.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (C) 2016 JT Olds -// See LICENSE for copying information - -package webhelp - -import ( - "net/http" - - "github.com/spacemonkeygo/spacelog" -) - -var ( - logger = spacelog.GetLogger() -) - -// LoggingHandler takes a Handler and makes it log requests. FatalHandlers -// should be placed *inside* LoggingHandlers if applicable. -func LoggingHandler(h http.Handler) http.Handler { - return RouteHandlerFunc(h, func(w http.ResponseWriter, r *http.Request) { - method, requestURI := r.Method, r.RequestURI - rw := wrapResponseWriter(w) - - logger.Infof("%s %s", method, requestURI) - h.ServeHTTP(rw, r) - - if !rw.WroteHeader() { - rw.WriteHeader(http.StatusOK) - } - - code := rw.StatusCode() - - level := spacelog.Error - if code >= 200 && code < 300 { - level = spacelog.Notice - } - - logger.Logf(level, `%s %#v %d %d %d`, method, requestURI, code, - r.ContentLength, rw.Written()) - }) -} diff --git a/mux.go b/mux.go deleted file mode 100644 index 1208a20..0000000 --- a/mux.go +++ /dev/null @@ -1,257 +0,0 @@ -// Copyright (C) 2016 JT Olds -// See LICENSE for copying information - -package webhelp - -import ( - "net/http" - "sort" - "strings" -) - -// DirMux is an http.Handler that mimics a directory. It mutates an incoming -// request's URL.Path to properly namespace handlers. This way a handler can -// assume it has the root of its section. If you want the original URL, use -// req.RequestURI (but don't modify it). -type DirMux map[string]http.Handler - -func (d DirMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { - dir, left := Shift(r.URL.Path) - handler, ok := d[dir] - if !ok { - HandleError(w, r, ErrNotFound.New("resource: %#v", dir)) - return - } - r.URL.Path = left - handler.ServeHTTP(w, r) -} - -func (d DirMux) Routes( - cb func(method, path string, annotations map[string]string)) { - keys := make([]string, 0, len(d)) - for element := range d { - keys = append(keys, element) - } - sort.Strings(keys) - for _, element := range keys { - Routes(d[element], - func(method, path string, annotations map[string]string) { - if element == "" { - cb(method, "/", annotations) - } else { - cb(method, "/"+element+path, annotations) - } - }) - } -} - -var _ http.Handler = DirMux(nil) -var _ RouteLister = DirMux(nil) - -// Shift pulls the first directory out of the path and returns the remainder. -func Shift(path string) (dir, left string) { - // slice off the first "/"s if they exists - path = strings.TrimLeft(path, "/") - - if len(path) == 0 { - return "", "" - } - - // find the first '/' after the initial one - split := strings.Index(path, "/") - if split == -1 { - return path, "" - } - return path[:split], path[split:] -} - -// MethodMux is an http.Handler muxer that keys off of the given HTTP request -// method -type MethodMux map[string]http.Handler - -func (m MethodMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if handler, found := m[r.Method]; found { - handler.ServeHTTP(w, r) - return - } - HandleError(w, r, ErrMethodNotAllowed.New("bad method: %#v", r.Method)) -} - -func (m MethodMux) Routes( - cb func(method, path string, annotations map[string]string)) { - keys := make([]string, 0, len(m)) - for method := range m { - keys = append(keys, method) - } - sort.Strings(keys) - for _, method := range keys { - handler := m[method] - Routes(handler, func(_, path string, annotations map[string]string) { - cb(method, path, annotations) - }) - } -} - -var _ http.Handler = MethodMux(nil) -var _ RouteLister = MethodMux(nil) - -// ExactPath takes an http.Handler that returns a new http.Handler that doesn't -// accept any more path elements -func ExactPath(h http.Handler) http.Handler { - return DirMux{"": h} -} - -// RequireMethod takes an http.Handler and returns a new http.Handler that only -// works with the given HTTP method. -func RequireMethod(method string, h http.Handler) http.Handler { - return MethodMux{method: h} -} - -// RequireGet is simply RequireMethod but called with "GET" as the first -// argument. -func RequireGet(h http.Handler) http.Handler { - return RequireMethod("GET", h) -} - -// Exact is simply RequireGet and ExactPath called together. -func Exact(h http.Handler) http.Handler { - return RequireGet(ExactPath(h)) -} - -// HostMux is an http.Handler that chooses a subhandler based on the request -// Host header. The star host ("*") is a default handler. -type HostMux map[string]http.Handler - -func (h HostMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { - handler, ok := h[r.Host] - if !ok { - handler, ok = h["*"] - if !ok { - HandleError(w, r, ErrNotFound.New("host: %#v", r.Host)) - return - } - } - handler.ServeHTTP(w, r) -} - -func (h HostMux) Routes( - cb func(method, path string, annotations map[string]string)) { - keys := make([]string, 0, len(h)) - for element := range h { - keys = append(keys, element) - } - sort.Strings(keys) - for _, host := range keys { - Routes(h[host], func(method, path string, annotations map[string]string) { - cp := make(map[string]string, len(annotations)+1) - for key, val := range annotations { - cp[key] = val - } - cp["Host"] = host - cb(method, path, cp) - }) - } -} - -var _ http.Handler = HostMux(nil) -var _ RouteLister = HostMux(nil) - -// OverlayMux is essentially a DirMux that you can put in front of another -// http.Handler. If the requested entry isn't in the overlay DirMux, the -// Default will be used. If no Default is specified this works exactly the -// same as a DirMux. -type OverlayMux struct { - Default http.Handler - Overlay DirMux -} - -func (o OverlayMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { - dir, left := Shift(r.URL.Path) - handler, ok := o.Overlay[dir] - if !ok { - if o.Default == nil { - HandleError(w, r, ErrNotFound.New("resource: %#v", dir)) - return - } - o.Default.ServeHTTP(w, r) - return - } - r.URL.Path = left - handler.ServeHTTP(w, r) -} - -func (o OverlayMux) Routes( - cb func(method, path string, annotations map[string]string)) { - Routes(o.Overlay, cb) - if o.Default != nil { - Routes(o.Default, cb) - } -} - -var _ http.Handler = OverlayMux{} -var _ RouteLister = OverlayMux{} - -type redirectHandler string - -// RedirectHandler returns an http.Handler that redirects all requests to url. -func RedirectHandler(url string) http.Handler { - return redirectHandler(url) -} - -func (t redirectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - Redirect(w, r, string(t)) -} - -func (t redirectHandler) Routes( - cb func(method, path string, annotations map[string]string)) { - cb(AllMethods, AllPaths, map[string]string{"Redirect": string(t)}) -} - -var _ http.Handler = redirectHandler("") -var _ RouteLister = redirectHandler("") - -// RedirectHandlerFunc is an http.Handler that redirects all requests to the -// returned URL. -type RedirectHandlerFunc func(r *http.Request) string - -func (f RedirectHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) { - Redirect(w, r, f(r)) -} - -func (f RedirectHandlerFunc) Routes( - cb func(method, path string, annotations map[string]string)) { - cb(AllMethods, AllPaths, map[string]string{"Redirect": "f(req)"}) -} - -var _ http.Handler = RedirectHandlerFunc(nil) -var _ RouteLister = RedirectHandlerFunc(nil) - -// RequireHTTPS returns a handler that will redirect to the same path but using -// https if https was not already used. -func RequireHTTPS(handler http.Handler) http.Handler { - return RouteHandlerFunc(handler, - func(w http.ResponseWriter, r *http.Request) { - if r.URL.Scheme != "https" { - u := *r.URL - u.Scheme = "https" - Redirect(w, r, u.String()) - } else { - handler.ServeHTTP(w, r) - } - }) -} - -// RequireHost returns a handler that will redirect to the same path but using -// the given host if the given host was not specifically requested. -func RequireHost(host string, handler http.Handler) http.Handler { - if host == "*" { - return handler - } - return HostMux{ - host: handler, - "*": RedirectHandlerFunc(func(r *http.Request) string { - u := *r.URL - u.Host = host - return u.String() - })} -} diff --git a/package.go b/package.go deleted file mode 100644 index e7a1ace..0000000 --- a/package.go +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (C) 2016 JT Olds -// See LICENSE for copying information - -// package webhelp is a bunch of useful utilities for whenever I do web -// programming in Go. Like a framework, but better, cause it's not. -// -// Note that this tightly integrates with Context objects. You can read more -// about them here: https://blog.golang.org/context -// -// Recently redone to include all of the new Go 1.7 things. -// -// See an example (that includes OAuth2) at -// https://github.com/jtolds/webhelp-oauth2/blob/master/examples/one/main.go -// -package webhelp diff --git a/pkg.go b/pkg.go new file mode 100644 index 0000000..cc8416c --- /dev/null +++ b/pkg.go @@ -0,0 +1,15 @@ +// Copyright (C) 2016 JT Olds +// See LICENSE for copying information + +// package webhelp is a bunch of useful utilities for doing web programming +// in Go. webhelp encourages you to use the standard library for web +// programming, but provides some oft-needed tools to help simplify the task. +// +// webhelp tightly integrates with the new Go 1.7 Request Context support, +// but has backported the functionality to previous Go releases in the whcompat +// subpackage. +// +// See an example (that includes OAuth2) at +// https://github.com/jtolds/webhelp-oauth2/blob/master/examples/one/main.go +// +package webhelp diff --git a/whcompat/close17.go b/whcompat/close17.go new file mode 100644 index 0000000..2a16bae --- /dev/null +++ b/whcompat/close17.go @@ -0,0 +1,42 @@ +// Copyright (C) 2016 JT Olds +// See LICENSE for copying information + +// +build !go1.8 + +package whcompat + +import ( + "net/http" + + "github.com/jtolds/webhelp/whroute" + "golang.org/x/net/context" +) + +// CloseNotify causes a handler to have its request.Context() canceled the +// second the client TCP connection goes away by hooking the http.CloseNotifier +// logic into the context. Prior to Go 1.8, this costs an extra goroutine in +// a read loop. Go 1.8 and on, this behavior happens automatically with or +// without this wrapper. +func CloseNotify(h http.Handler) http.Handler { + return whroute.HandlerFunc(h, + func(w http.ResponseWriter, r *http.Request) { + if cnw, ok := w.(http.CloseNotifier); ok { + doneChan := make(chan bool) + defer close(doneChan) + + closeChan := cnw.CloseNotify() + ctx, cancelFunc := context.WithCancel(Context(r)) + r = WithContext(r, ctx) + + go func() { + select { + case <-doneChan: + cancelFunc() + case <-closeChan: + cancelFunc() + } + }() + } + h.ServeHTTP(w, r) + }) +} diff --git a/whcompat/close18.go b/whcompat/close18.go new file mode 100644 index 0000000..01cd83c --- /dev/null +++ b/whcompat/close18.go @@ -0,0 +1,21 @@ +// Copyright (C) 2016 JT Olds +// See LICENSE for copying information + +// +build go1.8 + +package whcompat + +import ( + "net/http" + + "golang.org/x/net/context" +) + +// CloseNotify causes a handler to have its request.Context() canceled the +// second the client TCP connection goes away by hooking the http.CloseNotifier +// logic into the context. Prior to Go 1.8, this costs an extra goroutine in +// a read loop. Go 1.8 and on, this behavior happens automatically with or +// without this wrapper. +func CloseNotify(h http.Handler) http.Handler { + return h +} diff --git a/compat16.go b/whcompat/compat16.go similarity index 65% rename from compat16.go rename to whcompat/compat16.go index 961dec3..6108c86 100644 --- a/compat16.go +++ b/whcompat/compat16.go @@ -5,7 +5,7 @@ // +build !go1.7 -package webhelp +package whcompat import ( "net/http" @@ -13,6 +13,7 @@ import ( "runtime" "sync" + "github.com/jtolds/webhelp/whroute" "golang.org/x/net/context" ) @@ -26,7 +27,11 @@ var ( ) // Context is a light wrapper around the behavior of Go 1.7's -// (*http.Request).Context method, except this version works with Go 1.6 too. +// (*http.Request).Context method, except this version works with earlier Go +// releases, too. In Go 1.7 and on, this simply calls r.Context(). See the +// note for WithContext for how this works on previous Go releases. +// If building with the appengine tag, when needed, fresh contexts will be +// generated with appengine.NewContext(). func Context(r *http.Request) context.Context { reqCtxMappingsMtx.Lock() info, ok := reqCtxMappings[r.URL] @@ -61,10 +66,11 @@ func copyReqAndURL(r *http.Request) (c *http.Request) { } // WithContext is a light wrapper around the behavior of Go 1.7's -// (*http.Request).WithContext method, except this version works with Go 1.6 -// too. IMPORTANT CAVEAT: to get this to work for Go 1.6, a few tricks are -// pulled, such as expecting the returned r.URL to never change what object it -// points to, and a finalizer is set on the returned request. +// (*http.Request).WithContext method, except this version works with earlier +// Go releases, too. IMPORTANT CAVEAT: to get this to work for Go 1.6 and +// earlier, a few tricks are pulled, such as expecting the returned r.URL to +// never change what object it points to, and a finalizer is set on the +// returned request. func WithContext(r *http.Request, ctx context.Context) *http.Request { if ctx == nil { panic("nil ctx") @@ -75,11 +81,11 @@ func WithContext(r *http.Request, ctx context.Context) *http.Request { return r } -// ContextBase is a back-compat handler for Go1.7 context features in Go1.6. -// You'll need to have this at the base of your handler stack. You don't need -// to use this if you're using webhelp.ListenAndServe. -func ContextBase(h http.Handler) http.Handler { - return RouteHandlerFunc(h, +// DoneNotify cancels request contexts when the http.Handler returns in Go +// releases prior to Go 1.7. In Go 1.7 and forward, this is a no-op. +// You get this behavior for free if you use whlog.ListenAndServe. +func DoneNotify(h http.Handler) http.Handler { + return whroute.HandlerFunc(h, func(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithCancel(Context(r)) defer cancel() diff --git a/whcompat/compat17.go b/whcompat/compat17.go new file mode 100644 index 0000000..9155c1b --- /dev/null +++ b/whcompat/compat17.go @@ -0,0 +1,40 @@ +// Copyright (C) 2016 JT Olds +// See LICENSE for copying information + +// This file can go once everything uses go1.7 context semantics + +// +build go1.7 + +package whcompat + +import ( + "context" + "net/http" +) + +// Context is a light wrapper around the behavior of Go 1.7's +// (*http.Request).Context method, except this version works with earlier Go +// releases, too. In Go 1.7 and on, this simply calls r.Context(). See the +// note for WithContext for how this works on previous Go releases. +// If building with the appengine tag, when needed, fresh contexts will be +// generated with appengine.NewContext(). +func Context(r *http.Request) context.Context { + return r.Context() +} + +// WithContext is a light wrapper around the behavior of Go 1.7's +// (*http.Request).WithContext method, except this version works with earlier +// Go releases, too. IMPORTANT CAVEAT: to get this to work for Go 1.6 and +// earlier, a few tricks are pulled, such as expecting the returned r.URL to +// never change what object it points to, and a finalizer is set on the +// returned request. +func WithContext(r *http.Request, ctx context.Context) *http.Request { + return r.WithContext(ctx) +} + +// DoneNotify cancels request contexts when the http.Handler returns in Go +// releases prior to Go 1.7. In Go 1.7 and forward, this is a no-op. +// You get this behavior for free if you use whlog.ListenAndServe. +func DoneNotify(h http.Handler) http.Handler { + return h +} diff --git a/compat_ae.go b/whcompat/compat_ae.go similarity index 94% rename from compat_ae.go rename to whcompat/compat_ae.go index 35c900c..ffd2739 100644 --- a/compat_ae.go +++ b/whcompat/compat_ae.go @@ -4,7 +4,7 @@ // +build appengine // +build !go1.7 -package webhelp +package whcompat import ( "net/http" diff --git a/compat_nae.go b/whcompat/compat_nae.go similarity index 93% rename from compat_nae.go rename to whcompat/compat_nae.go index bfe896c..ffabbd9 100644 --- a/compat_nae.go +++ b/whcompat/compat_nae.go @@ -4,7 +4,7 @@ // +build !appengine // +build !go1.7 -package webhelp +package whcompat import ( "net/http" diff --git a/whcompat/pkg.go b/whcompat/pkg.go new file mode 100644 index 0000000..d20b69e --- /dev/null +++ b/whcompat/pkg.go @@ -0,0 +1,11 @@ +// Copyright (C) 2016 JT Olds +// See LICENSE for copying information + +// Package whcompat provides webhelp compatibility across different Go +// releases. +// +// The webhelp suite depends heavily on Go 1.7 style http.Request contexts, +// which aren't available in earlier Go releases. This package backports all +// of the functionality in a forwards-compatible way. You can use this package +// to get the desired behavior for all Go releases. +package whcompat diff --git a/wherr/errors.go b/wherr/errors.go new file mode 100644 index 0000000..d33c27f --- /dev/null +++ b/wherr/errors.go @@ -0,0 +1,103 @@ +// Copyright (C) 2016 JT Olds +// See LICENSE for copying information + +// Package wherr provides a unified error handling framework for http.Handlers. +package wherr + +import ( + "net/http" + + "github.com/jtolds/webhelp" + "github.com/jtolds/webhelp/whcompat" + "github.com/jtolds/webhelp/whroute" + "github.com/spacemonkeygo/errors" + "github.com/spacemonkeygo/errors/errhttp" + "github.com/spacemonkeygo/spacelog" + "golang.org/x/net/context" +) + +var ( + HTTPError = errors.NewClass("HTTP Error", errors.NoCaptureStack()) + + BadRequest = ErrorClass(http.StatusBadRequest) + Unauthorized = ErrorClass(http.StatusUnauthorized) + Forbidden = ErrorClass(http.StatusForbidden) + NotFound = ErrorClass(http.StatusNotFound) + MethodNotAllowed = ErrorClass(http.StatusMethodNotAllowed) + NotAcceptable = ErrorClass(http.StatusNotAcceptable) + RequestTimeout = ErrorClass(http.StatusRequestTimeout) + Conflict = ErrorClass(http.StatusConflict) + Gone = ErrorClass(http.StatusGone) + LengthRequired = ErrorClass(http.StatusLengthRequired) + PreconditionFailed = ErrorClass(http.StatusPreconditionFailed) + RequestEntityTooLarge = ErrorClass(http.StatusRequestEntityTooLarge) + RequestURITooLong = ErrorClass(http.StatusRequestURITooLong) + UnsupportedMediaType = ErrorClass(http.StatusUnsupportedMediaType) + RequestedRangeNotSatisfiable = ErrorClass(http.StatusRequestedRangeNotSatisfiable) + ExpectationFailed = ErrorClass(http.StatusExpectationFailed) + Teapot = ErrorClass(http.StatusTeapot) + InternalServerError = ErrorClass(http.StatusInternalServerError) + NotImplemented = ErrorClass(http.StatusNotImplemented) + BadGateway = ErrorClass(http.StatusBadGateway) + ServiceUnavailable = ErrorClass(http.StatusServiceUnavailable) + GatewayTimeout = ErrorClass(http.StatusGatewayTimeout) + + errHandler = webhelp.GenSym() + + logger = spacelog.GetLogger() +) + +// ErrorClass creates a new subclass of HTTPError using the given HTTP status +// code +func ErrorClass(code int) *errors.ErrorClass { + msg := http.StatusText(code) + if msg == "" { + msg = "Unknown error" + } + return HTTPError.NewClass(msg, errhttp.SetStatusCode(code)) +} + +// Handle uses the provided error handler given via HandleWith +// to handle the error, falling back to a built in default if not provided. +func Handle(w http.ResponseWriter, r *http.Request, err error) { + if handler, ok := whcompat.Context(r).Value(errHandler).(Handler); ok { + handler.HandleError(w, r, err) + return + } + logger.Errorf("error: %v", err) + http.Error(w, errhttp.GetErrorBody(err), + errhttp.GetStatusCode(err, http.StatusInternalServerError)) +} + +// Handlers handle errors. After HandleError returns, it's assumed a response +// has been written out and all error handling has completed. +type Handler interface { + HandleError(w http.ResponseWriter, r *http.Request, err error) +} + +// HandleWith binds the given eror Handler to the request contexts that pass +// through the given http.Handler. wherr.Handle will use this error Handler +// for handling errors. If you're using the whfatal package, you should place +// a whfatal.Catch inside this handler, so this error handler can deal +// with Fatal requests. +func HandleWith(eh Handler, h http.Handler) http.Handler { + return whroute.HandlerFunc(h, + func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(whcompat.Context(r), errHandler, eh) + h.ServeHTTP(w, whcompat.WithContext(r, ctx)) + }) +} + +// HandlingWith returns the error handler if registered, or nil if no error +// handler is registered and the default should be used. +func HandlingWith(ctx context.Context) Handler { + handler, _ := ctx.Value(errHandler).(Handler) + return handler +} + +type HandlerFunc func(w http.ResponseWriter, r *http.Request, err error) + +func (f HandlerFunc) HandleError(w http.ResponseWriter, r *http.Request, + err error) { + f(w, r, err) +} diff --git a/wherr/example_test.go b/wherr/example_test.go new file mode 100644 index 0000000..c6f0c26 --- /dev/null +++ b/wherr/example_test.go @@ -0,0 +1,49 @@ +// Copyright (C) 2016 JT Olds +// See LICENSE for copying information + +package wherr_test + +import ( + "fmt" + "net/http" + + "github.com/jtolds/webhelp/wherr" + "github.com/jtolds/webhelp/whlog" + "github.com/jtolds/webhelp/whmux" + "github.com/spacemonkeygo/errors/errhttp" +) + +func PageName(r *http.Request) (string, error) { + if r.FormValue("name") == "" { + return "", wherr.BadRequest.New("No page name supplied") + } + return r.FormValue("name"), nil +} + +func Page(w http.ResponseWriter, r *http.Request) { + name, err := PageName(r) + if err != nil { + // This will use our error handler! + wherr.Handle(w, r, err) + return + } + + fmt.Fprintf(w, name) + // do more stuff +} + +func Routes() http.Handler { + return whmux.Dir{ + "page": http.HandlerFunc(Page), + } +} + +func ErrorHandler(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, "some error happened!", errhttp.GetStatusCode(err, 500)) +} + +func Example() { + // If we didn't register our error handler, we'd end up using a default one. + whlog.ListenAndServe(":0", wherr.HandleWith(wherr.HandlerFunc(ErrorHandler), + Routes())) +} diff --git a/whfatal/fatal.go b/whfatal/fatal.go new file mode 100644 index 0000000..a6f38ed --- /dev/null +++ b/whfatal/fatal.go @@ -0,0 +1,96 @@ +// Copyright (C) 2016 JT Olds +// See LICENSE for copying information + +// Package whfatal uses panics to make early termination of http.Handlers +// easier. No other webhelp package depends on or uses this one. +package whfatal + +import ( + "net/http" + + "github.com/jtolds/webhelp/wherr" + "github.com/jtolds/webhelp/whmon" + "github.com/jtolds/webhelp/whredir" + "github.com/jtolds/webhelp/whroute" +) + +type fatalBehavior func(w http.ResponseWriter, r *http.Request) + +// Catch takes a Handler and returns a new one that works with Fatal, +// whfatal.Redirect, and whfatal.Error. Catch will also catch panics that are +// wherr.HTTPError errors. Catch should be placed *inside* a whlog.LogRequests +// handler, wherr.HandleWith handlers, and a few other handlers. Otherwise, +// the wrapper will be one of the things interrupted by Fatal calls. +func Catch(h http.Handler) http.Handler { + return whroute.HandlerFunc(h, + func(w http.ResponseWriter, r *http.Request) { + rw := whmon.WrapResponseWriter(w) + defer func() { + rec := recover() + if rec == nil { + return + } + behavior, ok := rec.(fatalBehavior) + if !ok { + perr, ok := rec.(error) + if !ok || !wherr.HTTPError.Contains(perr) { + panic(rec) + } + behavior = func(w http.ResponseWriter, r *http.Request) { + wherr.Handle(w, r, perr) + } + } + if behavior != nil { + behavior(rw, r) + } + if !rw.WroteHeader() { + rw.WriteHeader(http.StatusInternalServerError) + } + }() + h.ServeHTTP(rw, r) + }) +} + +// Redirect is like whredir.Redirect but panics so all additional request +// processing terminates. Implemented with Fatal(). +// +// IMPORTANT: must be used with whfatal.Catch, or else the http.ResponseWriter +// won't be able to be obtained. Because this requires whfatal.Catch, if +// you're writing a library intended to be used by others, please avoid this +// and other Fatal* methods. If you are writing a library intended to be used +// by yourself, you should probably avoid these methods anyway. +func Redirect(redirectTo string) { + Fatal(func(w http.ResponseWriter, r *http.Request) { + whredir.Redirect(w, r, redirectTo) + }) +} + +// Error is like wherr.Handle but panics so that all additional request +// processing terminates. Implemented with Fatal() +// +// IMPORTANT: must be used with whfatal.Catch, or else the http.ResponseWriter +// won't be able to be obtained. Because this requires whfatal.Catch, if +// you're writing a library intended to be used by others, please avoid this +// and other Fatal* methods. If you are writing a library intended to be used +// by yourself, you should probably avoid these methods anyway. +func Error(err error) { + Fatal(func(w http.ResponseWriter, r *http.Request) { + wherr.Handle(w, r, err) + }) +} + +// Fatal panics in a way that Catch understands to abort all additional +// request processing. Once request processing has been aborted, handler is +// called, if not nil. If handler doesn't write a response, a 500 will +// automatically be returned. whfatal.Error and whfatal.Redirect are +// implemented using this method. +// +// IMPORTANT: must be used with whfatal.Catch, or else the http.ResponseWriter +// won't be able to be obtained. Because this requires whfatal.Catch, if +// you're writing a library intended to be used by others, please avoid this +// and other Fatal* methods. If you are writing a library intended to be used +// by yourself, you should probably avoid these methods anyway. +func Fatal(handler func(w http.ResponseWriter, r *http.Request)) { + // even if handler == nil, this is NOT the same as panic(nil), so we're okay. + panic(fatalBehavior(handler)) +} diff --git a/whjson/json.go b/whjson/json.go new file mode 100644 index 0000000..68a86f4 --- /dev/null +++ b/whjson/json.go @@ -0,0 +1,62 @@ +// Copyright (C) 2016 JT Olds +// See LICENSE for copying information + +// Package whjson provides some nice utilities for dealing with JSON-based +// APIs, such as a good JSON wherr.Handler. +package whjson + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/jtolds/webhelp/whcompat" + "github.com/jtolds/webhelp/wherr" + "github.com/spacemonkeygo/errors/errhttp" + "github.com/spacemonkeygo/spacelog" +) + +var ( + // ErrHandler provides a good wherr.Handler. It will return a JSON object + // like `{"err": "message"}` where message is filled in with + // errhttp.GetErrorBody. The status code is set with errhttp.GetStatusCode. + ErrHandler = wherr.HandlerFunc(errHandler) + + logger = spacelog.GetLogger() +) + +func errHandler(w http.ResponseWriter, r *http.Request, err error) { + logger.Errorf("error: %v", err) + data, err := json.MarshalIndent(map[string]string{ + "err": errhttp.GetErrorBody(err)}, "", " ") + if err != nil { + logger.Critf("failed serializing error: %v", err) + data = []byte(`{"err": "Internal Server Error"}`) + } + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Length", fmt.Sprint(len(data))) + w.WriteHeader(errhttp.GetStatusCode(err, http.StatusInternalServerError)) + w.Write(data) +} + +// Render will render JSON `value` like `{"resp": }`, falling back to +// ErrHandler if no error handler was registered and an error is +// encountered. This is good for making sure your API is always returning +// usefully namespaced JSON objects that are clearly differentiated from error +// responses. +func Render(w http.ResponseWriter, r *http.Request, value interface{}) { + data, err := json.MarshalIndent( + map[string]interface{}{"resp": value}, "", " ") + if err != nil { + if handler := wherr.HandlingWith(whcompat.Context(r)); handler != nil { + handler.HandleError(w, r, err) + return + } + errHandler(w, r, err) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Length", fmt.Sprint(len(data))) + w.Write(data) +} diff --git a/whlog/logging.go b/whlog/logging.go new file mode 100644 index 0000000..b91ae37 --- /dev/null +++ b/whlog/logging.go @@ -0,0 +1,53 @@ +// Copyright (C) 2016 JT Olds +// See LICENSE for copying information + +// Package whlog provides functionality to log incoming requests and results. +package whlog + +import ( + "net/http" + + "github.com/jtolds/webhelp/whmon" + "github.com/jtolds/webhelp/whroute" + "github.com/spacemonkeygo/spacelog" +) + +var ( + logger = spacelog.GetLogger() +) + +// LogRequests takes a Handler and makes it log requests. LogRequests uses +// whmon's ResponseWriter to keep track of activity. whfatal.Catch should be +// placed *inside* if applicable. +func LogRequests(h http.Handler) http.Handler { + return whroute.HandlerFunc(h, + func(w http.ResponseWriter, r *http.Request) { + method, requestURI := r.Method, r.RequestURI + rw := whmon.WrapResponseWriter(w) + + logger.Infof("%s %s", method, requestURI) + + defer func() { + rec := recover() + if rec != nil { + logger.Critf("Panic: %v", rec) + panic(rec) + } + }() + h.ServeHTTP(rw, r) + + if !rw.WroteHeader() { + rw.WriteHeader(http.StatusOK) + } + + code := rw.StatusCode() + + level := spacelog.Error + if code >= 200 && code < 300 { + level = spacelog.Notice + } + + logger.Logf(level, `%s %#v %d %d %d`, method, requestURI, code, + r.ContentLength, rw.Written()) + }) +} diff --git a/serve.go b/whlog/serve.go similarity index 87% rename from serve.go rename to whlog/serve.go index e335b3b..8ca29de 100644 --- a/serve.go +++ b/whlog/serve.go @@ -1,12 +1,14 @@ // Copyright (C) 2016 JT Olds // See LICENSE for copying information -package webhelp +package whlog import ( "net" "net/http" "time" + + "github.com/jtolds/webhelp/whcompat" ) const ( @@ -43,12 +45,12 @@ func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { } // ListenAndServe creates a TCP listener prior to calling Serve. It also logs -// the address it listens on. +// the address it listens on, and wraps given handlers in whcompat.DoneNotify. func ListenAndServe(addr string, handler http.Handler) error { l, err := net.Listen("tcp", addr) if err != nil { return err } logger.Noticef("listening on %s", l.Addr()) - return Serve(l, ContextBase(handler)) + return Serve(l, whcompat.DoneNotify(handler)) } diff --git a/writer.go b/whmon/writer.go similarity index 62% rename from writer.go rename to whmon/writer.go index 16b86be..5843b94 100644 --- a/writer.go +++ b/whmon/writer.go @@ -1,10 +1,14 @@ // Copyright (C) 2016 JT Olds // See LICENSE for copying information -package webhelp +// Package whmon provides a means to wrap a ResponseWriter to monitor and +// keep track of responses. +package whmon import ( "net/http" + + "github.com/jtolds/webhelp/whroute" ) type rWriter struct { @@ -46,7 +50,7 @@ func (r *rWriter) StatusCode() int { func (r *rWriter) WroteHeader() bool { return r.wroteHeader } func (r *rWriter) Written() int64 { return r.written } -type MonitoredResponseWriter interface { +type ResponseWriter interface { // Header, Write, and WriteHeader are exactly like http.ResponseWriter Header() http.Header Write([]byte) (int, error) @@ -63,21 +67,23 @@ type MonitoredResponseWriter interface { } // MonitorResponse wraps all incoming http.ResponseWriters with a -// MonitoredResponseWriter that keeps track of additional status information +// monitored ResponseWriter that keeps track of additional status information // about the outgoing response. It preserves whether or not the passed in // response writer is an http.Flusher, http.CloseNotifier, or an http.Hijacker. -// LoggingHandler and FatalHandler also do this for you. +// whlog.LogRequests and whfatal.Catch also do this for you. func MonitorResponse(h http.Handler) http.Handler { - return RouteHandlerFunc(h, func(w http.ResponseWriter, r *http.Request) { - h.ServeHTTP(wrapResponseWriter(w), r) - }) + return whroute.HandlerFunc(h, + func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(WrapResponseWriter(w), r) + }) } -// wrapResponseWriter's goal is to make a webhelp.ResponseWriter that has the -// same optional methods as the wrapped http.ResponseWriter -// (Flush, CloseNotify, Hijack). this ends up being SUPER MESSY -func wrapResponseWriter(w http.ResponseWriter) MonitoredResponseWriter { - if ww, ok := w.(MonitoredResponseWriter); ok { +// WrapResponseWriter will wrap an http.ResponseWriter with the instrumentation +// to turn it into a whmon.ResponseWriter. An http.ResponseWriter must be +// turned into a whmon.ResponseWriter before being used. It's much better +// to use MonitorResponse instead of WrapResponseWriter. +func WrapResponseWriter(w http.ResponseWriter) ResponseWriter { + if ww, ok := w.(ResponseWriter); ok { // don't do it if we already have the methods we need return ww } @@ -89,45 +95,45 @@ func wrapResponseWriter(w http.ResponseWriter) MonitoredResponseWriter { if cnok { if hok { return struct { - MonitoredResponseWriter + ResponseWriter http.Flusher http.CloseNotifier http.Hijacker }{ - MonitoredResponseWriter: rw, - Flusher: fw, - CloseNotifier: cnw, - Hijacker: hw, + ResponseWriter: rw, + Flusher: fw, + CloseNotifier: cnw, + Hijacker: hw, } } else { return struct { - MonitoredResponseWriter + ResponseWriter http.Flusher http.CloseNotifier }{ - MonitoredResponseWriter: rw, - Flusher: fw, - CloseNotifier: cnw, + ResponseWriter: rw, + Flusher: fw, + CloseNotifier: cnw, } } } else { if hok { return struct { - MonitoredResponseWriter + ResponseWriter http.Flusher http.Hijacker }{ - MonitoredResponseWriter: rw, - Flusher: fw, - Hijacker: hw, + ResponseWriter: rw, + Flusher: fw, + Hijacker: hw, } } else { return struct { - MonitoredResponseWriter + ResponseWriter http.Flusher }{ - MonitoredResponseWriter: rw, - Flusher: fw, + ResponseWriter: rw, + Flusher: fw, } } } @@ -135,31 +141,31 @@ func wrapResponseWriter(w http.ResponseWriter) MonitoredResponseWriter { if cnok { if hok { return struct { - MonitoredResponseWriter + ResponseWriter http.CloseNotifier http.Hijacker }{ - MonitoredResponseWriter: rw, - CloseNotifier: cnw, - Hijacker: hw, + ResponseWriter: rw, + CloseNotifier: cnw, + Hijacker: hw, } } else { return struct { - MonitoredResponseWriter + ResponseWriter http.CloseNotifier }{ - MonitoredResponseWriter: rw, - CloseNotifier: cnw, + ResponseWriter: rw, + CloseNotifier: cnw, } } } else { if hok { return struct { - MonitoredResponseWriter + ResponseWriter http.Hijacker }{ - MonitoredResponseWriter: rw, - Hijacker: hw, + ResponseWriter: rw, + Hijacker: hw, } } else { return rw diff --git a/argmux.go b/whmux/argmux.go similarity index 64% rename from argmux.go rename to whmux/argmux.go index a06800a..4a2e7bc 100644 --- a/argmux.go +++ b/whmux/argmux.go @@ -1,24 +1,26 @@ // Copyright (C) 2016 JT Olds // See LICENSE for copying information -package webhelp +package whmux import ( "net/http" "strconv" - "sync/atomic" "golang.org/x/net/context" -) -// StringArgMux is a way to pull off arbitrary path elements from an incoming -// URL. You'll need to create one with NewStringArgMux. -type StringArgMux int64 + "github.com/jtolds/webhelp" + "github.com/jtolds/webhelp/whcompat" + "github.com/jtolds/webhelp/wherr" + "github.com/jtolds/webhelp/whroute" +) -var argMuxCounter int64 +// StringArg is a way to pull off arbitrary path elements from an incoming +// URL. You'll need to create one with NewStringArg. +type StringArg webhelp.ContextKey -func NewStringArgMux() StringArgMux { - return StringArgMux(atomic.AddInt64(&argMuxCounter, 1)) +func NewStringArg() StringArg { + return StringArg(webhelp.GenSym()) } // Shift takes an http.Handler and returns a new http.Handler that does @@ -27,18 +29,18 @@ func NewStringArgMux() StringArgMux { // path and puts the value in the current Context. It then passes processing // off to the wrapped http.Handler. The value will be an empty string if no // argument is found. -func (a StringArgMux) Shift(h http.Handler) http.Handler { +func (a StringArg) Shift(h http.Handler) http.Handler { return a.ShiftOpt(h, notFoundHandler{}) } type stringOptShift struct { - a StringArgMux + a StringArg found, notfound http.Handler } // ShiftOpt is like Shift but the first handler is used only if there's an // argument found and the second handler is used if there isn't. -func (a StringArgMux) ShiftOpt(found, notfound http.Handler) http.Handler { +func (a StringArg) ShiftOpt(found, notfound http.Handler) http.Handler { return stringOptShift{a: a, found: found, notfound: notfound} } @@ -49,55 +51,55 @@ func (ssi stringOptShift) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } r.URL.Path = newpath - ctx := context.WithValue(Context(r), ssi.a, arg) - ssi.found.ServeHTTP(w, WithContext(r, ctx)) + ctx := context.WithValue(whcompat.Context(r), ssi.a, arg) + ssi.found.ServeHTTP(w, whcompat.WithContext(r, ctx)) } func (ssi stringOptShift) Routes(cb func(string, string, map[string]string)) { - Routes(ssi.found, + whroute.Routes(ssi.found, func(method, path string, annotations map[string]string) { cb(method, "/"+path, annotations) }) - Routes(ssi.notfound, + whroute.Routes(ssi.notfound, func(method, path string, annotations map[string]string) { switch path { - case AllPaths, "/": + case whroute.AllPaths, "/": cb(method, "/", annotations) } }) } var _ http.Handler = stringOptShift{} -var _ RouteLister = stringOptShift{} +var _ whroute.Lister = stringOptShift{} // Get returns a stored value for the Arg from the Context, or "" if no value // was found (which won't be the case if a higher-level handler was this -// argmux) -func (a StringArgMux) Get(ctx context.Context) (val string) { +// arg) +func (a StringArg) Get(ctx context.Context) (val string) { if val, ok := ctx.Value(a).(string); ok { return val } return "" } -// IntArgMux is a way to pull off numeric path elements from an incoming -// URL. You'll need to create one with NewIntArgMux. -type IntArgMux int64 +// IntArg is a way to pull off numeric path elements from an incoming +// URL. You'll need to create one with NewIntArg. +type IntArg webhelp.ContextKey -func NewIntArgMux() IntArgMux { - return IntArgMux(atomic.AddInt64(&argMuxCounter, 1)) +func NewIntArg() IntArg { + return IntArg(webhelp.GenSym()) } type notFoundHandler struct{} func (notFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - HandleError(w, r, ErrNotFound.New("resource: %#v", r.URL.Path)) + wherr.Handle(w, r, wherr.NotFound.New("resource: %#v", r.URL.Path)) } func (notFoundHandler) Routes(cb func(string, string, map[string]string)) {} var _ http.Handler = notFoundHandler{} -var _ RouteLister = notFoundHandler{} +var _ whroute.Lister = notFoundHandler{} // Shift takes an http.Handler and returns a new http.Handler that does // additional request processing. When an incoming request is processed, the @@ -105,18 +107,18 @@ var _ RouteLister = notFoundHandler{} // path and puts the value in the current Context. It then passes processing // off to the wrapped http.Handler. It responds with a 404 if no numeric value // is found. -func (a IntArgMux) Shift(h http.Handler) http.Handler { +func (a IntArg) Shift(h http.Handler) http.Handler { return a.ShiftOpt(h, notFoundHandler{}) } type intOptShift struct { - a IntArgMux + a IntArg found, notfound http.Handler } // ShiftOpt is like Shift but will only use the first handler if there's a // numeric argument found and the second handler otherwise. -func (a IntArgMux) ShiftOpt(found, notfound http.Handler) http.Handler { +func (a IntArg) ShiftOpt(found, notfound http.Handler) http.Handler { return intOptShift{a: a, found: found, notfound: notfound} } @@ -128,24 +130,25 @@ func (isi intOptShift) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } r.URL.Path = newpath - ctx := context.WithValue(Context(r), isi.a, val) - isi.found.ServeHTTP(w, WithContext(r, ctx)) + ctx := context.WithValue(whcompat.Context(r), isi.a, val) + isi.found.ServeHTTP(w, whcompat.WithContext(r, ctx)) } func (isi intOptShift) Routes(cb func(string, string, map[string]string)) { - Routes(isi.found, func(method, path string, annotations map[string]string) { + whroute.Routes(isi.found, func(method, path string, + annotations map[string]string) { cb(method, "/"+path, annotations) }) - Routes(isi.notfound, cb) + whroute.Routes(isi.notfound, cb) } var _ http.Handler = intOptShift{} -var _ RouteLister = intOptShift{} +var _ whroute.Lister = intOptShift{} // Get returns a stored value for the Arg from the Context and ok = true if // found, or ok = false if no value was found (which won't be the case if a -// higher-level handler was this argmux) -func (a IntArgMux) Get(ctx context.Context) (val int64, ok bool) { +// higher-level handler was this arg) +func (a IntArg) Get(ctx context.Context) (val int64, ok bool) { if val, ok := ctx.Value(a).(int64); ok { return val, true } @@ -153,11 +156,11 @@ func (a IntArgMux) Get(ctx context.Context) (val int64, ok bool) { } // MustGet is like Get but panics in cases when ok would be false. If used with -// FatalHandler, will return a 404 to the user. -func (a IntArgMux) MustGet(ctx context.Context) (val int64) { +// whfatal.Catch, will return a 404 to the user. +func (a IntArg) MustGet(ctx context.Context) (val int64) { val, ok := ctx.Value(a).(int64) if !ok { - FatalError(ErrNotFound.New("Required argument missing")) + panic(wherr.NotFound.New("Required argument missing")) } return val } diff --git a/whmux/mux.go b/whmux/mux.go new file mode 100644 index 0000000..9135e13 --- /dev/null +++ b/whmux/mux.go @@ -0,0 +1,209 @@ +// Copyright (C) 2016 JT Olds +// See LICENSE for copying information + +// Package whmux provides some useful request mux helpers for demultiplexing +// requests to one of a number of handlers. +package whmux + +import ( + "net/http" + "sort" + "strings" + + "github.com/jtolds/webhelp/wherr" + "github.com/jtolds/webhelp/whroute" +) + +// Dir is an http.Handler that mimics a directory. It mutates an incoming +// request's URL.Path to properly namespace handlers. This way a handler can +// assume it has the root of its section. If you want the original URL, use +// req.RequestURI (but don't modify it). +type Dir map[string]http.Handler + +// ServeHTTP implements http.handler +func (d Dir) ServeHTTP(w http.ResponseWriter, r *http.Request) { + dir, left := Shift(r.URL.Path) + handler, ok := d[dir] + if !ok { + wherr.Handle(w, r, wherr.NotFound.New("resource: %#v", dir)) + return + } + r.URL.Path = left + handler.ServeHTTP(w, r) +} + +// Routes implements whroute.Lister +func (d Dir) Routes( + cb func(method, path string, annotations map[string]string)) { + keys := make([]string, 0, len(d)) + for element := range d { + keys = append(keys, element) + } + sort.Strings(keys) + for _, element := range keys { + whroute.Routes(d[element], + func(method, path string, annotations map[string]string) { + if element == "" { + cb(method, "/", annotations) + } else { + cb(method, "/"+element+path, annotations) + } + }) + } +} + +var _ http.Handler = Dir(nil) +var _ whroute.Lister = Dir(nil) + +// Shift pulls the first directory out of the path and returns the remainder. +func Shift(path string) (dir, left string) { + // slice off the first "/"s if they exists + path = strings.TrimLeft(path, "/") + + if len(path) == 0 { + return "", "" + } + + // find the first '/' after the initial one + split := strings.Index(path, "/") + if split == -1 { + return path, "" + } + return path[:split], path[split:] +} + +// Method is an http.Handler muxer that keys off of the given HTTP request +// method. +type Method map[string]http.Handler + +// ServeHTTP implements http.handler +func (m Method) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if handler, found := m[r.Method]; found { + handler.ServeHTTP(w, r) + return + } + wherr.Handle(w, r, + wherr.MethodNotAllowed.New("bad method: %#v", r.Method)) +} + +// Routes implements whroute.Lister +func (m Method) Routes( + cb func(method, path string, annotations map[string]string)) { + keys := make([]string, 0, len(m)) + for method := range m { + keys = append(keys, method) + } + sort.Strings(keys) + for _, method := range keys { + handler := m[method] + whroute.Routes(handler, + func(_, path string, annotations map[string]string) { + cb(method, path, annotations) + }) + } +} + +var _ http.Handler = Method(nil) +var _ whroute.Lister = Method(nil) + +// ExactPath takes an http.Handler that returns a new http.Handler that doesn't +// accept any more path elements and returns a 404 if more are provided. +func ExactPath(h http.Handler) http.Handler { + return Dir{"": h} +} + +// RequireMethod takes an http.Handler and returns a new http.Handler that only +// works with the given HTTP method. If a different method is used, a 405 is +// returned. +func RequireMethod(method string, h http.Handler) http.Handler { + return Method{method: h} +} + +// RequireGet is simply RequireMethod but called with "GET" as the first +// argument. +func RequireGet(h http.Handler) http.Handler { + return RequireMethod("GET", h) +} + +// Exact is simply RequireGet and ExactPath called together. +func Exact(h http.Handler) http.Handler { + return RequireGet(ExactPath(h)) +} + +// Host is an http.Handler that chooses a subhandler based on the request +// Host header. The star host ("*") is a default handler. +type Host map[string]http.Handler + +// ServeHTTP implements http.handler +func (h Host) ServeHTTP(w http.ResponseWriter, r *http.Request) { + handler, ok := h[r.Host] + if !ok { + handler, ok = h["*"] + if !ok { + wherr.Handle(w, r, wherr.NotFound.New("host: %#v", r.Host)) + return + } + } + handler.ServeHTTP(w, r) +} + +// Routes implements whroute.Lister +func (h Host) Routes( + cb func(method, path string, annotations map[string]string)) { + keys := make([]string, 0, len(h)) + for element := range h { + keys = append(keys, element) + } + sort.Strings(keys) + for _, host := range keys { + whroute.Routes(h[host], + func(method, path string, annotations map[string]string) { + cp := make(map[string]string, len(annotations)+1) + for key, val := range annotations { + cp[key] = val + } + cp["Host"] = host + cb(method, path, cp) + }) + } +} + +var _ http.Handler = Host(nil) +var _ whroute.Lister = Host(nil) + +// Overlay is essentially a Dir that you can put in front of another +// http.Handler. If the requested entry isn't in the overlay Dir, the +// Default will be used. If no Default is specified this works exactly the +// same as a Dir. +type Overlay struct { + Default http.Handler + Overlay Dir +} + +// ServeHTTP implements http.handler +func (o Overlay) ServeHTTP(w http.ResponseWriter, r *http.Request) { + dir, left := Shift(r.URL.Path) + handler, ok := o.Overlay[dir] + if !ok { + if o.Default == nil { + wherr.Handle(w, r, wherr.NotFound.New("resource: %#v", dir)) + return + } + o.Default.ServeHTTP(w, r) + return + } + r.URL.Path = left + handler.ServeHTTP(w, r) +} + +// Routes implements whroute.Lister +func (o Overlay) Routes( + cb func(method, path string, annotations map[string]string)) { + whroute.Routes(o.Overlay, cb) + if o.Default != nil { + whroute.Routes(o.Default, cb) + } +} + +var _ http.Handler = Overlay{} +var _ whroute.Lister = Overlay{} diff --git a/query.go b/whparse/query.go similarity index 93% rename from query.go rename to whparse/query.go index 4c06ebe..c0ae0d1 100644 --- a/query.go +++ b/whparse/query.go @@ -1,7 +1,8 @@ // Copyright (C) 2016 JT Olds // See LICENSE for copying information -package webhelp +// Package whparse provides some convenient input parsing helpers. +package whparse import ( "strconv" diff --git a/whredir/redirect.go b/whredir/redirect.go new file mode 100644 index 0000000..ed8f8e7 --- /dev/null +++ b/whredir/redirect.go @@ -0,0 +1,90 @@ +// Copyright (C) 2016 JT Olds +// See LICENSE for copying information + +// Package whredir provides some helper methods and handlers for redirecting +// incoming requests to other URLs. +package whredir + +import ( + "net/http" + + "github.com/jtolds/webhelp/whmux" + "github.com/jtolds/webhelp/whroute" +) + +// Redirect is just http.Redirect with http.StatusSeeOther which I always +// forget. +func Redirect(w http.ResponseWriter, r *http.Request, redirectTo string) { + http.Redirect(w, r, redirectTo, http.StatusSeeOther) +} + +type redirectHandler string + +// RedirectHandler returns an http.Handler that redirects all requests to url. +func RedirectHandler(url string) http.Handler { + return redirectHandler(url) +} + +// ServeHTTP implements http.handler +func (t redirectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + Redirect(w, r, string(t)) +} + +// Routes implements whroute.Lister +func (t redirectHandler) Routes( + cb func(method, path string, annotations map[string]string)) { + cb(whroute.AllMethods, whroute.AllPaths, + map[string]string{"Redirect": string(t)}) +} + +var _ http.Handler = redirectHandler("") +var _ whroute.Lister = redirectHandler("") + +// RedirectHandlerFunc is an http.Handler that redirects all requests to the +// returned URL. +type RedirectHandlerFunc func(r *http.Request) string + +// ServeHTTP implements http.handler +func (f RedirectHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) { + Redirect(w, r, f(r)) +} + +// Routes implements whroute.Lister +func (f RedirectHandlerFunc) Routes( + cb func(method, path string, annotations map[string]string)) { + cb(whroute.AllMethods, whroute.AllPaths, + map[string]string{"Redirect": "f(req)"}) +} + +var _ http.Handler = RedirectHandlerFunc(nil) +var _ whroute.Lister = RedirectHandlerFunc(nil) + +// RequireHTTPS returns a handler that will redirect to the same path but using +// https if https was not already used. +func RequireHTTPS(handler http.Handler) http.Handler { + return whroute.HandlerFunc(handler, + func(w http.ResponseWriter, r *http.Request) { + if r.URL.Scheme != "https" { + u := *r.URL + u.Scheme = "https" + Redirect(w, r, u.String()) + } else { + handler.ServeHTTP(w, r) + } + }) +} + +// RequireHost returns a handler that will redirect to the same path but using +// the given host if the given host was not specifically requested. +func RequireHost(host string, handler http.Handler) http.Handler { + if host == "*" { + return handler + } + return whmux.Host{ + host: handler, + "*": RedirectHandlerFunc(func(r *http.Request) string { + u := *r.URL + u.Host = host + return u.String() + })} +} diff --git a/routes.go b/whroute/routes.go similarity index 67% rename from routes.go rename to whroute/routes.go index 79be132..249791b 100644 --- a/routes.go +++ b/whroute/routes.go @@ -1,7 +1,9 @@ // Copyright (C) 2016 JT Olds // See LICENSE for copying information -package webhelp +// Package whroute provides utilities to implement route listing, whereby +// http.Handlers that opt in can list what routes they understand. +package whroute import ( "fmt" @@ -11,27 +13,32 @@ import ( ) const ( + // AllMethods should be returned from a whroute.Lister when all methods are + // successfully handled. AllMethods = "ALL" - AllPaths = "[/<*>]" + + // AllPaths should be returned from a whroute.Lister when all paths are + // successfully handled. + AllPaths = "[/<*>]" ) -// RouteLister is an interface handlers can implement if they want Routes to -// work. -type RouteLister interface { +// Lister is an interface handlers can implement if they want the Routes +// method to work. All http.Handlers in the webhelp package implement Routes. +type Lister interface { Routes(cb func(method, path string, annotations map[string]string)) } // Routes will call cb with all routes known to h. func Routes(h http.Handler, cb func(method, path string, annotations map[string]string)) { - if rl, ok := h.(RouteLister); ok { + if rl, ok := h.(Lister); ok { rl.Routes(cb) } else { cb(AllMethods, AllPaths, nil) } } -// PrintRoutes will write all routes of h to out. +// PrintRoutes will write all routes of h to out, using the Routes method. func PrintRoutes(out io.Writer, h http.Handler) (err error) { Routes(h, func(method, path string, annotations map[string]string) { if err != nil { @@ -65,9 +72,9 @@ type routeHandlerFunc struct { fn func(http.ResponseWriter, *http.Request) } -// RouteHandlerFunc advertises the routes from routes, but serves content using +// HandlerFunc advertises the routes from routes, but serves content using // fn. -func RouteHandlerFunc(routes http.Handler, +func HandlerFunc(routes http.Handler, fn func(http.ResponseWriter, *http.Request)) http.Handler { return routeHandlerFunc{ routes: routes, diff --git a/sessions/cookie.go b/whsess/cookie.go similarity index 99% rename from sessions/cookie.go rename to whsess/cookie.go index 935bc81..d722e6d 100644 --- a/sessions/cookie.go +++ b/whsess/cookie.go @@ -1,7 +1,7 @@ // Copyright (C) 2016 JT Olds // See LICENSE for copying information -package sessions +package whsess import ( "bytes" diff --git a/sessions/store.go b/whsess/store.go similarity index 88% rename from sessions/store.go rename to whsess/store.go index 7bc52cb..18b3804 100644 --- a/sessions/store.go +++ b/whsess/store.go @@ -1,16 +1,17 @@ // Copyright (C) 2016 JT Olds // See LICENSE for copying information -// package sessions is a lightweight session storage mechanism for the webhelp +// package whsess is a lightweight session storage mechanism for the webhelp // package. Attempting to be a combination of minimal and useful. Implementing // the Store interface is all one must do to provide a different session // storage mechanism. -package sessions +package whsess import ( "net/http" - "github.com/jtolds/webhelp" + "github.com/jtolds/webhelp/whcompat" + "github.com/jtolds/webhelp/whroute" "github.com/spacemonkeygo/errors" "golang.org/x/net/context" ) @@ -48,10 +49,10 @@ type reqCtx struct { // HandlerWithStore wraps a webhelp.Handler such that Load works with contexts // provided in that Handler. func HandlerWithStore(s Store, h http.Handler) http.Handler { - return webhelp.RouteHandlerFunc(h, + return whroute.HandlerFunc(h, func(w http.ResponseWriter, r *http.Request) { - h.ServeHTTP(w, webhelp.WithContext(r, context.WithValue( - webhelp.Context(r), reqCtxKey, &reqCtx{s: s, r: r}))) + h.ServeHTTP(w, whcompat.WithContext(r, context.WithValue( + whcompat.Context(r), reqCtxKey, &reqCtx{s: s, r: r}))) }) } diff --git a/tmpl.go b/whtmpl/tmpl.go similarity index 73% rename from tmpl.go rename to whtmpl/tmpl.go index c838cf3..7bc27eb 100644 --- a/tmpl.go +++ b/whtmpl/tmpl.go @@ -1,7 +1,9 @@ // Copyright (C) 2016 JT Olds // See LICENSE for copying information -package webhelp +// Package whtmpl provides some helpful utilities for constructing and using +// lots of html/templates +package whtmpl import ( "fmt" @@ -9,8 +11,9 @@ import ( "net/http" "path/filepath" "runtime" - "strings" + + "github.com/jtolds/webhelp/wherr" ) // Pair is a useful type that allows for passing more than one current template @@ -25,12 +28,12 @@ import ( // {{ $val1 := .First }} // {{ $val2 := .Second }} // -// "makepair" is registered as a template function inside TemplateCollections +// "makepair" is registered as a template function inside a Collection type Pair struct { First, Second interface{} } -// TemplateCollection is a useful type that helps when defining a bunch of html +// Collection is a useful type that helps when defining a bunch of html // inside of Go files. Assuming you want to define a template called "landing" // that references another template called "header". With a template // collection, you would make three files: @@ -39,9 +42,9 @@ type Pair struct { // // package views // -// import "github.com/jtolds/webhelp" +// import "github.com/jtolds/webhelp/whtmpl" // -// var Templates = webhelp.NewTemplateCollection() +// var Templates = whtmpl.NewCollection() // // landing.go: // @@ -67,13 +70,13 @@ type Pair struct { // * safeurl: calls template.URL with its first argument and returns the // result // -type TemplateCollection struct { +type Collection struct { group *template.Template } -// Creates a new TemplateCollection. -func NewTemplateCollection() *TemplateCollection { - return &TemplateCollection{group: template.New("").Funcs( +// Creates a new Collection. +func NewCollection() *Collection { + return &Collection{group: template.New("").Funcs( template.FuncMap{ "makepair": func(first, second interface{}) Pair { return Pair{First: first, Second: second} @@ -85,14 +88,14 @@ func NewTemplateCollection() *TemplateCollection { } // Allows you to add and overwrite template function definitions. -func (tc *TemplateCollection) Funcs(m template.FuncMap) { +func (tc *Collection) Funcs(m template.FuncMap) { tc.group = tc.group.Funcs(m) } // MustParse parses template source "tmpl" and stores it in the -// TemplateCollection using the name of the go file that MustParse is called +// Collection using the name of the go file that MustParse is called // from. -func (tc *TemplateCollection) MustParse(tmpl string) *template.Template { +func (tc *Collection) MustParse(tmpl string) *template.Template { _, filename, _, ok := runtime.Caller(1) if !ok { panic("unable to determine template name") @@ -107,7 +110,7 @@ func (tc *TemplateCollection) MustParse(tmpl string) *template.Template { // Parse parses the source "tmpl" and stores it in the template collection // using name "name". -func (tc *TemplateCollection) Parse(name string, tmpl string) ( +func (tc *Collection) Parse(name string, tmpl string) ( *template.Template, error) { if tc.group.Lookup(name) != nil { return nil, fmt.Errorf("template %#v already registered", name) @@ -117,24 +120,24 @@ func (tc *TemplateCollection) Parse(name string, tmpl string) ( } // Lookup a template by name. Returns nil if not found. -func (tc *TemplateCollection) Lookup(name string) *template.Template { +func (tc *Collection) Lookup(name string) *template.Template { return tc.group.Lookup(name) } // Render writes the template out to the response writer (or any errors that // come up), with value as the template value. -func (tc *TemplateCollection) Render(w http.ResponseWriter, r *http.Request, +func (tc *Collection) Render(w http.ResponseWriter, r *http.Request, template string, values interface{}) { tmpl := tc.Lookup(template) if tmpl == nil { - HandleError(w, r, ErrInternalServerError.New( + wherr.Handle(w, r, wherr.InternalServerError.New( "no template %#v registered", template)) return } w.Header().Set("Content-Type", "text/html") err := tmpl.Execute(w, values) if err != nil { - HandleError(w, r, err) + wherr.Handle(w, r, err) return } }