diff --git a/.gitignore b/.gitignore index bdd50c95cf..2ac6ea8cce 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ coverage.out count.out test profile.out -tmp.out +tmp.out \ No newline at end of file diff --git a/gin_integration_test.go b/gin_integration_test.go index 2eb2d52bf8..b7846be73c 100644 --- a/gin_integration_test.go +++ b/gin_integration_test.go @@ -22,7 +22,14 @@ import ( "github.com/stretchr/testify/assert" ) -func testRequest(t *testing.T, url string) { +// params[0]=url example:http://127.0.0.1:8080/index (cannot be empty) +// params[1]=response body (custom compare content) +func testRequest(t *testing.T, params ...string) { + + if len(params) == 0 { + t.Fatal("url cannot be empty") + } + tr := &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, @@ -30,13 +37,18 @@ func testRequest(t *testing.T, url string) { } client := &http.Client{Transport: tr} - resp, err := client.Get(url) + resp, err := client.Get(params[0]) assert.NoError(t, err) defer resp.Body.Close() body, ioerr := ioutil.ReadAll(resp.Body) assert.NoError(t, ioerr) - assert.Equal(t, "it worked", string(body), "resp body should match") + + var expected = "it worked" + if len(params) > 1 { + expected = params[1] + } + assert.Equal(t, expected, string(body), "resp body should match") assert.Equal(t, "200 OK", resp.Status, "should get a 200") } @@ -373,3 +385,31 @@ func testGetRequestHandler(t *testing.T, h http.Handler, url string) { assert.Equal(t, "it worked", w.Body.String(), "resp body should match") assert.Equal(t, 200, w.Code, "should get a 200") } + +func TestRunDynamicRouting(t *testing.T) { + router := New() + router.GET("/aa/*xx", func(c *Context) { c.String(http.StatusOK, "/aa/*xx") }) + router.GET("/ab/*xx", func(c *Context) { c.String(http.StatusOK, "/ab/*xx") }) + router.GET("/:cc", func(c *Context) { c.String(http.StatusOK, "/:cc") }) + router.GET("/:cc/cc", func(c *Context) { c.String(http.StatusOK, "/:cc/cc") }) + router.GET("/get/test/abc/", func(c *Context) { c.String(http.StatusOK, "/get/test/abc/") }) + router.GET("/get/:param/abc/", func(c *Context) { c.String(http.StatusOK, "/get/:param/abc/") }) + + ts := httptest.NewServer(router) + defer ts.Close() + + testRequest(t, ts.URL+"/aa/aa", "/aa/*xx") + testRequest(t, ts.URL+"/ab/ab", "/ab/*xx") + testRequest(t, ts.URL+"/all", "/:cc") + testRequest(t, ts.URL+"/all/cc", "/:cc/cc") + testRequest(t, ts.URL+"/a/cc", "/:cc/cc") + testRequest(t, ts.URL+"/a", "/:cc") + testRequest(t, ts.URL+"/get/test/abc/", "/get/test/abc/") + testRequest(t, ts.URL+"/get/te/abc/", "/get/:param/abc/") + testRequest(t, ts.URL+"/get/xx/abc/", "/get/:param/abc/") + testRequest(t, ts.URL+"/get/tt/abc/", "/get/:param/abc/") + testRequest(t, ts.URL+"/get/a/abc/", "/get/:param/abc/") + 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/") +} diff --git a/tree.go b/tree.go index 4b5656fb08..6d70510b5b 100644 --- a/tree.go +++ b/tree.go @@ -6,6 +6,7 @@ package gin import ( "bytes" + "math" "net/url" "strings" "unicode" @@ -405,11 +406,34 @@ type nodeValue struct { // made if a handle exists with an extra (without the) trailing slash for the // given path. func (n *node) getValue(path string, params *Params, unescape bool) (value nodeValue) { - var skipped *skip + // path: /abc/123/def + // level 1 router:abc + // level 2 router:123 + // level 3 router:def + var ( + skipped *skip + latestNode = &(*n) // not found `level 1 router` use latestNode + + // match '/' count + // default root node n.path is '/' matchNum++ + // matchNum < 2: `level 1 router` not found,the current node needs to be equal to latestNode + // matchNum >= 2: `level (2 or 3 or 4 or ...) router`: Normal handling + matchNum int // each match will accumulate + ) + // if path = '/', no need to look for router + if len(path) == 1 { + matchNum = math.MaxUint8 / 2 + } walk: // Outer loop for walking the tree for { prefix := n.path + + // match '/', If this condition is matched, the next route is found + if strings.HasSuffix(n.path, "/") || strings.Contains(n.fullPath, ":") || n.path == "" { + matchNum++ + } + if len(path) > len(prefix) { if path[:len(prefix)] == prefix { path = path[len(prefix):] @@ -437,6 +461,10 @@ walk: // Outer loop for walking the tree continue walk } } + // level 1 router not found,the current node needs to be equal to latestNode + if matchNum < 2 { + n = latestNode + } // If there is no wildcard pattern, recommend a redirection if !n.wildChild { @@ -483,6 +511,16 @@ walk: // Outer loop for walking the tree if len(n.children) > 0 { path = path[end:] n = n.children[0] + // next node,the latestNode needs to be equal to currentNode and handle next router + latestNode = n + // not found router in (level 1 router and handle next node),skipped cannot execute + // example: + // * /:cc/cc + // call /a/cc expectations:match/200 Actual:match/200 + // call /a/dd expectations:unmatch/404 Actual: panic + // call /addr/dd/aa expectations:unmatch/404 Actual: panic + // skipped: It can only be executed if the secondary route is not found + skipped = nil continue walk } @@ -533,8 +571,12 @@ walk: // Outer loop for walking the tree } } } - + // path = n.path if path == prefix { + // level 1 router not found and latestNode.wildChild is ture + if matchNum < 2 && latestNode.wildChild { + n = latestNode.children[len(latestNode.children)-1] + } // We should have reached the node containing the handle. // Check if this node has a handle registered. if value.handlers = n.handlers; value.handlers != nil { diff --git a/tree_test.go b/tree_test.go index 7459317fa3..ad8f214663 100644 --- a/tree_test.go +++ b/tree_test.go @@ -154,6 +154,12 @@ func TestTreeWildcard(t *testing.T) { "/info/:user/public", "/info/:user/project/:project", "/info/:user/project/golang", + "/aa/*xx", + "/ab/*xx", + "/:cc", + "/:cc/cc", + "/get/test/abc/", + "/get/:param/abc/", } for _, route := range routes { tree.addRoute(route, fakeHandler(route)) @@ -186,6 +192,22 @@ func TestTreeWildcard(t *testing.T) { {"/info/gordon/public", false, "/info/:user/public", Params{Param{Key: "user", Value: "gordon"}}}, {"/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/ab", false, "/ab/*xx", Params{Param{Key: "xx", Value: "/ab"}}}, + {"/a", false, "/:cc", Params{Param{Key: "cc", Value: "a"}}}, + // * level 1 router match param will be Intercept first + // new PR handle (/all /all/cc /a/cc) + {"/all", false, "/:cc", Params{Param{Key: "cc", Value: "ll"}}}, + {"/all/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "ll"}}}, + {"/a/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: ""}}}, + {"/get/test/abc/", false, "/get/test/abc/", nil}, + {"/get/te/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "te"}}}, + {"/get/xx/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "xx"}}}, + {"/get/tt/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "tt"}}}, + {"/get/a/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "a"}}}, + {"/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"}}}, }) checkPriorities(t, tree) @@ -565,8 +587,8 @@ func TestTreeFindCaseInsensitivePath(t *testing.T) { "/u/öpfêl", "/v/Äpfêl/", "/v/Öpfêl", - "/w/♬", // 3 byte - "/w/♭/", // 3 byte, last byte differs + "/w/♬", // 3 byte + "/w/♭/", // 3 byte, last byte differs "/w/𠜎", // 4 byte "/w/𠜏/", // 4 byte longPath,