Skip to content

Commit 510879a

Browse files
author
Sergio Andres Virviescas Santana
committed
Add optional regex validation for wildcards and allow wildcards with suffix
1 parent 137e277 commit 510879a

File tree

19 files changed

+756
-303
lines changed

19 files changed

+756
-303
lines changed

README.md

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -98,41 +98,56 @@ func Hello(ctx *fasthttp.RequestCtx) {
9898
func main() {
9999
r := router.New()
100100
r.GET("/", Index)
101-
r.GET("/hello/:name", Hello)
101+
r.GET("/hello/{name}", Hello)
102102

103103
log.Fatal(fasthttp.ListenAndServe(":8080", r.Handler))
104104
}
105105
```
106106

107107
### Named parameters
108108

109-
As you can see, `:name` is a _named parameter_. The values are accessible via `RequestCtx.UserValues`. You can get the value of a parameter by using the `ctx.UserValue("name")`.
109+
As you can see, `{name}` is a _named parameter_. The values are accessible via `RequestCtx.UserValues`. You can get the value of a parameter by using the `ctx.UserValue("name")`.
110110

111111
Named parameters only match a single path segment:
112112

113113
```
114-
Pattern: /user/:user
114+
Pattern: /user/{user}
115115
116-
/user/gordon match
117-
/user/you match
118-
/user/gordon/profile no match
119-
/user/ no match
116+
/user/gordon match
117+
/user/you match
118+
/user/gordon/profile no match
119+
/user/ no match
120+
121+
Pattern with suffix: /user/{user}_admin
122+
123+
/user/gordon_admin match
124+
/user/you_admin match
125+
/user/you no match
126+
/user/gordon/profile no match
127+
/user/gordon_admin/profile no match
128+
/user/ no match
120129
```
121130

122-
**Note:** Since this router has only explicit matches, you can not register static routes and parameters for the same path segment. For example you can not register the patterns `/user/new` and `/user/:user` for the same request method at the same time. The routing of different request methods is independent from each other.
131+
**Note:** Since this router has only explicit matches, you can not register static routes and parameters for the same path segment. For example you can not register the patterns `/user/new` and `/user/{user}` for the same request method at the same time. The routing of different request methods is independent from each other.
123132

124133
#### Optional parameters
125134

126-
If you need define an optional parameters, add `?` at the end of param name. `:name?`
135+
If you need define an optional parameters, add `?` at the end of param name. `{name?}`
136+
137+
#### Regex validation
138+
139+
If you need define a validation, you could use a custom regex for the paramater value, add `:<regex>` after the name. For example: `{name:[a-zA-Z]{5}}`.
140+
141+
**_Optional paramters and regex validation are compatibles, only add `?` between the name and the regex. For example: `{name?:[a-zA-Z]{5}}`._**
127142

128143
### Catch-All parameters
129144

130-
The second type are _catch-all_ parameters and have the form `*name`.
145+
The second type are _catch-all_ parameters and have the form `{name:*}`.
131146
Like the name suggests, they match everything.
132147
Therefore they must always be at the **end** of the pattern:
133148

134149
```
135-
Pattern: /src/*filepath
150+
Pattern: /src/{filepath:*}
136151
137152
/src/ match
138153
/src/somefile.go match
@@ -145,19 +160,19 @@ The router relies on a tree structure which makes heavy use of _common prefixes_
145160

146161
```
147162
Priority Path Handle
148-
9 \ *<1>
149-
3 ├s nil
150-
2 |├earch\ *<2>
151-
1 |└upport\ *<3>
152-
2 ├blog\ *<4>
153-
1 | └:post nil
154-
1 | └\ *<5>
155-
2 ├about-us\ *<6>
156-
1 | └team\ *<7>
157-
1 └contact\ *<8>
163+
9 \ *<1>
164+
3 ├s nil
165+
2 |├earch\ *<2>
166+
1 |└upport\ *<3>
167+
2 ├blog\ *<4>
168+
1 | └{post} nil
169+
1 | └\ *<5>
170+
2 ├about-us\ *<6>
171+
1 | └team\ *<7>
172+
1 └contact\ *<8>
158173
```
159174

160-
Every `*<num>` represents the memory address of a handler function (a pointer). If you follow a path trough the tree from the root to the leaf, you get the complete route path, e.g `\blog\:post\`, where `:post` is just a placeholder ([_parameter_](#named-parameters)) for an actual post name. Unlike hash-maps, a tree structure also allows us to use dynamic parts like the `:post` parameter, since we actually match against the routing patterns instead of just comparing hashes. [As benchmarks show][benchmark], this works very well and efficient.
175+
Every `*<num>` represents the memory address of a handler function (a pointer). If you follow a path trough the tree from the root to the leaf, you get the complete route path, e.g `\blog\{post}\`, where `{post}` is just a placeholder ([_parameter_](#named-parameters)) for an actual post name. Unlike hash-maps, a tree structure also allows us to use dynamic parts like the `{post}` parameter, since we actually match against the routing patterns instead of just comparing hashes. [As benchmarks show][benchmark], this works very well and efficient.
161176

162177
Since URL paths have a hierarchical structure and make use only of a limited set of characters (byte values), it is very likely that there are a lot of common prefixes. This allows us to easily reduce the routing into ever smaller problems. Moreover the router manages a separate tree for every request method. For one thing it is more space efficient than holding a method->handle map in every single node, for another thing is also allows us to greatly reduce the routing problem before even starting the look-up in the prefix-tree.
163178

@@ -208,7 +223,7 @@ The `NotFound` handler can for example be used to serve static files from the ro
208223
r.NotFound = fasthttp.FSHandler("./public", 0)
209224
```
210225

211-
But this approach sidesteps the strict core rules of this router to avoid routing problems. A cleaner approach is to use a distinct sub-path for serving files, like `/static/*filepath` or `/files/*filepath`.
226+
But this approach sidesteps the strict core rules of this router to avoid routing problems. A cleaner approach is to use a distinct sub-path for serving files, like `/static/{filepath:*}` or `/files/{filepath:*}`.
212227

213228
## Web Frameworks based on Router
214229

examples/basic/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ func QueryArgs(ctx *fasthttp.RequestCtx) {
4545
func main() {
4646
r := router.New()
4747
r.GET("/", Index)
48-
r.GET("/hello/:name", Hello)
49-
r.GET("/multi/:name/:word", MultiParams)
48+
r.GET("/hello/{name}", Hello)
49+
r.GET("/multi/{name}/{word}", MultiParams)
5050
r.GET("/ping", QueryArgs)
5151

5252
log.Fatal(fasthttp.ListenAndServe(":8080", r.Handler))

examples/basic/basic.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ func MultiParams(ctx *fasthttp.RequestCtx) {
2323
fmt.Fprintf(ctx, "hi, %s, %s!\n", ctx.UserValue("name"), ctx.UserValue("word"))
2424
}
2525

26+
// RegexParams is the params handler with regex validation
27+
func RegexParams(ctx *fasthttp.RequestCtx) {
28+
fmt.Fprintf(ctx, "hi, %s\n", ctx.UserValue("name"))
29+
}
30+
2631
// QueryArgs is used for uri query args test #11:
2732
// if the req uri is /ping?name=foo, output: Pong! foo
2833
// if the req uri is /piNg?name=foo, redirect to /ping, output: Pong!
@@ -34,9 +39,10 @@ func QueryArgs(ctx *fasthttp.RequestCtx) {
3439
func main() {
3540
r := router.New()
3641
r.GET("/", Index)
37-
r.GET("/hello/:name", Hello)
38-
r.GET("/multi/:name/:word", MultiParams)
39-
r.GET("/optional/:name?/:word?", MultiParams)
42+
r.GET("/hello/{name}", Hello)
43+
r.GET("/multi/{name}/{word}", MultiParams)
44+
r.GET("/regex/{name:[a-zA-Z]+}/test", RegexParams)
45+
r.GET("/optional/{name?:[a-zA-Z]+}/{word?}", MultiParams)
4046
r.GET("/ping", QueryArgs)
4147

4248
log.Fatal(fasthttp.ListenAndServe(":8080", r.Handler))

examples/hosts/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func main() {
5050
// Initialize a router as usual
5151
r := router.New()
5252
r.GET("/", Index)
53-
r.GET("/hello/:name", Hello)
53+
r.GET("/hello/{name}", Hello)
5454

5555
// Make a new HostSwitch and insert the router (our http handler)
5656
// for example.com and port 12345

examples/hosts/hosts.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func main() {
3939
// Initialize a router as usual
4040
r := router.New()
4141
r.GET("/", Index)
42-
r.GET("/hello/:name", Hello)
42+
r.GET("/hello/{name}", Hello)
4343

4444
// Make a new HostSwitch and insert the router (our http handler)
4545
// for example.com and port 12345

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/fasthttp/router
33
go 1.13
44

55
require (
6-
github.com/savsgio/gotils v0.0.0-20200117113501-90175b0fbe3f
6+
github.com/savsgio/gotils v0.0.0-20200319105752-a9cc718f6a3f
77
github.com/valyala/bytebufferpool v1.0.0
88
github.com/valyala/fasthttp v1.9.0
99
)

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ github.com/klauspost/compress v1.8.2 h1:Bx0qjetmNjdFXASH02NSAREKpiaDwkO1DRZ3dV2K
22
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
33
github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w=
44
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
5-
github.com/savsgio/gotils v0.0.0-20200117113501-90175b0fbe3f h1:PgA+Olipyj258EIEYnpFFONrrCcAIWNUNoFhUfMqAGY=
6-
github.com/savsgio/gotils v0.0.0-20200117113501-90175b0fbe3f/go.mod h1:lHhJedqxCoHN+zMtwGNTXWmF0u9Jt363FYRhV6g0CdY=
5+
github.com/savsgio/gotils v0.0.0-20200319105752-a9cc718f6a3f h1:XfUnevLK4O22at3R77FlyQHKwlQs75LELdsH2wRX2KQ=
6+
github.com/savsgio/gotils v0.0.0-20200319105752-a9cc718f6a3f/go.mod h1:lHhJedqxCoHN+zMtwGNTXWmF0u9Jt363FYRhV6g0CdY=
77
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
88
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
99
github.com/valyala/fasthttp v1.9.0 h1:hNpmUdy/+ZXYpGy0OBfm7K0UQTzb73W0T0U4iJIVrMw=

path.go

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@
55

66
package router
77

8-
import (
9-
"strings"
10-
11-
"github.com/savsgio/gotils"
12-
)
8+
import "github.com/savsgio/gotils"
139

1410
const stackBufSize = 128
1511

@@ -159,29 +155,53 @@ func bufApp(buf *[]byte, s string, w int, c byte) {
159155
func getOptionalPaths(path string) []string {
160156
paths := make([]string, 0)
161157

162-
index := 0
163-
newParam := false
164-
for i := 0; i < len(path); i++ {
165-
c := path[i]
166-
167-
if c == ':' {
168-
index = i
169-
newParam = true
170-
} else if i > 0 && newParam && c == '?' {
171-
p := strings.Replace(path[:index], "?", "", -1)
172-
p = p[:len(p)-1]
173-
if !gotils.StringSliceInclude(paths, p) {
174-
paths = append(paths, p)
175-
}
158+
start := 0
159+
walk:
160+
for {
161+
if start >= len(path) {
162+
return paths
163+
}
176164

177-
p = strings.Replace(path[:i], "?", "", -1)
178-
if !gotils.StringSliceInclude(paths, p) {
179-
paths = append(paths, p)
180-
}
165+
c := path[start]
166+
start++
181167

182-
newParam = false
168+
if c != '{' {
169+
continue
183170
}
184-
}
185171

186-
return paths
172+
newPath := ""
173+
questionMarkIndex := -1
174+
175+
for end, c := range []byte(path[start:]) {
176+
switch c {
177+
case '}':
178+
if questionMarkIndex == -1 {
179+
continue walk
180+
}
181+
182+
end++
183+
if len(path) > start+end && path[start+end] == '/' {
184+
// Include trailing slash for a better lookup
185+
end++
186+
}
187+
188+
newPath += path[questionMarkIndex+1 : start+end]
189+
190+
path = path[:questionMarkIndex] + path[questionMarkIndex+1:] // remove '?'
191+
paths = append(paths, newPath)
192+
start += end - 1
193+
194+
continue walk
195+
196+
case '?':
197+
questionMarkIndex = start + end
198+
newPath += path[:questionMarkIndex]
199+
200+
// include the path without the wildcard
201+
if !gotils.StringSliceInclude(paths, path[:start-1]) {
202+
paths = append(paths, path[:start-1])
203+
}
204+
}
205+
}
206+
}
187207
}

path_test.go

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package router
77

88
import (
9+
"reflect"
910
"strings"
1011
"testing"
1112

@@ -142,24 +143,39 @@ func TestGetOptionalPath(t *testing.T) {
142143
ctx.SetStatusCode(fasthttp.StatusOK)
143144
}
144145

145-
expectedPaths := []string{
146-
"/show/:name",
147-
"/show/:name/:surname",
148-
"/show/:name/:surname/at",
149-
"/show/:name/:surname/at/:address",
150-
"/show/:name/:surname/at/:address/:id",
151-
"/show/:name/:surname/at/:address/:id/:phone",
146+
expected := []struct {
147+
path string
148+
tsr bool
149+
handler fasthttp.RequestHandler
150+
}{
151+
{"/show/{name}", true, nil},
152+
{"/show/{name}/", false, handler},
153+
{"/show/{name}/{surname}", true, nil},
154+
{"/show/{name}/{surname}/", false, handler},
155+
{"/show/{name}/{surname}/at", true, nil},
156+
{"/show/{name}/{surname}/at/", false, handler},
157+
{"/show/{name}/{surname}/at/{address}", true, nil},
158+
{"/show/{name}/{surname}/at/{address}/", false, handler},
159+
{"/show/{name}/{surname}/at/{address}/{id}", true, nil},
160+
{"/show/{name}/{surname}/at/{address}/{id}/", false, handler},
161+
{"/show/{name}/{surname}/at/{address}/{id}/{phone:.*}", false, handler},
162+
{"/show/{name}/{surname}/at/{address}/{id}/{phone:.*}/", true, nil},
152163
}
164+
153165
r := New()
154-
r.GET("/show/:name/:surname?/at/:address?/:id/:phone?", handler)
166+
r.GET("/show/{name}/{surname?}/at/{address?}/{id}/{phone?:.*}", handler)
155167

156-
for _, path := range expectedPaths {
168+
for _, e := range expected {
157169
ctx := new(fasthttp.RequestCtx)
158170

159-
h, _ := r.Lookup("GET", path, ctx)
171+
h, tsr := r.Lookup("GET", e.path, ctx)
172+
173+
if tsr != e.tsr {
174+
t.Errorf("TSR (path: %s) == %v, want %v", e.path, tsr, e.tsr)
175+
}
160176

161-
if h == nil {
162-
t.Errorf("Expected optional path '%s' is not registered", path)
177+
if reflect.ValueOf(h).Pointer() != reflect.ValueOf(e.handler).Pointer() {
178+
t.Errorf("Handler (path: %s) == %p, want %p", e.path, h, e.handler)
163179
}
164180
}
165181
}

radix/conts.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package radix
2+
3+
const stackBufSize = 128
4+
5+
const (
6+
root nodeType = iota
7+
static
8+
param
9+
wildcard
10+
)

0 commit comments

Comments
 (0)