Skip to content

Commit ebe1733

Browse files
Add support for wildcards on the routes.
1 parent 2e67b64 commit ebe1733

File tree

4 files changed

+270
-13
lines changed

4 files changed

+270
-13
lines changed

README.md

+66
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,72 @@ func (h *Hello) Get(c *yarf.Context) error {
153153
```
154154

155155

156+
### Route wildcards
157+
158+
When some extra freedom is needed on your routes, you can use a `*` as part of your routes to match anything where the wildcard is present.
159+
160+
The route:
161+
162+
```
163+
/something/*/here
164+
```
165+
166+
Will match the routes
167+
168+
```
169+
/something/is/here
170+
/something/happen/here
171+
/something/isnt/here
172+
/something/was/here
173+
```
174+
175+
And so on...
176+
177+
You can also combine this with parameters inside the routes for extra complexity.
178+
179+
180+
### Catch-All wildcard
181+
182+
When using the `*` at the end of any route, the router will match everything from the wildcard and forward.
183+
184+
The route:
185+
186+
```
187+
/match/from/here/*
188+
```
189+
190+
Will match:
191+
192+
```
193+
/match/from/here
194+
/match/from/here/please
195+
/match/from/here/and/forward
196+
/match/from/here/and/forward/for/ever/and/ever
197+
```
198+
199+
And so on...
200+
201+
202+
#### Note about the wildcard
203+
204+
The `*` can only be used by itself and it doesn't works for single character matching like in regex.
205+
206+
So the route:
207+
208+
```
209+
/match/some*
210+
```
211+
212+
Will **NOT** match:
213+
214+
```
215+
/match/some
216+
/match/something
217+
/match/someone
218+
/match/some/please
219+
```
220+
221+
156222
### Context
157223

158224
The Context object is passed as a parameter to all Resource methods and contains all the information related to the ongoing request.

router.go

+18-4
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,11 @@ func (r *route) Match(url string, c *Context) bool {
5151
requestParts := prepareURL(url)
5252

5353
// YARF router only accepts exact route matches, so check for part count.
54-
if len(r.routeParts) != len(requestParts) {
55-
return false
54+
// Unless it's a catch-all route
55+
if len(r.routeParts) == 0 || (len(r.routeParts) > 0 && r.routeParts[len(r.routeParts)-1] != "*") {
56+
if len(r.routeParts) != len(requestParts) {
57+
return false
58+
}
5659
}
5760

5861
// check that requestParts matches routeParts
@@ -257,12 +260,23 @@ func removeEmpty(parts []string) []string {
257260
// matches returns true if requestParts matches routeParts up through len(routeParts)
258261
// ignoring params in routeParts
259262
func matches(routeParts, requestParts []string) bool {
260-
if len(requestParts) < len(routeParts) {
263+
routeCount := len(routeParts)
264+
265+
// Check for catch-all wildcard
266+
if len(routeParts) > 0 && routeParts[len(routeParts)-1] == "*" {
267+
routeCount--
268+
}
269+
270+
if len(requestParts) < routeCount {
261271
return false
262272
}
263273

264-
// Check for part matching, ignoring params
274+
// Check for part matching, ignoring params and * wildcards
265275
for i, p := range routeParts {
276+
// Skip wildcard
277+
if p == "*" {
278+
continue
279+
}
266280
if p != requestParts[i] && p[0] != ':' {
267281
return false
268282
}

router_test.go

+177
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,28 @@ func TestRouterRootMatch(t *testing.T) {
3333
}
3434
}
3535

36+
func TestRouterRootCatchAll(t *testing.T) {
37+
// Create empty handler
38+
h := new(Handler)
39+
40+
// Create empty context
41+
c := new(Context)
42+
c.Params = url.Values{}
43+
44+
// Create route
45+
r := Route("/*", h)
46+
47+
// Matching routes
48+
rs := []string{"/", "", "/something", "/*", "/something/else/more"}
49+
50+
// Check
51+
for _, s := range rs {
52+
if !r.Match(s, c) {
53+
t.Errorf("'%s' should match against '*'", s)
54+
}
55+
}
56+
}
57+
3658
func TestRouterRootUnmatch(t *testing.T) {
3759
// Create empty handler
3860
h := new(Handler)
@@ -77,6 +99,28 @@ func TestRouter1LevelMatch(t *testing.T) {
7799
}
78100
}
79101

102+
func TestRouter1LevelCatchAll(t *testing.T) {
103+
// Create empty handler
104+
h := new(Handler)
105+
106+
// Create empty context
107+
c := new(Context)
108+
c.Params = url.Values{}
109+
110+
// Create route
111+
r := Route("/level/*", h)
112+
113+
// Matching routes
114+
rs := []string{"/level/something", "level/something", "/level/*", "/level/something/else/and/more/because/this/matches/all"}
115+
116+
// Check
117+
for _, s := range rs {
118+
if !r.Match(s, c) {
119+
t.Errorf("'%s' should match against '/level/*'", s)
120+
}
121+
}
122+
}
123+
80124
func TestRouter1LevelUnmatch(t *testing.T) {
81125
// Create empty handler
82126
h := new(Handler)
@@ -121,6 +165,50 @@ func TestRouterMultiLevelMatch(t *testing.T) {
121165
}
122166
}
123167

168+
func TestRouterMultiLevelWildcard(t *testing.T) {
169+
// Create empty handler
170+
h := new(Handler)
171+
172+
// Create empty context
173+
c := new(Context)
174+
c.Params = url.Values{}
175+
176+
// Create route
177+
r := Route("/a/b/*/d", h)
178+
179+
// Matching routes
180+
rs := []string{"/a/b/c/d", "a/b/c/d", "/a/b/*/d", "a/b/something/d"}
181+
182+
// Check
183+
for _, s := range rs {
184+
if !r.Match(s, c) {
185+
t.Errorf("'%s' should match against '/a/b/*/d", s)
186+
}
187+
}
188+
}
189+
190+
func TestRouterMultiLevelCatchAll(t *testing.T) {
191+
// Create empty handler
192+
h := new(Handler)
193+
194+
// Create empty context
195+
c := new(Context)
196+
c.Params = url.Values{}
197+
198+
// Create route
199+
r := Route("/a/b/*", h)
200+
201+
// Matching routes
202+
rs := []string{"/a/b/c", "a/b/c", "/a/b/c/d/e", "a/b/c/d"}
203+
204+
// Check
205+
for _, s := range rs {
206+
if !r.Match(s, c) {
207+
t.Errorf("'%s' should match against '/a/b/*", s)
208+
}
209+
}
210+
}
211+
124212
func TestRouterMultiLevelUnmatch(t *testing.T) {
125213
// Create empty handler
126214
h := new(Handler)
@@ -165,6 +253,28 @@ func TestRouter1LevelParamMatch(t *testing.T) {
165253
}
166254
}
167255

