Skip to content

Commit 680e71e

Browse files
author
Olivier Poitrey
committed
Add route grouping capability
Integrate and adapt julienschmidt/httprouter#89
1 parent 960c771 commit 680e71e

File tree

5 files changed

+229
-0
lines changed

5 files changed

+229
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ The muxer is optimized for high performance and a small memory footprint. It sca
1818

1919
**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.
2020

21+
**RouteGroups:** A way to create [groups of routes](http://godoc.org/github.com/rs/xhandler/xmux#Mux.NewGroup) without incurring any per-request overhead.
22+
2123
**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.
2224

2325
**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.

group.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package xmux
2+
3+
import "github.com/rs/xhandler"
4+
5+
// Group makes it simple to configure a group of routes with the
6+
// same prefix. Use mux.NewGroup("/prefix") to create a group.
7+
type Group struct {
8+
m *Mux
9+
p string
10+
}
11+
12+
func newRouteGroup(mux *Mux, path string) *Group {
13+
if path[0] != '/' {
14+
panic("path must begin with '/' in path '" + path + "'")
15+
}
16+
17+
//Strip traling / (if present) as all added sub paths must start with a /
18+
if path[len(path)-1] == '/' {
19+
path = path[:len(path)-1]
20+
}
21+
return &Group{m: mux, p: path}
22+
}
23+
24+
// NewGroup creates a new sub routes group with the provided path prefix.
25+
// All routes added to the returned group will have the path prepended.
26+
func (g *Group) NewGroup(path string) *Group {
27+
return newRouteGroup(g.m, g.subPath(path))
28+
}
29+
30+
// GET is a shortcut for g.Handle("GET", path, handler)
31+
func (g *Group) GET(path string, handler xhandler.HandlerC) {
32+
g.Handle("GET", path, handler)
33+
}
34+
35+
// HEAD is a shortcut for g.Handle("HEAD", path, handler)
36+
func (g *Group) HEAD(path string, handler xhandler.HandlerC) {
37+
g.Handle("HEAD", path, handler)
38+
}
39+
40+
// OPTIONS is a shortcut for g.Handle("OPTIONS", path, handler)
41+
func (g *Group) OPTIONS(path string, handler xhandler.HandlerC) {
42+
g.Handle("OPTIONS", path, handler)
43+
}
44+
45+
// POST is a shortcut for g.Handle("POST", path, handler)
46+
func (g *Group) POST(path string, handler xhandler.HandlerC) {
47+
g.Handle("POST", path, handler)
48+
}
49+
50+
// PUT is a shortcut for g.Handle("PUT", path, handler)
51+
func (g *Group) PUT(path string, handler xhandler.HandlerC) {
52+
g.Handle("PUT", path, handler)
53+
}
54+
55+
// PATCH is a shortcut for g.Handle("PATCH", path, handler)
56+
func (g *Group) PATCH(path string, handler xhandler.HandlerC) {
57+
g.Handle("PATCH", path, handler)
58+
}
59+
60+
// DELETE is a shortcut for g.Handle("DELETE", path, handler)
61+
func (g *Group) DELETE(path string, handler xhandler.HandlerC) {
62+
g.Handle("DELETE", path, handler)
63+
}
64+
65+
// Handle registers a new request handle with the given path and method.
66+
//
67+
// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut
68+
// functions can be used.
69+
//
70+
// This function is intended for bulk loading and to allow the usage of less
71+
// frequently used, non-standardized or custom methods (e.g. for internal
72+
// communication with a proxy).
73+
func (g *Group) Handle(method, path string, handler xhandler.HandlerC) {
74+
g.m.Handle(method, g.subPath(path), handler)
75+
}
76+
77+
func (g *Group) subPath(path string) string {
78+
if path[0] != '/' {
79+
panic("path must start with a '/'")
80+
}
81+
return g.p + path
82+
}

group_example_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package xmux_test
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"net/http"
7+
8+
"github.com/rs/xhandler"
9+
"github.com/rs/xhandler/xmux"
10+
"golang.org/x/net/context"
11+
)
12+
13+
func ExampleMux_NewGroup() {
14+
mux := xmux.New()
15+
16+
api := mux.NewGroup("/api")
17+
18+
api.GET("/users/:name", xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
19+
fmt.Fprintf(w, "GET /api/users/%s", xmux.Params(ctx).Get("name"))
20+
}))
21+
22+
api.POST("/users/:name", xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
23+
fmt.Fprintf(w, "POST /api/users/%s", xmux.Params(ctx).Get("name"))
24+
}))
25+
26+
if err := http.ListenAndServe(":8080", xhandler.New(context.Background(), mux)); err != nil {
27+
log.Fatal(err)
28+
}
29+
}

