Skip to content

Commit

Permalink
Add route grouping capability
Browse files Browse the repository at this point in the history
Integrate and adapt julienschmidt/httprouter#89
  • Loading branch information
Olivier Poitrey committed Dec 13, 2015
1 parent 161ec6b commit 98bd70d
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 0 deletions.
2 changes: 2 additions & 0 deletions xmux/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
82 changes: 82 additions & 0 deletions xmux/group.go
Original file line number Diff line number Diff line change
@@ -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
}
29 changes: 29 additions & 0 deletions xmux/group_example_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
110 changes: 110 additions & 0 deletions xmux/group_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
6 changes: 6 additions & 0 deletions xmux/mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 98bd70d

Please sign in to comment.