diff --git a/xmux/README.md b/xmux/README.md index b5e0f34..2e04cd6 100644 --- a/xmux/README.md +++ b/xmux/README.md @@ -18,6 +18,8 @@ The muxer is optimized for high performance and a small memory footprint. It sca **Parameters in your routing pattern:** Stop parsing the requested URL path, just give the path segment a name and the router delivers the dynamic value to you. Because of the design of the router, path parameters are very cheap. +**RouteGroups:** A way to create [groups of routes](http://godoc.org/github.com/rs/xhandler/xmux#Mux.NewGroup) without incurring any per-request overhead. + **Zero Garbage:** The matching and dispatching process generates zero bytes of garbage. In fact, the only heap allocations that are made, is by building the slice of the key-value pairs for path parameters and the `net/context` instance to store them in the context. If the request path contains no parameters, not a single heap allocation is necessary. **No more server crashes:** You can set a [Panic handler](http://godoc.org/github.com/rs/xhandler/xmux#Mux.PanicHandler) to deal with panics occurring during handling a HTTP request. The router then recovers and lets the `PanicHandler` log what happened and deliver a nice error page. diff --git a/xmux/group.go b/xmux/group.go new file mode 100644 index 0000000..3757467 --- /dev/null +++ b/xmux/group.go @@ -0,0 +1,82 @@ +package xmux + +import "github.com/rs/xhandler" + +// Group makes it simple to configure a group of routes with the +// same prefix. +type Group struct { + m *Mux + p string +} + +func newRouteGroup(mux *Mux, path string) *Group { + if path[0] != '/' { + panic("path must begin with '/' in path '" + path + "'") + } + + //Strip traling / (if present) as all added sub paths must start with a / + if path[len(path)-1] == '/' { + path = path[:len(path)-1] + } + return &Group{m: mux, p: path} +} + +// NewGroup creates a new routes group with the provided path prefix. +// All routes added to the returned group will have the path prepended. +func (g *Group) NewGroup(path string) *Group { + return newRouteGroup(g.m, g.subPath(path)) +} + +// GET is a shortcut for g.Handle("GET", path, handler) +func (g *Group) GET(path string, handler xhandler.HandlerC) { + g.Handle("GET", path, handler) +} + +// HEAD is a shortcut for g.Handle("HEAD", path, handler) +func (g *Group) HEAD(path string, handler xhandler.HandlerC) { + g.Handle("HEAD", path, handler) +} + +// OPTIONS is a shortcut for g.Handle("OPTIONS", path, handler) +func (g *Group) OPTIONS(path string, handler xhandler.HandlerC) { + g.Handle("OPTIONS", path, handler) +} + +// POST is a shortcut for g.Handle("POST", path, handler) +func (g *Group) POST(path string, handler xhandler.HandlerC) { + g.Handle("POST", path, handler) +} + +// PUT is a shortcut for g.Handle("PUT", path, handler) +func (g *Group) PUT(path string, handler xhandler.HandlerC) { + g.Handle("PUT", path, handler) +} + +// PATCH is a shortcut for g.Handle("PATCH", path, handler) +func (g *Group) PATCH(path string, handler xhandler.HandlerC) { + g.Handle("PATCH", path, handler) +} + +// DELETE is a shortcut for g.Handle("DELETE", path, handler) +func (g *Group) DELETE(path string, handler xhandler.HandlerC) { + g.Handle("DELETE", path, handler) +} + +// Handle registers a new request handle with the given path and method. +// +// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut +// functions can be used. +// +// This function is intended for bulk loading and to allow the usage of less +// frequently used, non-standardized or custom methods (e.g. for internal +// communication with a proxy). +func (g *Group) Handle(method, path string, handler xhandler.HandlerC) { + g.m.Handle(method, g.subPath(path), handler) +} + +func (g *Group) subPath(path string) string { + if path[0] != '/' { + panic("path must start with a '/'") + } + return g.p + path +} diff --git a/xmux/group_example_test.go b/xmux/group_example_test.go new file mode 100644 index 0000000..9cb8039 --- /dev/null +++ b/xmux/group_example_test.go @@ -0,0 +1,29 @@ +package xmux_test + +import ( + "fmt" + "log" + "net/http" + + "github.com/rs/xhandler" + "github.com/rs/xhandler/xmux" + "golang.org/x/net/context" +) + +func ExampleMux_NewGroup() { + mux := xmux.New() + + api := mux.NewGroup("/api") + + api.GET("/users/:name", xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "GET /api/users/%s", xmux.Params(ctx).Get("name")) + })) + + api.POST("/users/:name", xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "POST /api/users/%s", xmux.Params(ctx).Get("name")) + })) + + if err := http.ListenAndServe(":8080", xhandler.New(context.Background(), mux)); err != nil { + log.Fatal(err) + } +} diff --git a/xmux/group_test.go b/xmux/group_test.go new file mode 100644 index 0000000..2a0d043 --- /dev/null +++ b/xmux/group_test.go @@ -0,0 +1,110 @@ +package xmux + +import ( + "net/http" + "testing" + + "github.com/rs/xhandler" + "github.com/stretchr/testify/assert" + "golang.org/x/net/context" +) + +func TestRouteGroupOfARouteGroup(t *testing.T) { + var get bool + mux := New() + foo := mux.NewGroup("/foo") // creates /foo group + bar := foo.NewGroup("/bar") + + bar.GET("/GET", xhandler.HandlerFuncC(func(_ context.Context, _ http.ResponseWriter, _ *http.Request) { + get = true + })) + + w := new(mockResponseWriter) + r, _ := http.NewRequest("GET", "/foo/bar/GET", nil) + mux.ServeHTTPC(context.Background(), w, r) + assert.True(t, get, "routing GET /foo/bar/GET failed") +} + +func TestRouteNewGroupStripTrailingSlash(t *testing.T) { + var get bool + mux := New() + foo := mux.NewGroup("/foo/") + + foo.GET("/GET", xhandler.HandlerFuncC(func(_ context.Context, _ http.ResponseWriter, _ *http.Request) { + get = true + })) + + w := new(mockResponseWriter) + r, _ := http.NewRequest("GET", "/foo/GET", nil) + mux.ServeHTTPC(context.Background(), w, r) + assert.True(t, get, "routing GET /foo/GET failed") +} + +func TestRouteNewGroupError(t *testing.T) { + mux := New() + assert.Panics(t, func() { + mux.NewGroup("foo") + }) + assert.Panics(t, func() { + mux.NewGroup("/foo").NewGroup("bar") + }) +} + +func TestRouteGroupAPI(t *testing.T) { + var get, head, options, post, put, patch, delete bool + + mux := New() + group := mux.NewGroup("/foo") // creates /foo group + + group.GET("/GET", xhandler.HandlerFuncC(func(_ context.Context, _ http.ResponseWriter, _ *http.Request) { + get = true + })) + group.HEAD("/GET", xhandler.HandlerFuncC(func(_ context.Context, _ http.ResponseWriter, _ *http.Request) { + head = true + })) + group.OPTIONS("/GET", xhandler.HandlerFuncC(func(_ context.Context, _ http.ResponseWriter, _ *http.Request) { + options = true + })) + group.POST("/POST", xhandler.HandlerFuncC(func(_ context.Context, _ http.ResponseWriter, _ *http.Request) { + post = true + })) + group.PUT("/PUT", xhandler.HandlerFuncC(func(_ context.Context, _ http.ResponseWriter, _ *http.Request) { + put = true + })) + group.PATCH("/PATCH", xhandler.HandlerFuncC(func(_ context.Context, _ http.ResponseWriter, _ *http.Request) { + patch = true + })) + group.DELETE("/DELETE", xhandler.HandlerFuncC(func(_ context.Context, _ http.ResponseWriter, _ *http.Request) { + delete = true + })) + + w := new(mockResponseWriter) + + r, _ := http.NewRequest("GET", "/foo/GET", nil) + mux.ServeHTTPC(context.Background(), w, r) + assert.True(t, get, "routing /foo/GET failed") + + r, _ = http.NewRequest("HEAD", "/foo/GET", nil) + mux.ServeHTTPC(context.Background(), w, r) + assert.True(t, head, "routing /foo/GET failed") + + r, _ = http.NewRequest("OPTIONS", "/foo/GET", nil) + mux.ServeHTTPC(context.Background(), w, r) + assert.True(t, options, "routing /foo/GET failed") + + r, _ = http.NewRequest("POST", "/foo/POST", nil) + mux.ServeHTTPC(context.Background(), w, r) + assert.True(t, post, "routing /foo/POST failed") + + r, _ = http.NewRequest("PUT", "/foo/PUT", nil) + mux.ServeHTTPC(context.Background(), w, r) + assert.True(t, put, "routing /foo/PUT failed") + + r, _ = http.NewRequest("PATCH", "/foo/PATCH", nil) + mux.ServeHTTPC(context.Background(), w, r) + assert.True(t, patch, "routing /foo/PATCH failed") + + r, _ = http.NewRequest("DELETE", "/foo/DELETE", nil) + mux.ServeHTTPC(context.Background(), w, r) + assert.True(t, delete, "routing /foo/DELETE failed") +} diff --git a/xmux/mux.go b/xmux/mux.go index 8f3ff52..4986363 100644 --- a/xmux/mux.go +++ b/xmux/mux.go @@ -176,6 +176,12 @@ func New() *Mux { } } +// NewGroup creates a new routes group with the provided path prefix. +// All routes added to the returned group will have the path prepended. +func (mux *Mux) NewGroup(path string) *Group { + return newRouteGroup(mux, path) +} + // GET is a shortcut for mux.Handle("GET", path, handler) func (mux *Mux) GET(path string, handler xhandler.HandlerC) { mux.Handle("GET", path, handler)