From cc8646c14e33550a4b94c77414177111fecc3c6a Mon Sep 17 00:00:00 2001 From: Stephano George Date: Fri, 5 Aug 2022 17:07:08 +0800 Subject: [PATCH] Feat: allow "/*catch-all" and "/normal" routes coexist --- README.md | 8 +- gin_integration_test.go | 21 +- tree.go | 430 +++++++++++++++++++--------------------- tree_test.go | 91 +++++++-- 4 files changed, 310 insertions(+), 240 deletions(-) diff --git a/README.md b/README.md index 8d7b5ec428..e4d67fb32a 100644 --- a/README.md +++ b/README.md @@ -281,6 +281,11 @@ func main() { router.GET("/user/groups", func(c *gin.Context) { c.String(http.StatusOK, "The available groups are [...]") }) + + // This handler will match when none of the above routes match + router.GET("/*action", func(c *gin.Context) { + c.String(http.StatusOK, "Unknown Action") + }) router.Run(":8080") } @@ -1271,11 +1276,12 @@ func main() { ```go func main() { router := gin.Default() + router.StaticFS("/", http.Dir("dist")) router.Static("/assets", "./assets") router.StaticFS("/more_static", http.Dir("my_file_system")) router.StaticFile("/favicon.ico", "./resources/favicon.ico") router.StaticFileFS("/more_favicon.ico", "more_favicon.ico", http.Dir("my_file_system")) - + // Listen and serve on 0.0.0.0:8080 router.Run(":8080") } diff --git a/gin_integration_test.go b/gin_integration_test.go index b0532a25d1..e87846d699 100644 --- a/gin_integration_test.go +++ b/gin_integration_test.go @@ -414,9 +414,14 @@ func testGetRequestHandler(t *testing.T, h http.Handler, url string) { func TestTreeRunDynamicRouting(t *testing.T) { router := New() + router.GET("/", func(c *Context) { c.String(http.StatusOK, "home") }) router.GET("/aa/*xx", func(c *Context) { c.String(http.StatusOK, "/aa/*xx") }) + router.GET("/ab/aa", func(c *Context) { c.String(http.StatusOK, "/ab/aa") }) router.GET("/ab/*xx", func(c *Context) { c.String(http.StatusOK, "/ab/*xx") }) - router.GET("/", func(c *Context) { c.String(http.StatusOK, "home") }) + router.GET("/ab/zz", func(c *Context) { c.String(http.StatusOK, "/ab/zz") }) + router.GET("/abc/def", func(c *Context) { c.String(http.StatusOK, "/abc/def") }) + router.GET("/abc/d/:ee/*all", func(c *Context) { c.String(http.StatusOK, "/abc/d/:ee/*all") }) + router.GET("/abc/de/*all", func(c *Context) { c.String(http.StatusOK, "/abc/de/*all") }) router.GET("/:cc", func(c *Context) { c.String(http.StatusOK, "/:cc") }) router.GET("/c1/:dd/e", func(c *Context) { c.String(http.StatusOK, "/c1/:dd/e") }) router.GET("/c1/:dd/e1", func(c *Context) { c.String(http.StatusOK, "/c1/:dd/e1") }) @@ -429,6 +434,7 @@ func TestTreeRunDynamicRouting(t *testing.T) { router.GET("/:cc/:dd/:ee/:ff/gg", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/:ee/:ff/gg") }) router.GET("/:cc/:dd/:ee/:ff/:gg/hh", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/:ee/:ff/:gg/hh") }) router.GET("/get/test/abc/", func(c *Context) { c.String(http.StatusOK, "/get/test/abc/") }) + router.GET("/get/:param/*all", func(c *Context) { c.String(http.StatusOK, "/get/:param/*all") }) router.GET("/get/:param/abc/", func(c *Context) { c.String(http.StatusOK, "/get/:param/abc/") }) router.GET("/something/:paramname/thirdthing", func(c *Context) { c.String(http.StatusOK, "/something/:paramname/thirdthing") }) router.GET("/something/secondthing/test", func(c *Context) { c.String(http.StatusOK, "/something/secondthing/test") }) @@ -457,7 +463,13 @@ func TestTreeRunDynamicRouting(t *testing.T) { testRequest(t, ts.URL+"/", "", "home") testRequest(t, ts.URL+"/aa/aa", "", "/aa/*xx") + testRequest(t, ts.URL+"/ab/aa", "", "/ab/aa") + testRequest(t, ts.URL+"/ab/zz", "", "/ab/zz") testRequest(t, ts.URL+"/ab/ab", "", "/ab/*xx") + testRequest(t, ts.URL+"/ab/aab", "", "/ab/*xx") + testRequest(t, ts.URL+"/ab/", "", "/ab/*xx") + testRequest(t, ts.URL+"/abc/d/e", "", "/abc/d/:ee/*all") + testRequest(t, ts.URL+"/abc/d//", "", "/abc/d/:ee/*all") testRequest(t, ts.URL+"/all", "", "/:cc") testRequest(t, ts.URL+"/all/cc", "", "/:cc/cc") testRequest(t, ts.URL+"/a/cc", "", "/:cc/cc") @@ -496,6 +508,10 @@ func TestTreeRunDynamicRouting(t *testing.T) { testRequest(t, ts.URL+"/get/t/abc/", "", "/get/:param/abc/") testRequest(t, ts.URL+"/get/aa/abc/", "", "/get/:param/abc/") testRequest(t, ts.URL+"/get/abas/abc/", "", "/get/:param/abc/") + testRequest(t, ts.URL+"/get/testt/abc", "", "/get/:param/abc/") + testRequest(t, ts.URL+"/get/test/", "", "/get/:param") + testRequest(t, ts.URL+"/get/test/abcde/", "", "/get/:param/*all") + testRequest(t, ts.URL+"/get/test//abc", "", "/get/:param/*all") testRequest(t, ts.URL+"/something/secondthing/test", "", "/something/secondthing/test") testRequest(t, ts.URL+"/something/secondthingaaaa/thirdthing", "", "/something/:paramname/thirdthing") testRequest(t, ts.URL+"/something/abcdad/thirdthing", "", "/something/:paramname/thirdthing") @@ -547,6 +563,9 @@ func TestTreeRunDynamicRouting(t *testing.T) { testRequest(t, ts.URL+"/get/abc/123abf/testss", "", "/get/abc/123abf/:param") testRequest(t, ts.URL+"/get/abc/123abfff/te", "", "/get/abc/123abfff/:param") // 404 not found + testRequest(t, ts.URL+"/abc/deX", "404 Not Found") + testRequest(t, ts.URL+"/abc/defX", "404 Not Found") + testRequest(t, ts.URL+"/abc/d/", "404 Not Found") testRequest(t, ts.URL+"/c/d/e", "404 Not Found") testRequest(t, ts.URL+"/c/d/e1", "404 Not Found") testRequest(t, ts.URL+"/c/d/eee", "404 Not Found") diff --git a/tree.go b/tree.go index 956bf4dd3c..7a64955892 100644 --- a/tree.go +++ b/tree.go @@ -6,6 +6,7 @@ package gin import ( "bytes" + "fmt" "net/url" "strings" "unicode" @@ -146,6 +147,19 @@ func (n *node) incrementChildPrio(pos int) int { return newPos } +func (n *node) panicWildcardConflict(fullPath, path string) { + wildChild := n.children[len(n.children)-1] + pathSeg := path + if wildChild.nType != catchAll { + pathSeg = strings.SplitN(pathSeg, "/", 2)[0] + } + prefix := fullPath[:strings.Index(fullPath, pathSeg)] + wildChild.path + panic(fmt.Sprintf( + "'%s' in new path '%s' conflicts with existing wildcard '%s' in existing prefix '%s'", + pathSeg, fullPath, wildChild.path, prefix, + )) +} + // addRoute adds a node with the given handle to the path. // Not concurrency-safe! func (n *node) addRoute(path string, handlers HandlersChain) { @@ -167,6 +181,9 @@ walk: // This also implies that the common prefix contains no ':' or '*' // since the existing key can't contain those chars. i := longestCommonPrefix(path, n.path) + if i > 0 && i < len(path) && path[i-1:i+1] == "/*" { + i-- + } // Split edge if i < len(n.path) { @@ -193,27 +210,37 @@ walk: if i < len(path) { path = path[i:] c := path[0] - - // '/' after param - if n.nType == param && c == '/' && len(n.children) == 1 { - parentFullPathIndex += len(n.path) - n = n.children[0] - n.priority++ - continue walk - } - - // Check if a child with the next path byte exists - for i, max := 0, len(n.indices); i < max; i++ { - if c == n.indices[i] { - parentFullPathIndex += len(n.path) - i = n.incrementChildPrio(i) - n = n.children[i] - continue walk + isCatchAll := strings.HasPrefix(path, "/*") + + if len(n.children) > 0 { + child := n.children[len(n.children)-1] + if child.nType == param && c == ':' { + if strings.HasPrefix(path, child.path) && (len(path) == len(child.path) || path[len(child.path)] == '/') { + child.priority++ + n = child + continue walk + } + n.panicWildcardConflict(fullPath, path) + } + if child.nType == catchAll && (isCatchAll || strings.HasPrefix(path, "/:")) { + n.panicWildcardConflict(fullPath, path) + } + if isCatchAll && child.path == "/" && child.wildChild && child.children[len(child.children)-1].nType == param { + n.panicWildcardConflict(fullPath, path) } } - // Otherwise insert it - if c != ':' && c != '*' && n.nType != catchAll { + if c != ':' && !isCatchAll { + // Check if a child with the next path byte exists + for i, max := 0, len(n.indices); i < max; i++ { + if c == n.indices[i] { + parentFullPathIndex += len(n.path) + i = n.incrementChildPrio(i) + n = n.children[i] + continue walk + } + } + // Otherwise insert it // []byte for proper unicode char conversion, see #65 n.indices += bytesconv.BytesToString([]byte{c}) child := &node{ @@ -222,31 +249,6 @@ walk: n.addChild(child) n.incrementChildPrio(len(n.indices) - 1) 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) @@ -328,13 +330,15 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain) // will be another subpath starting with '/' if len(wildcard) < len(path) { path = path[len(wildcard):] - - child := &node{ - priority: 1, - fullPath: fullPath, + if !strings.HasPrefix(path, "/*") { + child := &node{ + priority: 1, + fullPath: fullPath, + } + n.addChild(child) + n.indices = "/" + n = child } - n.addChild(child) - n = child continue } @@ -363,29 +367,16 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain) panic("no / before catch-all in path '" + fullPath + "'") } - n.path = path[:i] - - // First node: catchAll node with empty path child := &node{ - wildChild: true, - nType: catchAll, - fullPath: fullPath, - } - - n.addChild(child) - n.indices = string('/') - n = child - n.priority++ - - // second node: node holding the variable - child = &node{ path: path[i:], nType: catchAll, handlers: handlers, priority: 1, fullPath: fullPath, } - n.children = []*node{child} + n.addChild(child) + n.path += path[:i] + n.wildChild = true return } @@ -417,177 +408,178 @@ type skippedNode struct { // given path. func (n *node) getValue(path string, params *Params, skippedNodes *[]skippedNode, unescape bool) (value nodeValue) { var globalParamsCount int16 + parentHasHandlers := false + + rollbackSkipped := func() bool { + if l := len(*skippedNodes); l > 0 { + skippedNode := (*skippedNodes)[l-1] + *skippedNodes = (*skippedNodes)[:l-1] + path = skippedNode.path + n = skippedNode.node + if value.params != nil { + *value.params = (*value.params)[:skippedNode.paramsCount] + } + globalParamsCount = skippedNode.paramsCount + return true + } + return false + } walk: // Outer loop for walking the tree for { prefix := n.path - if len(path) > len(prefix) { - if path[:len(prefix)] == prefix { - path = path[len(prefix):] + // Search in children + if len(path) > len(prefix) && path[:len(prefix)] == prefix || n.nType == param { + if n.nType == param { + prefix = "" + } + path = path[len(prefix):] - // Try all the non-wildcard children first by matching the indices - idxc := path[0] - for i, c := range []byte(n.indices) { - if c == idxc { - // strings.HasPrefix(n.children[len(n.children)-1].path, ":") == n.wildChild - if n.wildChild { - index := len(*skippedNodes) - *skippedNodes = (*skippedNodes)[:index+1] - (*skippedNodes)[index] = skippedNode{ - path: prefix + path, - node: &node{ - path: n.path, - wildChild: n.wildChild, - nType: n.nType, - priority: n.priority, - children: n.children, - handlers: n.handlers, - fullPath: n.fullPath, - }, - paramsCount: globalParamsCount, - } + // Try all the non-wildcard children first by matching the indices + idxc := path[0] + for i, c := range []byte(n.indices) { + if c == idxc { + parentHasHandlers = len(n.handlers) > 0 + if n.wildChild { + index := len(*skippedNodes) + *skippedNodes = (*skippedNodes)[:index+1] + (*skippedNodes)[index] = skippedNode{ + path: prefix + path, + node: &node{ + path: n.path, + wildChild: n.wildChild, + nType: n.nType, + priority: n.priority, + children: n.children, + handlers: n.handlers, + fullPath: n.fullPath, + }, + paramsCount: globalParamsCount, } - - n = n.children[i] - continue walk } - } - if !n.wildChild { - // If the path at the end of the loop is not equal to '/' and the current node has no child nodes - // the current node needs to roll back to last valid skippedNode - if path != "/" { - for l := len(*skippedNodes); l > 0; { - skippedNode := (*skippedNodes)[l-1] - *skippedNodes = (*skippedNodes)[:l-1] - if strings.HasSuffix(skippedNode.path, path) { - path = skippedNode.path - n = skippedNode.node - if value.params != nil { - *value.params = (*value.params)[:skippedNode.paramsCount] - } - globalParamsCount = skippedNode.paramsCount - continue walk - } - } - } + n = n.children[i] + continue walk + } + } - // Nothing found. - // We can recommend to redirect to the same URL without a - // trailing slash if a leaf exists for that path. - value.tsr = path == "/" && n.handlers != nil - return + if !n.wildChild { + // If the path at the end of the loop is not equal to '/' and the current node has no child nodes + // the current node needs to roll back to last valid skippedNode + if path != "/" && rollbackSkipped() { + continue walk } - // Handle wildcard child, which is always at the end of the array - n = n.children[len(n.children)-1] - globalParamsCount++ + // Nothing found. + // We can recommend to redirect to the same URL without a + // trailing slash if a leaf exists for that path. + value.tsr = path == "/" && n.handlers != nil + return + } - switch n.nType { - case param: - // fix truncate the parameter - // tree_test.go line: 204 + // Handle wildcard child, which is always at the end of the array + n = n.children[len(n.children)-1] + globalParamsCount++ - // Find param end (either '/' or path end) - end := 0 - for end < len(path) && path[end] != '/' { - end++ - } + switch n.nType { + case param: + // fix truncate the parameter + // tree_test.go line: 204 - // Save param value - if params != nil && cap(*params) > 0 { - if value.params == nil { - value.params = params - } - // Expand slice within preallocated capacity - i := len(*value.params) - *value.params = (*value.params)[:i+1] - val := path[:end] - if unescape { - if v, err := url.QueryUnescape(val); err == nil { - val = v - } - } - (*value.params)[i] = Param{ - Key: n.path[1:], - Value: val, + // Find param end (either '/' or path end) + end := 0 + for end < len(path) && path[end] != '/' { + end++ + } + + // Save param value + if params != nil && cap(*params) > 0 { + if value.params == nil { + value.params = params + } + // Expand slice within preallocated capacity + i := len(*value.params) + *value.params = (*value.params)[:i+1] + val := path[:end] + if unescape { + if v, err := url.QueryUnescape(val); err == nil { + val = v } } + (*value.params)[i] = Param{ + Key: n.path[1:], + Value: val, + } + } - // we need to go deeper! - if end < len(path) { - if len(n.children) > 0 { - path = path[end:] + // we need to go deeper! + if end < len(path) { + if len(n.children) > 0 { + path = path[end:] + parentHasHandlers = len(n.handlers) > 0 + if n.children[len(n.children)-1].nType != catchAll { n = n.children[0] - continue walk } - - // ... but we can't - value.tsr = len(path) == end+1 - return + continue walk } - if value.handlers = n.handlers; value.handlers != nil { - value.fullPath = n.fullPath - return - } - if len(n.children) == 1 { - // No handle found. Check if a handle for this path + a - // trailing slash exists for TSR recommendation - n = n.children[0] - value.tsr = (n.path == "/" && n.handlers != nil) || (n.path == "" && n.indices == "/") - } + // ... but we can't + value.tsr = len(path) == end+1 return + } - case catchAll: - // Save param value - if params != nil { - if value.params == nil { - value.params = params - } - // Expand slice within preallocated capacity - i := len(*value.params) - *value.params = (*value.params)[:i+1] - val := path - if unescape { - if v, err := url.QueryUnescape(path); err == nil { - val = v - } - } - (*value.params)[i] = Param{ - Key: n.path[2:], - Value: val, - } - } - - value.handlers = n.handlers + if value.handlers = n.handlers; value.handlers != nil { value.fullPath = n.fullPath return + } + if len(n.children) == 1 { + // No handle found. Check if a handle for this path + a + // trailing slash exists for TSR recommendation + n = n.children[0] + value.tsr = (n.path == "/" && n.handlers != nil) || n.nType == catchAll + } + return - default: - panic("invalid node type") + case catchAll: + if path[0] != '/' { + if rollbackSkipped() { + continue walk + } + return + } + // Save param value + if params != nil { + if value.params == nil { + value.params = params + } + // Expand slice within preallocated capacity + i := len(*value.params) + *value.params = (*value.params)[:i+1] + val := path + if unescape { + if v, err := url.QueryUnescape(path); err == nil { + val = v + } + } + (*value.params)[i] = Param{ + Key: n.path[2:], + Value: val, + } } + value.handlers = n.handlers + value.fullPath = n.fullPath + return + + default: + panic("invalid node type") } } if path == prefix { // If the current path does not equal '/' and the node does not have a registered handle and the most recently matched node has a child node // the current node needs to roll back to last valid skippedNode - if n.handlers == nil && path != "/" { - for l := len(*skippedNodes); l > 0; { - skippedNode := (*skippedNodes)[l-1] - *skippedNodes = (*skippedNodes)[:l-1] - if strings.HasSuffix(skippedNode.path, path) { - path = skippedNode.path - n = skippedNode.node - if value.params != nil { - *value.params = (*value.params)[:skippedNode.paramsCount] - } - globalParamsCount = skippedNode.paramsCount - continue walk - } - } - // n = latestNode.children[len(latestNode.children)-1] + if n.handlers == nil && rollbackSkipped() { + continue walk } // We should have reached the node containing the handle. // Check if this node has a handle registered. @@ -609,36 +601,26 @@ walk: // Outer loop for walking the tree for i, c := range []byte(n.indices) { if c == '/' { n = n.children[i] - value.tsr = (len(n.path) == 1 && n.handlers != nil) || - (n.nType == catchAll && n.children[0].handlers != nil) + value.tsr = len(n.path) == 1 && n.handlers != nil return } } + value.tsr = n.children[len(n.children)-1].nType == catchAll return } - // Nothing found. We can recommend to redirect to the same URL with an - // extra trailing slash if a leaf exists for that path - value.tsr = path == "/" || - (len(prefix) == len(path)+1 && prefix[len(path)] == '/' && - path == prefix[:len(prefix)-1] && n.handlers != nil) + // Nothing found. Look for trailing slash recommendation + value.tsr = + // URL of parent node has one less trailing slash than path + path == "/" && parentHasHandlers || + // URL of current node has one more trailing slash than path + len(prefix) == len(path)+1 && prefix[len(path)] == '/' && + path == prefix[:len(prefix)-1] && n.handlers != nil // roll back to last valid skippedNode - if !value.tsr && path != "/" { - for l := len(*skippedNodes); l > 0; { - skippedNode := (*skippedNodes)[l-1] - *skippedNodes = (*skippedNodes)[:l-1] - if strings.HasSuffix(skippedNode.path, path) { - path = skippedNode.path - n = skippedNode.node - if value.params != nil { - *value.params = (*value.params)[:skippedNode.paramsCount] - } - globalParamsCount = skippedNode.paramsCount - continue walk - } - } + if !value.tsr && rollbackSkipped() { + continue walk } return diff --git a/tree_test.go b/tree_test.go index 085b58037b..5de0298f9d 100644 --- a/tree_test.go +++ b/tree_test.go @@ -160,7 +160,12 @@ func TestTreeWildcard(t *testing.T) { "/info/:user/project/:project", "/info/:user/project/golang", "/aa/*xx", + "/ab/aa", "/ab/*xx", + "/ab/zz", + "/abc/def", + "/abc/d/:ee/*all", + "/abc/de/*all", "/:cc", "/c1/:dd/e", "/c1/:dd/e1", @@ -170,6 +175,7 @@ func TestTreeWildcard(t *testing.T) { "/:cc/:dd/:ee/:ff/gg", "/:cc/:dd/:ee/:ff/:gg/hh", "/get/test/abc/", + "/get/:param/*all", "/get/:param/abc/", "/something/:paramname/thirdthing", "/something/secondthing/test", @@ -225,7 +231,16 @@ func TestTreeWildcard(t *testing.T) { {"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}}, {"/info/gordon/project/golang", false, "/info/:user/project/golang", Params{Param{Key: "user", Value: "gordon"}}}, {"/aa/aa", false, "/aa/*xx", Params{Param{Key: "xx", Value: "/aa"}}}, + {"/ab/aa", false, "/ab/aa", nil}, + {"/ab/zz", false, "/ab/zz", nil}, {"/ab/ab", false, "/ab/*xx", Params{Param{Key: "xx", Value: "/ab"}}}, + {"/ab/aab", false, "/ab/*xx", Params{Param{Key: "xx", Value: "/aab"}}}, + {"/ab/", false, "/ab/*xx", Params{Param{Key: "xx", Value: "/"}}}, + {"/abc/d/e", true, "/abc/d/:ee/*all", Params{Param{Key: "ee", Value: "e"}}}, + {"/abc/d//", false, "/abc/d/:ee/*all", Params{Param{Key: "ee", Value: ""}, Param{Key: "all", Value: "/"}}}, + {"/abc/deX", true, "", Params{Param{Key: "cc", Value: "abc"}, Param{Key: "dd", Value: "deX"}}}, + {"/abc/defX", true, "", Params{Param{Key: "cc", Value: "abc"}, Param{Key: "dd", Value: "defX"}}}, + {"/abc/d/", true, "", Params{Param{Key: "cc", Value: "abc"}, Param{Key: "dd", Value: "d"}}}, {"/a", false, "/:cc", Params{Param{Key: "cc", Value: "a"}}}, // * Error with argument being intercepted // new PR handle (/all /all/cc /a/cc) @@ -261,6 +276,10 @@ func TestTreeWildcard(t *testing.T) { {"/get/t/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "t"}}}, {"/get/aa/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "aa"}}}, {"/get/abas/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "abas"}}}, + {"/get/testt/abc", true, "/get/:param/abc/", Params{Param{Key: "param", Value: "testt"}}}, + {"/get/test/", true, "/get/:param", Params{Param{Key: "param", Value: "test"}}}, + {"/get/test/abcde/", false, "/get/:param/*all", Params{Param{Key: "param", Value: "test"}, Param{Key: "all", Value: "/abcde/"}}}, + {"/get/test//abc", false, "/get/:param/*all", Params{Param{Key: "param", Value: "test"}, Param{Key: "all", Value: "//abc"}}}, {"/something/secondthing/test", false, "/something/secondthing/test", nil}, {"/something/abcdad/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "abcdad"}}}, {"/something/secondthingaaaa/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "secondthingaaaa"}}}, @@ -402,12 +421,12 @@ func TestTreeWildcardConflict(t *testing.T) { {"/cmd/:tool/:badsub/details", true}, {"/src/*filepath", false}, {"/src/:file", true}, - {"/src/static.json", true}, + {"/src/static.json", false}, {"/src/*filepathx", true}, - {"/src/", true}, - {"/src/foo/bar", true}, + {"/src/", false}, + {"/src/foo/bar", false}, {"/src1/", false}, - {"/src1/*filepath", true}, + {"/src1/*filepath", false}, {"/src2*filepath", true}, {"/src2/*filepath", false}, {"/search/:query", false}, @@ -436,7 +455,7 @@ func TestTreeChildConflict(t *testing.T) { {"/cmd/:tool/misc", false}, {"/cmd/:tool/:othersub", true}, {"/src/AUTHORS", false}, - {"/src/*filepath", true}, + {"/src/*filepath", false}, {"/user_x", false}, {"/user_:name", false}, {"/id/:id", false}, @@ -474,7 +493,7 @@ func TestTreeDuplicatePath(t *testing.T) { } } - //printChildren(tree, "") + // printChildren(tree, "") checkRequests(t, tree, testRequests{ {"/", false, "/", nil}, @@ -515,12 +534,53 @@ func TestTreeCatchAllConflict(t *testing.T) { testRoutes(t, routes) } -func TestTreeCatchAllConflictRoot(t *testing.T) { - routes := []testRoute{ - {"/", false}, - {"/*filepath", true}, +func TestTreeCatchAllRoot(t *testing.T) { + tree := &node{} + routes := []string{ + "/index.html", + "/*all", } - testRoutes(t, routes) + for _, route := range routes { + tree.addRoute(route, fakeHandler(route)) + } + checkRequests(t, tree, testRequests{ + {"/", false, "/*all", Params{Param{"all", "/"}}}, + {"/index.html", false, "/index.html", nil}, + {"/users", false, "/*all", Params{Param{"all", "/users"}}}, + }, true) + + // More tests + tree = &node{} + routes = []string{ + "/", + "/index.html", + "/*all", + } + for _, route := range routes { + tree.addRoute(route, fakeHandler(route)) + } + checkRequests(t, tree, testRequests{ + {"/", false, "/", nil}, + {"/index.html", false, "/index.html", nil}, + {"/users", false, "/*all", Params{Param{"all", "/users"}}}, + }, true) +} + +func TestCatchAllPathNoLeadingSlash(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/abc/defg/:param", + "/abc/def/*all", + } + for _, route := range routes { + tree.addRoute(route, fakeHandler(route)) + } + + checkRequests(t, tree, testRequests{ + {"/abc/defX", true, "", nil}, + {"/abc/def/X", false, "/abc/def/*all", Params{Param{"all", "/X"}}}, + }) } func TestTreeCatchMaxParams(t *testing.T) { @@ -568,11 +628,13 @@ func TestTreeTrailingSlashRedirect(t *testing.T) { "/b/", "/search/:query", "/cmd/:tool/", + "/src/", "/src/*filepath", "/x", "/x/y", "/y/", "/y/z", + "/z/*all", "/0/:id", "/0/:id/1", "/1/:id/", @@ -614,6 +676,7 @@ func TestTreeTrailingSlashRedirect(t *testing.T) { "/src", "/x/", "/y", + "/z", "/0/go/", "/1/go", "/a", @@ -891,9 +954,9 @@ func TestTreeWildcardConflictEx(t *testing.T) { existPath string existSegPath string }{ - {"/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", "/:foo", `/who/are/\*you`, `/\*you`}, + {"/who/are/:foo/", "/:foo/", `/who/are/\*you`, `/\*you`}, + {"/who/are/*foo", `/\*foo`, `/who/are/\*you`, `/\*you`}, {"/con:nection", ":nection", `/con:tact`, `:tact`}, }