handle level 1 router

add more comments

update comment

add example

fix benchmark not found

add comment and update test method
gin_integration_test.go#L407

update comment and lastedNode directly assign current node

optimize code

Optimize the search next router logic

optimize code

Adjust the matching rules

Adjust the matching order

update condition code
This commit is contained in:
qm012 2021-06-27 20:36:18 +08:00
parent 690aa2b1b9
commit 3fc08c9daf
4 changed files with 129 additions and 33 deletions

2
.gitignore vendored
View File

@ -4,4 +4,4 @@ coverage.out
count.out
test
profile.out
tmp.out
tmp.out

View File

@ -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,33 @@ 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("/", func(c *Context) { c.String(http.StatusOK, "home") })
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+"/", "home")
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/")
}

86
tree.go
View File

@ -118,11 +118,6 @@ type node struct {
fullPath string
}
type skip struct {
path string
paramNode *node
}
// Increments priority of the given child and reorders if necessary
func (n *node) incrementChildPrio(pos int) int {
cs := n.children
@ -405,7 +400,23 @@ 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 (
skippedPath string
latestNode = n // not found `level 2 router` use latestNode
// match '/' count
// matchNum < 1: `level 2 router` not found,the current node needs to be equal to latestNode
// matchNum >= 1: `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 = 1
}
walk: // Outer loop for walking the tree
for {
@ -418,32 +429,41 @@ walk: // Outer loop for walking the tree
idxc := path[0]
for i, c := range []byte(n.indices) {
if c == idxc {
if strings.HasPrefix(n.children[len(n.children)-1].path, ":") {
skipped = &skip{
path: prefix + path,
paramNode: &node{
path: n.path,
wildChild: n.wildChild,
nType: n.nType,
priority: n.priority,
children: n.children,
handlers: n.handlers,
fullPath: n.fullPath,
},
// strings.HasPrefix(n.children[len(n.children)-1].path, ":") == n.wildChild
if n.wildChild {
skippedPath = prefix + path
latestNode = &node{
path: n.path,
wildChild: n.wildChild,
nType: n.nType,
priority: n.priority,
children: n.children,
handlers: n.handlers,
fullPath: n.fullPath,
}
}
n = n.children[i]
// match '/', If this condition is matched, the next route is found
if len(n.fullPath) != 0 && n.wildChild {
matchNum++
}
continue walk
}
}
// level 2 router not found,the current node needs to be equal to latestNode
if matchNum < 1 {
n = latestNode
}
// If there is no wildcard pattern, recommend a redirection
if !n.wildChild {
// 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)
value.tsr = path == "/" && n.handlers != nil
return
}
@ -483,6 +503,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
// skippedPath: It can only be executed if the secondary route is not found
skippedPath = ""
continue walk
}
@ -533,8 +563,12 @@ walk: // Outer loop for walking the tree
}
}
}
// path = n.path
if path == prefix {
// level 2 router not found and latestNode.wildChild is ture
if matchNum < 1 && 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 {
@ -564,18 +598,16 @@ walk: // Outer loop for walking the tree
return
}
if path != "/" && skipped != nil && strings.HasSuffix(skipped.path, path) {
path = skipped.path
n = skipped.paramNode
skipped = nil
if path != "/" && strings.HasSuffix(skippedPath, path) {
path = skippedPath
n = latestNode
skippedPath = ""
continue walk
}
// 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)
value.tsr = true
return
}
}

View File

@ -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,