256+
func TestRouter1LevelParamCatchAll(t *testing.T) {
257+
// Create empty handler
258+
h := new(Handler)
259+
260+
// Create empty context
261+
c := new(Context)
262+
c.Params = url.Values{}
263+
264+
// Create route
265+
r := Route("/:param/*", h)
266+
267+
// Matching routes
268+
rs := []string{"/a", "a", "/cafewafewa", "/:paramStyle", "/trailer/", "/something/more/to/catch"}
269+
270+
// Check
271+
for _, s := range rs {
272+
if !r.Match(s, c) {
273+
t.Errorf("'%s' should match against '/:param/*'", s)
274+
}
275+
}
276+
}
277+
168278
func TestRouter1LevelParamUnmatch(t *testing.T) {
169279
// Create empty handler
170280
h := new(Handler)
@@ -209,6 +319,50 @@ func TestRouterMultiLevelParamMatch(t *testing.T) {
209319
}
210320
}
211321

322+
func TestRouterMultiLevelParamWildcard(t *testing.T) {
323+
// Create empty handler
324+
h := new(Handler)
325+
326+
// Create empty context
327+
c := new(Context)
328+
c.Params = url.Values{}
329+
330+
// Create route
331+
r := Route("/a/*/:param", h)
332+
333+
// Matching routes
334+
rs := []string{"/a/b/c", "a/b/c", "/a/b/c/", "a/b/c/", "/a/b/:c", "/a/b/:param"}
335+
336+
// Check
337+
for _, s := range rs {
338+
if !r.Match(s, c) {
339+
t.Errorf("'%s' should match against '/a/*/:param'", s)
340+
}
341+
}
342+
}
343+
344+
func TestRouterMultiLevelParamCatchAll(t *testing.T) {
345+
// Create empty handler
346+
h := new(Handler)
347+
348+
// Create empty context
349+
c := new(Context)
350+
c.Params = url.Values{}
351+
352+
// Create route
353+
r := Route("/a/b/:param/*", h)
354+
355+
// Matching routes
356+
rs := []string{"/a/b/c", "a/b/c", "/a/b/c/", "a/b/c/", "/a/b/:c", "/a/b/:param", "/a/b/c/d/e/f/g", "/a/b/c/d/:param/*"}
357+
358+
// Check
359+
for _, s := range rs {
360+
if !r.Match(s, c) {
361+
t.Errorf("'%s' should match against '/a/b/:param/*'", s)
362+
}
363+
}
364+
}
365+
212366
func TestRouterMultiLevelParamUnmatch(t *testing.T) {
213367
// Create empty handler
214368
h := new(Handler)
@@ -328,6 +482,29 @@ func TestRouterGroupMatch(t *testing.T) {
328482
}
329483
}
330484

