Skip to content

Commit f3de813

Browse files
rw-accessappleboy
andauthored
Add mixed param and non-param paths (port of httprouter#329) (#2663)
Co-authored-by: Bo-Yi Wu <[email protected]>
1 parent a331dc6 commit f3de813

File tree

5 files changed

+111
-67
lines changed

5 files changed

+111
-67
lines changed

AUTHORS.md

+2
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ People and companies, who have contributed, in alphabetical order.
190190
**@rogierlommers (Rogier Lommers)**
191191
- Add updated static serve example
192192

193+
**@rw-access (Ross Wolf)**
194+
- Added support to mix exact and param routes
193195

194196
**@se77en (Damon Zhao)**
195197
- Improve color logging

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Gin ChangeLog
22

3+
## Gin v1.7.0
4+
5+
### ENHANCEMENTS
6+
7+
* Support params and exact routes without creating conflicts [#2663](https://github.com/gin-gonic/gin/pull/2663)
8+
39
## Gin v1.6.3
410

511
### ENHANCEMENTS

README.md

+7
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,13 @@ func main() {
243243
c.FullPath() == "/user/:name/*action" // true
244244
})
245245

246+
// This handler will add a new router for /user/groups.
247+
// Exact routes are resolved before param routes, regardless of the order they were defined.
248+
// Routes starting with /user/groups are never interpreted as /user/:name/... routes
249+
router.GET("/user/groups", func(c *gin.Context) {
250+
c.String(http.StatusOK, "The available groups are [...]", name)
251+
})
252+
246253
router.Run(":8080")
247254
}
248255
```

tree.go

+58-55
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,16 @@ func longestCommonPrefix(a, b string) int {
8080
return i
8181
}
8282

83+
// addChild will add a child node, keeping wildcards at the end
84+
func (n *node) addChild(child *node) {
85+
if n.wildChild && len(n.children) > 0 {
86+
wildcardChild := n.children[len(n.children)-1]
87+
n.children = append(n.children[:len(n.children)-1], child, wildcardChild)
88+
} else {
89+
n.children = append(n.children, child)
90+
}
91+
}
92+
8393
func countParams(path string) uint16 {
8494
var n uint16
8595
s := bytesconv.StringToBytes(path)
@@ -103,7 +113,7 @@ type node struct {
103113
wildChild bool
104114
nType nodeType
105115
priority uint32
106-
children []*node
116+
children []*node // child nodes, at most 1 :param style node at the end of the array
107117
handlers HandlersChain
108118
fullPath string
109119
}
@@ -177,36 +187,9 @@ walk:
177187
// Make new node a child of this node
178188
if i < len(path) {
179189
path = path[i:]
180-
181-
if n.wildChild {
182-
parentFullPathIndex += len(n.path)
183-
n = n.children[0]
184-
n.priority++
185-
186-
// Check if the wildcard matches
187-
if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
188-
// Adding a child to a catchAll is not possible
189-
n.nType != catchAll &&
190-
// Check for longer wildcard, e.g. :name and :names
191-
(len(n.path) >= len(path) || path[len(n.path)] == '/') {
192-
continue walk
193-
}
194-
195-
pathSeg := path
196-
if n.nType != catchAll {
197-
pathSeg = strings.SplitN(path, "/", 2)[0]
198-
}
199-
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
200-
panic("'" + pathSeg +
201-
"' in new path '" + fullPath +
202-
"' conflicts with existing wildcard '" + n.path +
203-
"' in existing prefix '" + prefix +
204-
"'")
205-
}
206-
207190
c := path[0]
208191

209-
// slash after param
192+
// '/' after param
210193
if n.nType == param && c == '/' && len(n.children) == 1 {
211194
parentFullPathIndex += len(n.path)
212195
n = n.children[0]
@@ -225,21 +208,47 @@ walk:
225208
}
226209

227210
// Otherwise insert it
228-
if c != ':' && c != '*' {
211+
if c != ':' && c != '*' && n.nType != catchAll {
229212
// []byte for proper unicode char conversion, see #65
230213
n.indices += bytesconv.BytesToString([]byte{c})
231214
child := &node{
232215
fullPath: fullPath,
233216
}
234-
n.children = append(n.children, child)
217+
n.addChild(child)
235218
n.incrementChildPrio(len(n.indices) - 1)
236219
n = child
220+
} else if n.wildChild {
221+
// inserting a wildcard node, need to check if it conflicts with the existing wildcard
222+
n = n.children[len(n.children)-1]
223+
n.priority++
224+
225+
// Check if the wildcard matches
226+
if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
227+
// Adding a child to a catchAll is not possible
228+
n.nType != catchAll &&
229+
// Check for longer wildcard, e.g. :name and :names
230+
(len(n.path) >= len(path) || path[len(n.path)] == '/') {
231+
continue walk
232+
}
233+
234+
// Wildcard conflict
235+
pathSeg := path
236+
if n.nType != catchAll {
237+
pathSeg = strings.SplitN(pathSeg, "/", 2)[0]
238+
}
239+
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
240+
panic("'" + pathSeg +
241+
"' in new path '" + fullPath +
242+
"' conflicts with existing wildcard '" + n.path +
243+
"' in existing prefix '" + prefix +
244+
"'")
237245
}
246+
238247
n.insertChild(path, fullPath, handlers)
239248
return
240249
}
241250

242-
// Otherwise and handle to current node
251+
// Otherwise add handle to current node
243252
if n.handlers != nil {
244253
panic("handlers are already registered for path '" + fullPath + "'")
245254
}
@@ -293,27 +302,20 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
293302
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
294303
}
295304

296-
// Check if this node has existing children which would be
297-
// unreachable if we insert the wildcard here
298-
if len(n.children) > 0 {
299-
panic("wildcard segment '" + wildcard +
300-
"' conflicts with existing children in path '" + fullPath + "'")
301-
}
302-
303305
if wildcard[0] == ':' { // param
304306
if i > 0 {
305307
// Insert prefix before the current wildcard
306308
n.path = path[:i]
307309
path = path[i:]
308310
}
309311

310-
n.wildChild = true
311312
child := &node{
312313
nType: param,
313314
path: wildcard,
314315
fullPath: fullPath,
315316
}
316-
n.children = []*node{child}
317+
n.addChild(child)
318+
n.wildChild = true
317319
n = child
318320
n.priority++
319321

@@ -326,7 +328,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
326328
priority: 1,
327329
fullPath: fullPath,
328330
}
329-
n.children = []*node{child}
331+
n.addChild(child)
330332
n = child
331333
continue
332334
}
@@ -360,7 +362,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
360362
fullPath: fullPath,
361363
}
362364

363-
n.children = []*node{child}
365+
n.addChild(child)
364366
n.indices = string('/')
365367
n = child
366368
n.priority++
@@ -404,27 +406,28 @@ walk: // Outer loop for walking the tree
404406
if len(path) > len(prefix) {
405407
if path[:len(prefix)] == prefix {
406408
path = path[len(prefix):]
407-
// If this node does not have a wildcard (param or catchAll)
408-
// child, we can just look up the next child node and continue
409-
// to walk down the tree
410-
if !n.wildChild {
411-
idxc := path[0]
412-
for i, c := range []byte(n.indices) {
413-
if c == idxc {
414-
n = n.children[i]
415-
continue walk
416-
}
409+
410+
// Try all the non-wildcard children first by matching the indices
411+
idxc := path[0]
412+
for i, c := range []byte(n.indices) {
413+
if c == idxc {
414+
n = n.children[i]
415+
continue walk
417416
}
417+
}
418418

419+
// If there is no wildcard pattern, recommend a redirection
420+
if !n.wildChild {
419421
// Nothing found.
420422
// We can recommend to redirect to the same URL without a
421423
// trailing slash if a leaf exists for that path.
422424
value.tsr = (path == "/" && n.handlers != nil)
423425
return
424426
}
425427

426-
// Handle wildcard child
427-
n = n.children[0]
428+
// Handle wildcard child, which is always at the end of the array
429+
n = n.children[len(n.children)-1]
430+
428431
switch n.nType {
429432
case param:
430433
// Find param end (either '/' or path end)

tree_test.go

+38-12
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ func TestTreeWildcard(t *testing.T) {
137137
"/",
138138
"/cmd/:tool/:sub",
139139
"/cmd/:tool/",
140+
"/cmd/whoami",
141+
"/cmd/whoami/root/",
140142
"/src/*filepath",
141143
"/search/",
142144
"/search/:query",
@@ -155,8 +157,12 @@ func TestTreeWildcard(t *testing.T) {
155157

156158
checkRequests(t, tree, testRequests{
157159
{"/", false, "/", nil},
158-
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}},
159-
{"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}},
160+
{"/cmd/test", true, "/cmd/:tool/", Params{Param{"tool", "test"}}},
161+
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
162+
{"/cmd/whoami", false, "/cmd/whoami", nil},
163+
{"/cmd/whoami/", true, "/cmd/whoami", nil},
164+
{"/cmd/whoami/root/", false, "/cmd/whoami/root/", nil},
165+
{"/cmd/whoami/root", true, "/cmd/whoami/root/", nil},
160166
{"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "test"}, Param{Key: "sub", Value: "3"}}},
161167
{"/src/", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/"}}},
162168
{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
@@ -245,35 +251,56 @@ func testRoutes(t *testing.T, routes []testRoute) {
245251
func TestTreeWildcardConflict(t *testing.T) {
246252
routes := []testRoute{
247253
{"/cmd/:tool/:sub", false},
248-
{"/cmd/vet", true},
254+
{"/cmd/vet", false},
255+
{"/foo/bar", false},
256+
{"/foo/:name", false},
257+
{"/foo/:names", true},
258+
{"/cmd/*path", true},
259+
{"/cmd/:badvar", true},
260+
{"/cmd/:tool/names", false},
261+
{"/cmd/:tool/:badsub/details", true},
249262
{"/src/*filepath", false},
263+
{"/src/:file", true},
264+
{"/src/static.json", true},
250265
{"/src/*filepathx", true},
251266
{"/src/", true},
267+
{"/src/foo/bar", true},
252268
{"/src1/", false},
253269
{"/src1/*filepath", true},
254270
{"/src2*filepath", true},
271+
{"/src2/*filepath", false},
255272
{"/search/:query", false},
256-
{"/search/invalid", true},
273+
{"/search/valid", false},
257274
{"/user_:name", false},
258-
{"/user_x", true},
275+
{"/user_x", false},
259276
{"/user_:name", false},
260277
{"/id:id", false},
261-
{"/id/:id", true},
278+
{"/id/:id", false},
279+
}
280+
testRoutes(t, routes)
281+
}
282+
283+
func TestCatchAllAfterSlash(t *testing.T) {
284+
routes := []testRoute{
285+
{"/non-leading-*catchall", true},
262286
}
263287
testRoutes(t, routes)
264288
}
265289

266290
func TestTreeChildConflict(t *testing.T) {
267291
routes := []testRoute{
268292
{"/cmd/vet", false},
269-
{"/cmd/:tool/:sub", true},
293+
{"/cmd/:tool", false},
294+
{"/cmd/:tool/:sub", false},
295+
{"/cmd/:tool/misc", false},
296+
{"/cmd/:tool/:othersub", true},
270297
{"/src/AUTHORS", false},
271298
{"/src/*filepath", true},
272299
{"/user_x", false},
273-
{"/user_:name", true},
300+
{"/user_:name", false},
274301
{"/id/:id", false},
275-
{"/id:id", true},
276-
{"/:id", true},
302+
{"/id:id", false},
303+
{"/:id", false},
277304
{"/*filepath", true},
278305
}
279306
testRoutes(t, routes)
@@ -688,8 +715,7 @@ func TestTreeWildcardConflictEx(t *testing.T) {
688715
{"/who/are/foo", "/foo", `/who/are/\*you`, `/\*you`},
689716
{"/who/are/foo/", "/foo/", `/who/are/\*you`, `/\*you`},
690717
{"/who/are/foo/bar", "/foo/bar", `/who/are/\*you`, `/\*you`},
691-
{"/conxxx", "xxx", `/con:tact`, `:tact`},
692-
{"/conooo/xxx", "ooo", `/con:tact`, `:tact`},
718+
{"/con:nection", ":nection", `/con:tact`, `:tact`},
693719
}
694720

695721
for _, conflict := range conflicts {

0 commit comments

Comments
 (0)