group_test.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package xmux
2+
3+
import (
4+
"net/http"
5+
"testing"
6+
7+
"github.com/rs/xhandler"
8+
"github.com/stretchr/testify/assert"
9+
"golang.org/x/net/context"
10+
)
11+
12+
func TestRouteGroupOfARouteGroup(t *testing.T) {
13+
var get bool
14+
mux := New()
15+
foo := mux.NewGroup("/foo") // creates /foo group
16+
bar := foo.NewGroup("/bar")
17+
18+
bar.GET("/GET", xhandler.HandlerFuncC(func(_ context.Context, _ http.ResponseWriter, _ *http.Request) {
19+
get = true
20+
}))
21+
22+
w := new(mockResponseWriter)
23+
r, _ := http.NewRequest("GET", "/foo/bar/GET", nil)
24+
mux.ServeHTTPC(context.Background(), w, r)
25+
assert.True(t, get, "routing GET /foo/bar/GET failed")
26+
}
27+
28+
func TestRouteNewGroupStripTrailingSlash(t *testing.T) {
29+
var get bool
30+
mux := New()
31+
foo := mux.NewGroup("/foo/")
32+
33+
foo.GET("/GET", xhandler.HandlerFuncC(func(_ context.Context, _ http.ResponseWriter, _ *http.Request) {
34+
get = true
35+
}))
36+
37+
w := new(mockResponseWriter)
38+
r, _ := http.NewRequest("GET", "/foo/GET", nil)
39+
mux.ServeHTTPC(context.Background(), w, r)
40+
assert.True(t, get, "routing GET /foo/GET failed")
41+
}
42+
43+
func TestRouteNewGroupError(t *testing.T) {
44+
mux := New()
45+
assert.Panics(t, func() {
46+
mux.NewGroup("foo")
47+
})
48+
assert.Panics(t, func() {
49+
mux.NewGroup("/foo").NewGroup("bar")
50+
})
51+
}
52+
53+
func TestRouteGroupAPI(t *testing.T) {
54+
var get, head, options, post, put, patch, delete bool
55+
56+
mux := New()
57+
group := mux.NewGroup("/foo") // creates /foo group
58+
59+
group.GET("/GET", xhandler.HandlerFuncC(func(_ context.Context, _ http.ResponseWriter, _ *http.Request) {
60+
get = true
61+
}))
62+
group.HEAD("/GET", xhandler.HandlerFuncC(func(_ context.Context, _ http.ResponseWriter, _ *http.Request) {
63+
head = true
64+
}))
65+
group.OPTIONS("/GET", xhandler.HandlerFuncC(func(_ context.Context, _ http.ResponseWriter, _ *http.Request) {
66+
options = true
67+
}))
68+
group.POST("/POST", xhandler.HandlerFuncC(func(_ context.Context, _ http.ResponseWriter, _ *http.Request) {
69+
post = true
70+
}))
71+
group.PUT("/PUT", xhandler.HandlerFuncC(func(_ context.Context, _ http.ResponseWriter, _ *http.Request) {
72+
put = true
73+
}))
74+
group.PATCH("/PATCH", xhandler.HandlerFuncC(func(_ context.Context, _ http.ResponseWriter, _ *http.Request) {
75+
patch = true
76+
}))
77+
group.DELETE("/DELETE", xhandler.HandlerFuncC(func(_ context.Context, _ http.ResponseWriter, _ *http.Request) {
78+
delete = true
79+
}))
80+
81+
w := new(mockResponseWriter)
82+
83+
r, _ := http.NewRequest("GET", "/foo/GET", nil)
84+
mux.ServeHTTPC(context.Background(), w, r)
85+
assert.True(t, get, "routing /foo/GET failed")
86+
87+
r, _ = http.NewRequest("HEAD", "/foo/GET", nil)
88+
mux.ServeHTTPC(context.Background(), w, r)
89+
assert.True(t, head, "routing /foo/GET failed")
90+
91+
r, _ = http.NewRequest("OPTIONS", "/foo/GET", nil)
92+
mux.ServeHTTPC(context.Background(), w, r)
93+
assert.True(t, options, "routing /foo/GET failed")
94+
95+
r, _ = http.NewRequest("POST", "/foo/POST", nil)
96+
mux.ServeHTTPC(context.Background(), w, r)
97+
assert.True(t, post, "routing /foo/POST failed")
98+
99+
r, _ = http.NewRequest("PUT", "/foo/PUT", nil)
100+
mux.ServeHTTPC(context.Background(), w, r)
101+
assert.True(t, put, "routing /foo/PUT failed")
102+
103+
r, _ = http.NewRequest("PATCH", "/foo/PATCH", nil)
104+
mux.ServeHTTPC(context.Background(), w, r)
105+
assert.True(t, patch, "routing /foo/PATCH failed")
106+
107+
r, _ = http.NewRequest("DELETE", "/foo/DELETE", nil)
108+
mux.ServeHTTPC(context.Background(), w, r)
109+
assert.True(t, delete, "routing /foo/DELETE failed")
110+
}

mux.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,12 @@ func New() *Mux {
176176
}
177177
}
178178

179+
// NewGroup creates a new routes group with the provided path prefix.
180+
// All routes added to the returned group will have the path prepended.
181+
func (mux *Mux) NewGroup(path string) *Group {
182+
return newRouteGroup(mux, path)
183+
}
184+
179185
// GET is a shortcut for mux.Handle("GET", path, handler)
180186
func (mux *Mux) GET(path string, handler xhandler.HandlerC) {
181187
mux.Handle("GET", path, handler)

0 commit comments

Comments
 (0)