485+
func TestRouterGroupCatchAll(t *testing.T) {
486+
// Create empty handler
487+
h := new(Handler)
488+
489+
// Create empty context
490+
c := new(Context)
491+
c.Params = url.Values{}
492+
493+
// Create group
494+
g := RouteGroup("/v1")
495+
g.Add("/test/*", h)
496+
497+
// Matching routes
498+
rs := []string{"/v1/test/test", "/v1/test/:param/", "/v1/test/this/is/a/wild/card"}
499+
500+
// Check
501+
for _, s := range rs {
502+
if !g.Match(s, c) {
503+
t.Errorf("'%s' should match", s)
504+
}
505+
}
506+
}
507+
331508
func TestRouterGroupNotMatch(t *testing.T) {
332509
// Create empty handler
333510
h := new(Handler)

yarf.go

+9-9
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
)
77

88
// Framework version string
9-
const Version = "0.7"
9+
const Version = "0.7.1"
1010

1111
// Yarf is the main entry point for the framework and it centralizes most of the functionality.
1212
// All configuration actions are handled by this object.
@@ -147,14 +147,6 @@ func (y *Yarf) finish(c *Context, err error) {
147147
return
148148
}
149149

150-
// Write error data to response.
151-
c.Response.WriteHeader(yerr.Code())
152-
153-
// Render errors if debug enabled
154-
if y.Debug {
155-
c.Render(yerr.Body())
156-
}
157-
158150
// Log errors
159151
if y.Logger != nil {
160152
y.Logger.Printf(
@@ -165,6 +157,14 @@ func (y *Yarf) finish(c *Context, err error) {
165157
yerr.Msg(),
166158
)
167159
}
160+
161+
// Render errors if debug enabled
162+
if y.Debug {
163+
c.Render(yerr.Body())
164+
}
165+
166+
// Write error data to response.
167+
c.Response.WriteHeader(yerr.Code())
168168
}
169169

170170
// Start initiates a new http yarf server and start listening.

0 commit comments

Comments
 (0)