Add mixed param and non-param paths (port of httprouter#329) (#2663)
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
This commit is contained in:
parent
a331dc6a31
commit
f3de8132c5
@ -190,6 +190,8 @@ People and companies, who have contributed, in alphabetical order.
|
|||||||
**@rogierlommers (Rogier Lommers)**
|
**@rogierlommers (Rogier Lommers)**
|
||||||
- Add updated static serve example
|
- Add updated static serve example
|
||||||
|
|
||||||
|
**@rw-access (Ross Wolf)**
|
||||||
|
- Added support to mix exact and param routes
|
||||||
|
|
||||||
**@se77en (Damon Zhao)**
|
**@se77en (Damon Zhao)**
|
||||||
- Improve color logging
|
- Improve color logging
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
# Gin ChangeLog
|
# Gin ChangeLog
|
||||||
|
|
||||||
|
## Gin v1.7.0
|
||||||
|
|
||||||
|
### ENHANCEMENTS
|
||||||
|
|
||||||
|
* Support params and exact routes without creating conflicts [#2663](https://github.com/gin-gonic/gin/pull/2663)
|
||||||
|
|
||||||
## Gin v1.6.3
|
## Gin v1.6.3
|
||||||
|
|
||||||
### ENHANCEMENTS
|
### ENHANCEMENTS
|
||||||
|
@ -243,6 +243,13 @@ func main() {
|
|||||||
c.FullPath() == "/user/:name/*action" // true
|
c.FullPath() == "/user/:name/*action" // true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// This handler will add a new router for /user/groups.
|
||||||
|
// Exact routes are resolved before param routes, regardless of the order they were defined.
|
||||||
|
// Routes starting with /user/groups are never interpreted as /user/:name/... routes
|
||||||
|
router.GET("/user/groups", func(c *gin.Context) {
|
||||||
|
c.String(http.StatusOK, "The available groups are [...]", name)
|
||||||
|
})
|
||||||
|
|
||||||
router.Run(":8080")
|
router.Run(":8080")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
115
tree.go
115
tree.go
@ -80,6 +80,16 @@ func longestCommonPrefix(a, b string) int {
|
|||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// addChild will add a child node, keeping wildcards at the end
|
||||||
|
func (n *node) addChild(child *node) {
|
||||||
|
if n.wildChild && len(n.children) > 0 {
|
||||||
|
wildcardChild := n.children[len(n.children)-1]
|
||||||
|
n.children = append(n.children[:len(n.children)-1], child, wildcardChild)
|
||||||
|
} else {
|
||||||
|
n.children = append(n.children, child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func countParams(path string) uint16 {
|
func countParams(path string) uint16 {
|
||||||
var n uint16
|
var n uint16
|
||||||
s := bytesconv.StringToBytes(path)
|
s := bytesconv.StringToBytes(path)
|
||||||
@ -103,7 +113,7 @@ type node struct {
|
|||||||
wildChild bool
|
wildChild bool
|
||||||
nType nodeType
|
nType nodeType
|
||||||
priority uint32
|
priority uint32
|
||||||
children []*node
|
children []*node // child nodes, at most 1 :param style node at the end of the array
|
||||||
handlers HandlersChain
|
handlers HandlersChain
|
||||||
fullPath string
|
fullPath string
|
||||||
}
|
}
|
||||||
@ -177,36 +187,9 @@ walk:
|
|||||||
// Make new node a child of this node
|
// Make new node a child of this node
|
||||||
if i < len(path) {
|
if i < len(path) {
|
||||||
path = path[i:]
|
path = path[i:]
|
||||||
|
|
||||||
if n.wildChild {
|
|
||||||
parentFullPathIndex += len(n.path)
|
|
||||||
n = n.children[0]
|
|
||||||
n.priority++
|
|
||||||
|
|
||||||
// Check if the wildcard matches
|
|
||||||
if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
|
|
||||||
// Adding a child to a catchAll is not possible
|
|
||||||
n.nType != catchAll &&
|
|
||||||
// Check for longer wildcard, e.g. :name and :names
|
|
||||||
(len(n.path) >= len(path) || path[len(n.path)] == '/') {
|
|
||||||
continue walk
|
|
||||||
}
|
|
||||||
|
|
||||||
pathSeg := path
|
|
||||||
if n.nType != catchAll {
|
|
||||||
pathSeg = strings.SplitN(path, "/", 2)[0]
|
|
||||||
}
|
|
||||||
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
|
|
||||||
panic("'" + pathSeg +
|
|
||||||
"' in new path '" + fullPath +
|
|
||||||
"' conflicts with existing wildcard '" + n.path +
|
|
||||||
"' in existing prefix '" + prefix +
|
|
||||||
"'")
|
|
||||||
}
|
|
||||||
|
|
||||||
c := path[0]
|
c := path[0]
|
||||||
|
|
||||||
// slash after param
|
// '/' after param
|
||||||
if n.nType == param && c == '/' && len(n.children) == 1 {
|
if n.nType == param && c == '/' && len(n.children) == 1 {
|
||||||
parentFullPathIndex += len(n.path)
|
parentFullPathIndex += len(n.path)
|
||||||
n = n.children[0]
|
n = n.children[0]
|
||||||
@ -225,21 +208,47 @@ walk:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise insert it
|
// Otherwise insert it
|
||||||
if c != ':' && c != '*' {
|
if c != ':' && c != '*' && n.nType != catchAll {
|
||||||
// []byte for proper unicode char conversion, see #65
|
// []byte for proper unicode char conversion, see #65
|
||||||
n.indices += bytesconv.BytesToString([]byte{c})
|
n.indices += bytesconv.BytesToString([]byte{c})
|
||||||
child := &node{
|
child := &node{
|
||||||
fullPath: fullPath,
|
fullPath: fullPath,
|
||||||
}
|
}
|
||||||
n.children = append(n.children, child)
|
n.addChild(child)
|
||||||
n.incrementChildPrio(len(n.indices) - 1)
|
n.incrementChildPrio(len(n.indices) - 1)
|
||||||
n = child
|
n = child
|
||||||
|
} else if n.wildChild {
|
||||||
|
// inserting a wildcard node, need to check if it conflicts with the existing wildcard
|
||||||
|
n = n.children[len(n.children)-1]
|
||||||
|
n.priority++
|
||||||
|
|
||||||
|
// Check if the wildcard matches
|
||||||
|
if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
|
||||||
|
// Adding a child to a catchAll is not possible
|
||||||
|
n.nType != catchAll &&
|
||||||
|
// Check for longer wildcard, e.g. :name and :names
|
||||||
|
(len(n.path) >= len(path) || path[len(n.path)] == '/') {
|
||||||
|
continue walk
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wildcard conflict
|
||||||
|
pathSeg := path
|
||||||
|
if n.nType != catchAll {
|
||||||
|
pathSeg = strings.SplitN(pathSeg, "/", 2)[0]
|
||||||
|
}
|
||||||
|
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
|
||||||
|
panic("'" + pathSeg +
|
||||||
|
"' in new path '" + fullPath +
|
||||||
|
"' conflicts with existing wildcard '" + n.path +
|
||||||
|
"' in existing prefix '" + prefix +
|
||||||
|
"'")
|
||||||
}
|
}
|
||||||
|
|
||||||
n.insertChild(path, fullPath, handlers)
|
n.insertChild(path, fullPath, handlers)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise and handle to current node
|
// Otherwise add handle to current node
|
||||||
if n.handlers != nil {
|
if n.handlers != nil {
|
||||||
panic("handlers are already registered for path '" + fullPath + "'")
|
panic("handlers are already registered for path '" + fullPath + "'")
|
||||||
}
|
}
|
||||||
@ -293,13 +302,6 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
|
|||||||
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
|
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this node has existing children which would be
|
|
||||||
// unreachable if we insert the wildcard here
|
|
||||||
if len(n.children) > 0 {
|
|
||||||
panic("wildcard segment '" + wildcard +
|
|
||||||
"' conflicts with existing children in path '" + fullPath + "'")
|
|
||||||
}
|
|
||||||
|
|
||||||
if wildcard[0] == ':' { // param
|
if wildcard[0] == ':' { // param
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
// Insert prefix before the current wildcard
|
// Insert prefix before the current wildcard
|
||||||
@ -307,13 +309,13 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
|
|||||||
path = path[i:]
|
path = path[i:]
|
||||||
}
|
}
|
||||||
|
|
||||||
n.wildChild = true
|
|
||||||
child := &node{
|
child := &node{
|
||||||
nType: param,
|
nType: param,
|
||||||
path: wildcard,
|
path: wildcard,
|
||||||
fullPath: fullPath,
|
fullPath: fullPath,
|
||||||
}
|
}
|
||||||
n.children = []*node{child}
|
n.addChild(child)
|
||||||
|
n.wildChild = true
|
||||||
n = child
|
n = child
|
||||||
n.priority++
|
n.priority++
|
||||||
|
|
||||||
@ -326,7 +328,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
|
|||||||
priority: 1,
|
priority: 1,
|
||||||
fullPath: fullPath,
|
fullPath: fullPath,
|
||||||
}
|
}
|
||||||
n.children = []*node{child}
|
n.addChild(child)
|
||||||
n = child
|
n = child
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -360,7 +362,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
|
|||||||
fullPath: fullPath,
|
fullPath: fullPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
n.children = []*node{child}
|
n.addChild(child)
|
||||||
n.indices = string('/')
|
n.indices = string('/')
|
||||||
n = child
|
n = child
|
||||||
n.priority++
|
n.priority++
|
||||||
@ -404,18 +406,18 @@ walk: // Outer loop for walking the tree
|
|||||||
if len(path) > len(prefix) {
|
if len(path) > len(prefix) {
|
||||||
if path[:len(prefix)] == prefix {
|
if path[:len(prefix)] == prefix {
|
||||||
path = path[len(prefix):]
|
path = path[len(prefix):]
|
||||||
// If this node does not have a wildcard (param or catchAll)
|
|
||||||
// child, we can just look up the next child node and continue
|
|
||||||
// to walk down the tree
|
|
||||||
if !n.wildChild {
|
|
||||||
idxc := path[0]
|
|
||||||
for i, c := range []byte(n.indices) {
|
|
||||||
if c == idxc {
|
|
||||||
n = n.children[i]
|
|
||||||
continue walk
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Try all the non-wildcard children first by matching the indices
|
||||||
|
idxc := path[0]
|
||||||
|
for i, c := range []byte(n.indices) {
|
||||||
|
if c == idxc {
|
||||||
|
n = n.children[i]
|
||||||
|
continue walk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is no wildcard pattern, recommend a redirection
|
||||||
|
if !n.wildChild {
|
||||||
// Nothing found.
|
// Nothing found.
|
||||||
// We can recommend to redirect to the same URL without a
|
// We can recommend to redirect to the same URL without a
|
||||||
// trailing slash if a leaf exists for that path.
|
// trailing slash if a leaf exists for that path.
|
||||||
@ -423,8 +425,9 @@ walk: // Outer loop for walking the tree
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle wildcard child
|
// Handle wildcard child, which is always at the end of the array
|
||||||
n = n.children[0]
|
n = n.children[len(n.children)-1]
|
||||||
|
|
||||||
switch n.nType {
|
switch n.nType {
|
||||||
case param:
|
case param:
|
||||||
// Find param end (either '/' or path end)
|
// Find param end (either '/' or path end)
|
||||||
|
50
tree_test.go
50
tree_test.go
@ -137,6 +137,8 @@ func TestTreeWildcard(t *testing.T) {
|
|||||||
"/",
|
"/",
|
||||||
"/cmd/:tool/:sub",
|
"/cmd/:tool/:sub",
|
||||||
"/cmd/:tool/",
|
"/cmd/:tool/",
|
||||||
|
"/cmd/whoami",
|
||||||
|
"/cmd/whoami/root/",
|
||||||
"/src/*filepath",
|
"/src/*filepath",
|
||||||
"/search/",
|
"/search/",
|
||||||
"/search/:query",
|
"/search/:query",
|
||||||
@ -155,8 +157,12 @@ func TestTreeWildcard(t *testing.T) {
|
|||||||
|
|
||||||
checkRequests(t, tree, testRequests{
|
checkRequests(t, tree, testRequests{
|
||||||
{"/", false, "/", nil},
|
{"/", false, "/", nil},
|
||||||
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}},
|
{"/cmd/test", true, "/cmd/:tool/", Params{Param{"tool", "test"}}},
|
||||||
{"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}},
|
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
|
||||||
|
{"/cmd/whoami", false, "/cmd/whoami", nil},
|
||||||
|
{"/cmd/whoami/", true, "/cmd/whoami", nil},
|
||||||
|
{"/cmd/whoami/root/", false, "/cmd/whoami/root/", nil},
|
||||||
|
{"/cmd/whoami/root", true, "/cmd/whoami/root/", nil},
|
||||||
{"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "test"}, Param{Key: "sub", Value: "3"}}},
|
{"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "test"}, Param{Key: "sub", Value: "3"}}},
|
||||||
{"/src/", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/"}}},
|
{"/src/", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/"}}},
|
||||||
{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
|
{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
|
||||||
@ -245,20 +251,38 @@ func testRoutes(t *testing.T, routes []testRoute) {
|
|||||||
func TestTreeWildcardConflict(t *testing.T) {
|
func TestTreeWildcardConflict(t *testing.T) {
|
||||||
routes := []testRoute{
|
routes := []testRoute{
|
||||||
{"/cmd/:tool/:sub", false},
|
{"/cmd/:tool/:sub", false},
|
||||||
{"/cmd/vet", true},
|
{"/cmd/vet", false},
|
||||||
|
{"/foo/bar", false},
|
||||||
|
{"/foo/:name", false},
|
||||||
|
{"/foo/:names", true},
|
||||||
|
{"/cmd/*path", true},
|
||||||
|
{"/cmd/:badvar", true},
|
||||||
|
{"/cmd/:tool/names", false},
|
||||||
|
{"/cmd/:tool/:badsub/details", true},
|
||||||
{"/src/*filepath", false},
|
{"/src/*filepath", false},
|
||||||
|
{"/src/:file", true},
|
||||||
|
{"/src/static.json", true},
|
||||||
{"/src/*filepathx", true},
|
{"/src/*filepathx", true},
|
||||||
{"/src/", true},
|
{"/src/", true},
|
||||||
|
{"/src/foo/bar", true},
|
||||||
{"/src1/", false},
|
{"/src1/", false},
|
||||||
{"/src1/*filepath", true},
|
{"/src1/*filepath", true},
|
||||||
{"/src2*filepath", true},
|
{"/src2*filepath", true},
|
||||||
|
{"/src2/*filepath", false},
|
||||||
{"/search/:query", false},
|
{"/search/:query", false},
|
||||||
{"/search/invalid", true},
|
{"/search/valid", false},
|
||||||
{"/user_:name", false},
|
{"/user_:name", false},
|
||||||
{"/user_x", true},
|
{"/user_x", false},
|
||||||
{"/user_:name", false},
|
{"/user_:name", false},
|
||||||
{"/id:id", false},
|
{"/id:id", false},
|
||||||
{"/id/:id", true},
|
{"/id/:id", false},
|
||||||
|
}
|
||||||
|
testRoutes(t, routes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCatchAllAfterSlash(t *testing.T) {
|
||||||
|
routes := []testRoute{
|
||||||
|
{"/non-leading-*catchall", true},
|
||||||
}
|
}
|
||||||
testRoutes(t, routes)
|
testRoutes(t, routes)
|
||||||
}
|
}
|
||||||
@ -266,14 +290,17 @@ func TestTreeWildcardConflict(t *testing.T) {
|
|||||||
func TestTreeChildConflict(t *testing.T) {
|
func TestTreeChildConflict(t *testing.T) {
|
||||||
routes := []testRoute{
|
routes := []testRoute{
|
||||||
{"/cmd/vet", false},
|
{"/cmd/vet", false},
|
||||||
{"/cmd/:tool/:sub", true},
|
{"/cmd/:tool", false},
|
||||||
|
{"/cmd/:tool/:sub", false},
|
||||||
|
{"/cmd/:tool/misc", false},
|
||||||
|
{"/cmd/:tool/:othersub", true},
|
||||||
{"/src/AUTHORS", false},
|
{"/src/AUTHORS", false},
|
||||||
{"/src/*filepath", true},
|
{"/src/*filepath", true},
|
||||||
{"/user_x", false},
|
{"/user_x", false},
|
||||||
{"/user_:name", true},
|
{"/user_:name", false},
|
||||||
{"/id/:id", false},
|
{"/id/:id", false},
|
||||||
{"/id:id", true},
|
{"/id:id", false},
|
||||||
{"/:id", true},
|
{"/:id", false},
|
||||||
{"/*filepath", true},
|
{"/*filepath", true},
|
||||||
}
|
}
|
||||||
testRoutes(t, routes)
|
testRoutes(t, routes)
|
||||||
@ -688,8 +715,7 @@ func TestTreeWildcardConflictEx(t *testing.T) {
|
|||||||
{"/who/are/foo", "/foo", `/who/are/\*you`, `/\*you`},
|
{"/who/are/foo", "/foo", `/who/are/\*you`, `/\*you`},
|
||||||
{"/who/are/foo/", "/foo/", `/who/are/\*you`, `/\*you`},
|
{"/who/are/foo/", "/foo/", `/who/are/\*you`, `/\*you`},
|
||||||
{"/who/are/foo/bar", "/foo/bar", `/who/are/\*you`, `/\*you`},
|
{"/who/are/foo/bar", "/foo/bar", `/who/are/\*you`, `/\*you`},
|
||||||
{"/conxxx", "xxx", `/con:tact`, `:tact`},
|
{"/con:nection", ":nection", `/con:tact`, `:tact`},
|
||||||
{"/conooo/xxx", "ooo", `/con:tact`, `:tact`},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, conflict := range conflicts {
|
for _, conflict := range conflicts {
|
||||||
|
Loading…
Reference in New Issue
Block a user