Browse Source

Configurable routing and router middleware

This change exposes a new type, Match, which represents a matched route.
When present in the Goji environment (bound to the key MatchKey),
routing will be skipped and the bound Match will be dispatched to
instead.

In addition, the Goji router has been exposed as a middleware using the
Match mechanism above. This allows middleware inserted after the router
access to the Match object and any bound URLParams.

Fixes #76. See also #32.
Carl Jackson 11 years ago
parent
commit
707cf7e127
7 changed files with 237 additions and 43 deletions
  1. +2
    -8
      web/func_equal.go
  2. +42
    -0
      web/handler.go
  3. +66
    -0
      web/match.go
  4. +50
    -0
      web/match_test.go
  5. +25
    -0
      web/mux.go
  6. +17
    -35
      web/router.go
  7. +35
    -0
      web/router_middleware_test.go

+ 2
- 8
web/func_equal.go View File

@ -4,10 +4,6 @@ import (
"reflect" "reflect"
) )
func isFunc(fn interface{}) bool {
return reflect.ValueOf(fn).Kind() == reflect.Func
}
/* /*
This is more than a little sketchtacular. Go's rules for function pointer This is more than a little sketchtacular. Go's rules for function pointer
equality are pretty restrictive: nil function pointers always compare equal, and equality are pretty restrictive: nil function pointers always compare equal, and
@ -25,12 +21,10 @@ purposes.
If you're curious, you can read more about the representation of functions here: If you're curious, you can read more about the representation of functions here:
http://golang.org/s/go11func http://golang.org/s/go11func
We're in effect comparing the pointers of the indirect layer. We're in effect comparing the pointers of the indirect layer.
This function also works on non-function values.
*/ */
func funcEqual(a, b interface{}) bool { func funcEqual(a, b interface{}) bool {
if !isFunc(a) || !isFunc(b) {
panic("funcEqual: type error!")
}
av := reflect.ValueOf(&a).Elem() av := reflect.ValueOf(&a).Elem()
bv := reflect.ValueOf(&b).Elem() bv := reflect.ValueOf(&b).Elem()


+ 42
- 0
web/handler.go View File

@ -0,0 +1,42 @@
package web
import (
"log"
"net/http"
)
const unknownHandler = `Unknown handler type %T. See http://godoc.org/github.com/zenazn/goji/web#HandlerType for a list of acceptable types.`
type netHTTPHandlerWrap struct{ http.Handler }
type netHTTPHandlerFuncWrap struct {
fn func(http.ResponseWriter, *http.Request)
}
type handlerFuncWrap struct {
fn func(C, http.ResponseWriter, *http.Request)
}
func (h netHTTPHandlerWrap) ServeHTTPC(c C, w http.ResponseWriter, r *http.Request) {
h.Handler.ServeHTTP(w, r)
}
func (h netHTTPHandlerFuncWrap) ServeHTTPC(c C, w http.ResponseWriter, r *http.Request) {
h.fn(w, r)
}
func (h handlerFuncWrap) ServeHTTPC(c C, w http.ResponseWriter, r *http.Request) {
h.fn(c, w, r)
}
func parseHandler(h HandlerType) Handler {
switch f := h.(type) {
case func(c C, w http.ResponseWriter, r *http.Request):
return handlerFuncWrap{f}
case func(w http.ResponseWriter, r *http.Request):
return netHTTPHandlerFuncWrap{f}
case Handler:
return f
case http.Handler:
return netHTTPHandlerWrap{f}
default:
log.Fatalf(unknownHandler, h)
panic("log.Fatalf does not return")
}
}

+ 66
- 0
web/match.go View File

@ -0,0 +1,66 @@
package web
// The key used to store route Matches in the Goji environment. If this key is
// present in the environment and contains a value of type Match, routing will
// not be performed, and the Match's Handler will be used instead.
const MatchKey = "goji.web.Match"
// Match is the type of routing matches. It is inserted into C.Env under
// MatchKey when the Mux.Router middleware is invoked. If MatchKey is present at
// route dispatch time, the Handler of the corresponding Match will be called
// instead of performing routing as usual.
//
// By computing a Match and inserting it into the Goji environment as part of a
// middleware stack (see Mux.Router, for instance), it is possible to customize
// Goji's routing behavior or replace it entirely.
type Match struct {
// Pattern is the Pattern that matched during routing. Will be nil if no
// route matched (Handler will be set to the Mux's NotFound handler)
Pattern Pattern
// The Handler corresponding to the matched pattern.
Handler Handler
}
// GetMatch returns the Match stored in the Goji environment, or an empty Match
// if none exists (valid Matches always have a Handler property).
func GetMatch(c C) Match {
if c.Env == nil {
return Match{}
}
mi, ok := c.Env[MatchKey]
if !ok {
return Match{}
}
if m, ok := mi.(Match); ok {
return m
}
return Match{}
}
// RawPattern returns the PatternType that was originally passed to ParsePattern
// or any of the HTTP method functions (Get, Post, etc.).
func (m Match) RawPattern() PatternType {
switch v := m.Pattern.(type) {
case regexpPattern:
return v.re
case stringPattern:
return v.raw
default:
return v
}
}
// RawHandler returns the HandlerType that was originally passed to the HTTP
// method functions (Get, Post, etc.).
func (m Match) RawHandler() HandlerType {
switch v := m.Handler.(type) {
case netHTTPHandlerWrap:
return v.Handler
case handlerFuncWrap:
return v.fn
case netHTTPHandlerFuncWrap:
return v.fn
default:
return v
}
}

+ 50
- 0
web/match_test.go View File

@ -0,0 +1,50 @@
package web
import (
"net/http"
"regexp"
"testing"
)
var rawPatterns = []PatternType{
"/hello/:name",
regexp.MustCompile("^/hello/(?P<name>[^/]+)$"),
testPattern{},
}
func TestRawPattern(t *testing.T) {
t.Parallel()
for _, p := range rawPatterns {
m := Match{Pattern: ParsePattern(p)}
if rp := m.RawPattern(); rp != p {
t.Errorf("got %#v, expected %#v", rp, p)
}
}
}
type httpHandlerOnly struct{}
func (httpHandlerOnly) ServeHTTP(w http.ResponseWriter, r *http.Request) {}
type handlerOnly struct{}
func (handlerOnly) ServeHTTPC(c C, w http.ResponseWriter, r *http.Request) {}
var rawHandlers = []HandlerType{
func(w http.ResponseWriter, r *http.Request) {},
func(c C, w http.ResponseWriter, r *http.Request) {},
httpHandlerOnly{},
handlerOnly{},
}
func TestRawHandler(t *testing.T) {
t.Parallel()
for _, h := range rawHandlers {
m := Match{Handler: parseHandler(h)}
if rh := m.RawHandler(); !funcEqual(rh, h) {
t.Errorf("got %#v, expected %#v", rh, h)
}
}
}

+ 25
- 0
web/mux.go View File

@ -86,6 +86,31 @@ func (m *Mux) Abandon(middleware MiddlewareType) error {
// Router functions // Router functions
type routerMiddleware struct {
m *Mux
c *C
h http.Handler
}
func (rm routerMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if rm.c.Env == nil {
rm.c.Env = make(map[interface{}]interface{}, 1)
}
rm.c.Env[MatchKey] = rm.m.rt.getMatch(rm.c, w, r)
rm.h.ServeHTTP(w, r)
}
// Router is a middleware that performs routing and stores the resulting Match
// in Goji's environment. If a routing Match is present at the end of the
// middleware stack, that Match is used instead of re-routing.
//
// This middleware is especially useful to create post-routing middleware, e.g.
// a request logger which prints which pattern or handler was selected, or an
// authentication middleware which only applies to certain routes.
func (m *Mux) Router(c *C, h http.Handler) http.Handler {
return routerMiddleware{m, c, h}
}
/* /*
Dispatch to the given handler when the pattern matches, regardless of HTTP Dispatch to the given handler when the pattern matches, regardless of HTTP
method. method.


+ 17
- 35
web/router.go View File

@ -1,7 +1,6 @@
package web package web
import ( import (
"log"
"net/http" "net/http"
"sort" "sort"
"strings" "strings"
@ -30,7 +29,7 @@ const (
// The key used to communicate to the NotFound handler what methods would have // The key used to communicate to the NotFound handler what methods would have
// been allowed if they'd been provided. // been allowed if they'd been provided.
const ValidMethodsKey = "goji.web.validMethods"
const ValidMethodsKey = "goji.web.ValidMethods"
var validMethodsMap = map[string]method{ var validMethodsMap = map[string]method{
"CONNECT": mCONNECT, "CONNECT": mCONNECT,
@ -58,32 +57,6 @@ type router struct {
machine *routeMachine machine *routeMachine
} }
type netHTTPWrap struct {
http.Handler
}
func (h netHTTPWrap) ServeHTTPC(c C, w http.ResponseWriter, r *http.Request) {
h.Handler.ServeHTTP(w, r)
}
const unknownHandler = `Unknown handler type %T. See http://godoc.org/github.com/zenazn/goji/web#HandlerType for a list of acceptable types.`
func parseHandler(h interface{}) Handler {
switch f := h.(type) {
case Handler:
return f
case http.Handler:
return netHTTPWrap{f}
case func(c C, w http.ResponseWriter, r *http.Request):
return HandlerFunc(f)
case func(w http.ResponseWriter, r *http.Request):
return netHTTPWrap{http.HandlerFunc(f)}
default:
log.Fatalf(unknownHandler, h)
panic("log.Fatalf does not return")
}
}
func httpMethod(mname string) method { func httpMethod(mname string) method {
if method, ok := validMethodsMap[mname]; ok { if method, ok := validMethodsMap[mname]; ok {
return method return method
@ -102,7 +75,7 @@ func (rt *router) compile() *routeMachine {
return &sm return &sm
} }
func (rt *router) route(c *C, w http.ResponseWriter, r *http.Request) {
func (rt *router) getMatch(c *C, w http.ResponseWriter, r *http.Request) Match {
rm := rt.getMachine() rm := rt.getMachine()
if rm == nil { if rm == nil {
rm = rt.compile() rm = rt.compile()
@ -110,13 +83,14 @@ func (rt *router) route(c *C, w http.ResponseWriter, r *http.Request) {
methods, route := rm.route(c, w, r) methods, route := rm.route(c, w, r)
if route != nil { if route != nil {
route.handler.ServeHTTPC(*c, w, r)
return
return Match{
Pattern: route.pattern,
Handler: route.handler,
}
} }
if methods == 0 { if methods == 0 {
rt.notFound.ServeHTTPC(*c, w, r)
return
return Match{Handler: rt.notFound}
} }
var methodsList = make([]string, 0) var methodsList = make([]string, 0)
@ -134,10 +108,18 @@ func (rt *router) route(c *C, w http.ResponseWriter, r *http.Request) {
} else { } else {
c.Env[ValidMethodsKey] = methodsList c.Env[ValidMethodsKey] = methodsList
} }
rt.notFound.ServeHTTPC(*c, w, r)
return Match{Handler: rt.notFound}
}
func (rt *router) route(c *C, w http.ResponseWriter, r *http.Request) {
match := GetMatch(*c)
if match.Handler == nil {
match = rt.getMatch(c, w, r)
}
match.Handler.ServeHTTPC(*c, w, r)
} }
func (rt *router) handleUntyped(p interface{}, m method, h interface{}) {
func (rt *router) handleUntyped(p PatternType, m method, h HandlerType) {
rt.handle(ParsePattern(p), m, parseHandler(h)) rt.handle(ParsePattern(p), m, parseHandler(h))
} }


+ 35
- 0
web/router_middleware_test.go View File

@ -0,0 +1,35 @@
package web
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestRouterMiddleware(t *testing.T) {
t.Parallel()
m := New()
ch := make(chan string, 1)
m.Get("/a", chHandler(ch, "a"))
m.Get("/b", chHandler(ch, "b"))
m.Use(m.Router)
m.Use(func(c *C, h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
m := GetMatch(*c)
if rp := m.RawPattern(); rp != "/a" {
t.Fatalf("RawPattern was not /a: %v", rp)
}
r.URL.Path = "/b"
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
})
r, _ := http.NewRequest("GET", "/a", nil)
w := httptest.NewRecorder()
m.ServeHTTP(w, r)
if v := <-ch; v != "a" {
t.Error("Routing was not frozen! %s", v)
}
}

Loading…
Cancel
Save