beego:router tree
Showing
2 changed files
with
424 additions
and
0 deletions
tree.go
0 → 100644
| 1 | package beego | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "path" | ||
| 5 | "regexp" | ||
| 6 | "strings" | ||
| 7 | |||
| 8 | "github.com/astaxie/beego/utils" | ||
| 9 | ) | ||
| 10 | |||
| 11 | type Tree struct { | ||
| 12 | //search fix route first | ||
| 13 | fixrouters map[string]*Tree | ||
| 14 | |||
| 15 | //if set, failure to match fixrouters search then search wildcard | ||
| 16 | wildcard *Tree | ||
| 17 | |||
| 18 | //if set, failure to match wildcard search | ||
| 19 | leaf *leafInfo | ||
| 20 | } | ||
| 21 | |||
| 22 | func NewTree() *Tree { | ||
| 23 | return &Tree{ | ||
| 24 | fixrouters: make(map[string]*Tree), | ||
| 25 | } | ||
| 26 | } | ||
| 27 | |||
| 28 | // call addseg function | ||
| 29 | func (t *Tree) AddRouter(pattern string, runObject interface{}) { | ||
| 30 | t.addseg(splitPath(pattern), runObject, nil, "") | ||
| 31 | } | ||
| 32 | |||
| 33 | // "/" | ||
| 34 | // "admin" -> | ||
| 35 | func (t *Tree) addseg(segments []string, route interface{}, wildcards []string, reg string) { | ||
| 36 | if len(segments) == 0 { | ||
| 37 | if reg != "" { | ||
| 38 | filterCards := []string{} | ||
| 39 | for _, v := range wildcards { | ||
| 40 | if v == ":" || v == "." { | ||
| 41 | continue | ||
| 42 | } | ||
| 43 | filterCards = append(filterCards, v) | ||
| 44 | } | ||
| 45 | t.leaf = &leafInfo{runObject: route, wildcards: wildcards, regexps: regexp.MustCompile("^" + reg + "$")} | ||
| 46 | } else { | ||
| 47 | t.leaf = &leafInfo{runObject: route, wildcards: wildcards} | ||
| 48 | } | ||
| 49 | |||
| 50 | } else { | ||
| 51 | seg := segments[0] | ||
| 52 | iswild, params, regexpStr := splitSegment(seg) | ||
| 53 | if iswild { | ||
| 54 | if t.wildcard == nil { | ||
| 55 | t.wildcard = NewTree() | ||
| 56 | } | ||
| 57 | t.wildcard.addseg(segments[1:], route, append(wildcards, params...), reg+regexpStr) | ||
| 58 | } else { | ||
| 59 | subTree, ok := t.fixrouters[seg] | ||
| 60 | if !ok { | ||
| 61 | subTree = NewTree() | ||
| 62 | t.fixrouters[seg] = subTree | ||
| 63 | } | ||
| 64 | subTree.addseg(segments[1:], route, wildcards, reg) | ||
| 65 | } | ||
| 66 | } | ||
| 67 | } | ||
| 68 | |||
| 69 | // match router to runObject & params | ||
| 70 | func (t *Tree) Match(pattern string) (runObject interface{}, params map[string]string) { | ||
| 71 | if len(pattern) == 0 || pattern[0] != '/' { | ||
| 72 | return nil, nil | ||
| 73 | } | ||
| 74 | |||
| 75 | return t.match(splitPath(pattern), nil) | ||
| 76 | } | ||
| 77 | |||
| 78 | func (t *Tree) match(segments []string, wildcardValues []string) (runObject interface{}, params map[string]string) { | ||
| 79 | // Handle leaf nodes: | ||
| 80 | if len(segments) == 0 { | ||
| 81 | if t.leaf != nil { | ||
| 82 | if ok, pa := t.leaf.match(wildcardValues); ok { | ||
| 83 | return t.leaf.runObject, pa | ||
| 84 | } | ||
| 85 | } | ||
| 86 | return nil, nil | ||
| 87 | } | ||
| 88 | |||
| 89 | var seg string | ||
| 90 | seg, segments = segments[0], segments[1:] | ||
| 91 | |||
| 92 | subTree, ok := t.fixrouters[seg] | ||
| 93 | if ok { | ||
| 94 | runObject, params = subTree.match(segments, wildcardValues) | ||
| 95 | } | ||
| 96 | if runObject == nil && t.wildcard != nil { | ||
| 97 | runObject, params = t.wildcard.match(segments, append(wildcardValues, seg)) | ||
| 98 | } | ||
| 99 | if runObject == nil { | ||
| 100 | if t.leaf != nil { | ||
| 101 | if ok, pa := t.leaf.match(append(wildcardValues, seg)); ok { | ||
| 102 | return t.leaf.runObject, pa | ||
| 103 | } | ||
| 104 | } | ||
| 105 | } | ||
| 106 | |||
| 107 | return runObject, params | ||
| 108 | } | ||
| 109 | |||
| 110 | type leafInfo struct { | ||
| 111 | // names of wildcards that lead to this leaf. eg, ["id" "name"] for the wildcard ":id" and ":name" | ||
| 112 | wildcards []string | ||
| 113 | |||
| 114 | // if the leaf is regexp | ||
| 115 | regexps *regexp.Regexp | ||
| 116 | |||
| 117 | runObject interface{} | ||
| 118 | } | ||
| 119 | |||
| 120 | func (leaf *leafInfo) match(wildcardValues []string) (ok bool, params map[string]string) { | ||
| 121 | if leaf.regexps == nil { | ||
| 122 | // has error | ||
| 123 | if len(wildcardValues) == 0 && len(leaf.wildcards) > 0 { | ||
| 124 | if utils.InSlice(":", leaf.wildcards) { | ||
| 125 | return true, nil | ||
| 126 | } | ||
| 127 | Error("bug of router") | ||
| 128 | return false, nil | ||
| 129 | } else if len(wildcardValues) == 0 { // static path | ||
| 130 | return true, nil | ||
| 131 | } | ||
| 132 | // match * | ||
| 133 | if len(leaf.wildcards) == 1 && leaf.wildcards[0] == ":splat" { | ||
| 134 | params = make(map[string]string) | ||
| 135 | params[":splat"] = path.Join(wildcardValues...) | ||
| 136 | return true, params | ||
| 137 | } | ||
| 138 | // match *.* | ||
| 139 | if len(leaf.wildcards) == 3 && leaf.wildcards[0] == "." { | ||
| 140 | params = make(map[string]string) | ||
| 141 | lastone := wildcardValues[len(wildcardValues)-1] | ||
| 142 | strs := strings.SplitN(lastone, ".", 2) | ||
| 143 | if len(strs) == 2 { | ||
| 144 | params[":ext"] = strs[1] | ||
| 145 | } else { | ||
| 146 | params[":ext"] = "" | ||
| 147 | } | ||
| 148 | params[":path"] = path.Join(wildcardValues[:len(wildcardValues)-1]...) + "/" + strs[0] | ||
| 149 | return true, params | ||
| 150 | } | ||
| 151 | // match :id | ||
| 152 | params = make(map[string]string) | ||
| 153 | j := 0 | ||
| 154 | for _, v := range leaf.wildcards { | ||
| 155 | if v == ":" { | ||
| 156 | continue | ||
| 157 | } | ||
| 158 | params[v] = wildcardValues[j] | ||
| 159 | j += 1 | ||
| 160 | } | ||
| 161 | if len(params) != len(wildcardValues) { | ||
| 162 | Error("bug of router") | ||
| 163 | return false, nil | ||
| 164 | } | ||
| 165 | return true, params | ||
| 166 | } | ||
| 167 | |||
| 168 | if !leaf.regexps.MatchString(strings.Join(wildcardValues, "")) { | ||
| 169 | return false, nil | ||
| 170 | } | ||
| 171 | params = make(map[string]string) | ||
| 172 | matches := leaf.regexps.FindStringSubmatch(strings.Join(wildcardValues, "")) | ||
| 173 | for i, match := range matches[1:] { | ||
| 174 | params[leaf.wildcards[i]] = match | ||
| 175 | } | ||
| 176 | return true, params | ||
| 177 | } | ||
| 178 | |||
| 179 | // "/" -> [] | ||
| 180 | // "/admin" -> ["admin"] | ||
| 181 | // "/admin/" -> ["admin"] | ||
| 182 | // "/admin/users" -> ["admin", "users"] | ||
| 183 | func splitPath(key string) []string { | ||
| 184 | elements := strings.Split(key, "/") | ||
| 185 | if elements[0] == "" { | ||
| 186 | elements = elements[1:] | ||
| 187 | } | ||
| 188 | if elements[len(elements)-1] == "" { | ||
| 189 | elements = elements[:len(elements)-1] | ||
| 190 | } | ||
| 191 | return elements | ||
| 192 | } | ||
| 193 | |||
| 194 | // "admin" -> false, nil, "" | ||
| 195 | // ":id" -> true, [:id], "" | ||
| 196 | // "?:id" -> true, [: id], "" : meaning can empty | ||
| 197 | // ":id:int" -> true, [:id], ([0-9]+) | ||
| 198 | // ":name:string" -> true, [:name], ([\w]+) | ||
| 199 | // ":id([0-9]+)" -> true, [:id], ([0-9]+) | ||
| 200 | // ":id([0-9]+)_:name" -> true, [:id :name], ([0-9]+)_(.+) | ||
| 201 | // "cms_:id_:page.html" -> true, [:id :page], cms_(.+)_(.+).html | ||
| 202 | // "*" -> true, [:splat], "" | ||
| 203 | // "*.*" -> true,[. :path :ext], "" . meaning separator | ||
| 204 | func splitSegment(key string) (bool, []string, string) { | ||
| 205 | if strings.HasPrefix(key, "*") { | ||
| 206 | if key == "*.*" { | ||
| 207 | return true, []string{".", ":path", ":ext"}, "" | ||
| 208 | } else { | ||
| 209 | return true, []string{":splat"}, "" | ||
| 210 | } | ||
| 211 | } | ||
| 212 | if strings.ContainsAny(key, ":") { | ||
| 213 | var paramsNum int | ||
| 214 | var out []rune | ||
| 215 | var start bool | ||
| 216 | var startexp bool | ||
| 217 | var param []rune | ||
| 218 | var expt []rune | ||
| 219 | var skipnum int | ||
| 220 | params := []string{} | ||
| 221 | reg := regexp.MustCompile(`[a-zA-Z0-9]+`) | ||
| 222 | for i, v := range key { | ||
| 223 | if skipnum > 0 { | ||
| 224 | skipnum -= 1 | ||
| 225 | continue | ||
| 226 | } | ||
| 227 | if start { | ||
| 228 | //:id:int and :name:string | ||
| 229 | if v == ':' { | ||
| 230 | if len(key) >= i+4 { | ||
| 231 | if key[i+1:i+4] == "int" { | ||
| 232 | out = append(out, []rune("([0-9]+)")...) | ||
| 233 | params = append(params, ":"+string(param)) | ||
| 234 | start = false | ||
| 235 | startexp = false | ||
| 236 | skipnum = 3 | ||
| 237 | param = make([]rune, 0) | ||
| 238 | paramsNum += 1 | ||
| 239 | continue | ||
| 240 | } | ||
| 241 | } | ||
| 242 | if len(key) >= i+7 { | ||
| 243 | if key[i+1:i+7] == "string" { | ||
| 244 | out = append(out, []rune(`([\w]+)`)...) | ||
| 245 | params = append(params, ":"+string(param)) | ||
| 246 | paramsNum += 1 | ||
| 247 | start = false | ||
| 248 | startexp = false | ||
| 249 | skipnum = 6 | ||
| 250 | param = make([]rune, 0) | ||
| 251 | continue | ||
| 252 | } | ||
| 253 | } | ||
| 254 | } | ||
| 255 | // params only support a-zA-Z0-9 | ||
| 256 | if reg.MatchString(string(v)) { | ||
| 257 | param = append(param, v) | ||
| 258 | continue | ||
| 259 | } | ||
| 260 | if v != '(' { | ||
| 261 | out = append(out, []rune(`(.+)`)...) | ||
| 262 | params = append(params, ":"+string(param)) | ||
| 263 | param = make([]rune, 0) | ||
| 264 | paramsNum += 1 | ||
| 265 | start = false | ||
| 266 | startexp = false | ||
| 267 | } | ||
| 268 | } | ||
| 269 | if startexp { | ||
| 270 | if v != ')' { | ||
| 271 | expt = append(expt, v) | ||
| 272 | continue | ||
| 273 | } | ||
| 274 | } | ||
| 275 | if v == ':' { | ||
| 276 | param = make([]rune, 0) | ||
| 277 | start = true | ||
| 278 | } else if v == '(' { | ||
| 279 | startexp = true | ||
| 280 | start = false | ||
| 281 | params = append(params, ":"+string(param)) | ||
| 282 | paramsNum += 1 | ||
| 283 | expt = make([]rune, 0) | ||
| 284 | expt = append(expt, '(') | ||
| 285 | } else if v == ')' { | ||
| 286 | startexp = false | ||
| 287 | expt = append(expt, ')') | ||
| 288 | out = append(out, expt...) | ||
| 289 | param = make([]rune, 0) | ||
| 290 | } else if v == '?' { | ||
| 291 | params = append(params, ":") | ||
| 292 | } else { | ||
| 293 | out = append(out, v) | ||
| 294 | } | ||
| 295 | } | ||
| 296 | if len(param) > 0 { | ||
| 297 | if paramsNum > 0 { | ||
| 298 | out = append(out, []rune(`(.+)`)...) | ||
| 299 | } | ||
| 300 | params = append(params, ":"+string(param)) | ||
| 301 | } | ||
| 302 | return true, params, string(out) | ||
| 303 | } else { | ||
| 304 | return false, nil, "" | ||
| 305 | } | ||
| 306 | } |
tree_test.go
0 → 100644
| 1 | package beego | ||
| 2 | |||
| 3 | import "testing" | ||
| 4 | |||
| 5 | type testinfo struct { | ||
| 6 | url string | ||
| 7 | requesturl string | ||
| 8 | params map[string]string | ||
| 9 | } | ||
| 10 | |||
| 11 | var routers []testinfo | ||
| 12 | |||
| 13 | func init() { | ||
| 14 | routers = make([]testinfo, 0) | ||
| 15 | routers = append(routers, testinfo{"/:id", "/123", map[string]string{":id": "123"}}) | ||
| 16 | routers = append(routers, testinfo{"/", "/", nil}) | ||
| 17 | routers = append(routers, testinfo{"/customer/login", "/customer/login", nil}) | ||
| 18 | routers = append(routers, testinfo{"/*", "/customer/123", map[string]string{":splat": "customer/123"}}) | ||
| 19 | routers = append(routers, testinfo{"/*.*", "/nice/api.json", map[string]string{":path": "nice/api", ":ext": "json"}}) | ||
| 20 | routers = append(routers, testinfo{"/v1/shop/:id:int", "/v1/shop/123", map[string]string{":id": "123"}}) | ||
| 21 | routers = append(routers, testinfo{"/v1/shop/:id/:name", "/v1/shop/123/nike", map[string]string{":id": "123", ":name": "nike"}}) | ||
| 22 | routers = append(routers, testinfo{"/v1/shop/:id/account", "/v1/shop/123/account", map[string]string{":id": "123"}}) | ||
| 23 | routers = append(routers, testinfo{"/v1/shop/:name:string", "/v1/shop/nike", map[string]string{":name": "nike"}}) | ||
| 24 | routers = append(routers, testinfo{"/v1/shop/:id([0-9]+)", "/v1/shop//123", map[string]string{":id": "123"}}) | ||
| 25 | routers = append(routers, testinfo{"/v1/shop/:id([0-9]+)_:name", "/v1/shop/123_nike", map[string]string{":id": "123", ":name": "nike"}}) | ||
| 26 | routers = append(routers, testinfo{"/v1/shop/:id_cms.html", "/v1/shop/123_cms.html", map[string]string{":id": "123"}}) | ||
| 27 | routers = append(routers, testinfo{"/v1/shop/cms_:id_:page.html", "/v1/shop/cms_123_1.html", map[string]string{":id": "123", ":page": "1"}}) | ||
| 28 | } | ||
| 29 | |||
| 30 | func TestTreeRouters(t *testing.T) { | ||
| 31 | for _, r := range routers { | ||
| 32 | tr := NewTree() | ||
| 33 | tr.AddRouter(r.url, "astaxie") | ||
| 34 | obj, param := tr.Match(r.requesturl) | ||
| 35 | if obj == nil || obj.(string) != "astaxie" { | ||
| 36 | t.Fatal(r.url + " can't get obj ") | ||
| 37 | } | ||
| 38 | if r.params != nil { | ||
| 39 | for k, v := range r.params { | ||
| 40 | if vv, ok := param[k]; !ok { | ||
| 41 | t.Fatal(r.url + r.requesturl + " get param empty:" + k) | ||
| 42 | } else if vv != v { | ||
| 43 | t.Fatal(r.url + " " + r.requesturl + " should be:" + v + " get param:" + vv) | ||
| 44 | } | ||
| 45 | } | ||
| 46 | } | ||
| 47 | } | ||
| 48 | } | ||
| 49 | |||
| 50 | func TestSplitPath(t *testing.T) { | ||
| 51 | a := splitPath("/") | ||
| 52 | if len(a) != 0 { | ||
| 53 | t.Fatal("/ should retrun []") | ||
| 54 | } | ||
| 55 | a = splitPath("/admin") | ||
| 56 | if len(a) != 1 || a[0] != "admin" { | ||
| 57 | t.Fatal("/admin should retrun [admin]") | ||
| 58 | } | ||
| 59 | a = splitPath("/admin/") | ||
| 60 | if len(a) != 1 || a[0] != "admin" { | ||
| 61 | t.Fatal("/admin/ should retrun [admin]") | ||
| 62 | } | ||
| 63 | a = splitPath("/admin/users") | ||
| 64 | if len(a) != 2 || a[0] != "admin" || a[1] != "users" { | ||
| 65 | t.Fatal("/admin should retrun [admin users]") | ||
| 66 | } | ||
| 67 | a = splitPath("/admin/:id:int") | ||
| 68 | if len(a) != 2 || a[0] != "admin" || a[1] != ":id:int" { | ||
| 69 | t.Fatal("/admin should retrun [admin :id:int]") | ||
| 70 | } | ||
| 71 | } | ||
| 72 | |||
| 73 | func TestSplitSegment(t *testing.T) { | ||
| 74 | b, w, r := splitSegment("admin") | ||
| 75 | if b || len(w) != 0 || r != "" { | ||
| 76 | t.Fatal("admin should return false, nil, ''") | ||
| 77 | } | ||
| 78 | b, w, r = splitSegment("*") | ||
| 79 | if !b || len(w) != 1 || w[0] != ":splat" || r != "" { | ||
| 80 | t.Fatal("* should return true, [:splat], ''") | ||
| 81 | } | ||
| 82 | b, w, r = splitSegment("*.*") | ||
| 83 | if !b || len(w) != 3 || w[1] != ":path" || w[2] != ":ext" || w[0] != "." || r != "" { | ||
| 84 | t.Fatal("admin should return true,[. :path :ext], ''") | ||
| 85 | } | ||
| 86 | b, w, r = splitSegment(":id") | ||
| 87 | if !b || len(w) != 1 || w[0] != ":id" || r != "" { | ||
| 88 | t.Fatal(":id should return true, [:id], ''") | ||
| 89 | } | ||
| 90 | b, w, r = splitSegment("?:id") | ||
| 91 | if !b || len(w) != 2 || w[0] != ":" || w[1] != ":id" || r != "" { | ||
| 92 | t.Fatal("?:id should return true, [: :id], ''") | ||
| 93 | } | ||
| 94 | b, w, r = splitSegment(":id:int") | ||
| 95 | if !b || len(w) != 1 || w[0] != ":id" || r != "([0-9]+)" { | ||
| 96 | t.Fatal(":id:int should return true, [:id], '([0-9]+)'") | ||
| 97 | } | ||
| 98 | b, w, r = splitSegment(":name:string") | ||
| 99 | if !b || len(w) != 1 || w[0] != ":name" || r != `([\w]+)` { | ||
| 100 | t.Fatal(`:name:string should return true, [:name], '([\w]+)'`) | ||
| 101 | } | ||
| 102 | b, w, r = splitSegment(":id([0-9]+)") | ||
| 103 | if !b || len(w) != 1 || w[0] != ":id" || r != `([0-9]+)` { | ||
| 104 | t.Fatal(`:id([0-9]+) should return true, [:id], '([0-9]+)'`) | ||
| 105 | } | ||
| 106 | b, w, r = splitSegment(":id([0-9]+)_:name") | ||
| 107 | if !b || len(w) != 2 || w[0] != ":id" || w[1] != ":name" || r != `([0-9]+)_(.+)` { | ||
| 108 | t.Fatal(`:id([0-9]+)_:name should return true, [:id :name], '([0-9]+)_(.+)'`) | ||
| 109 | } | ||
| 110 | b, w, r = splitSegment(":id_cms.html") | ||
| 111 | if !b || len(w) != 1 || w[0] != ":id" || r != `(.+)_cms.html` { | ||
| 112 | t.Fatal(":id_cms.html should return true, [:id], '(.+)_cms.html'") | ||
| 113 | } | ||
| 114 | b, w, r = splitSegment("cms_:id_:page.html") | ||
| 115 | if !b || len(w) != 2 || w[0] != ":id" || w[1] != ":page" || r != `cms_(.+)_(.+).html` { | ||
| 116 | t.Fatal(":id_cms.html should return true, [:id :page], cms_(.+)_(.+).html") | ||
| 117 | } | ||
| 118 | } |
-
Please register or sign in to post a comment