bfabcfcb by astaxie

beego:router tree

1 parent f06ba52e
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 }
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 }
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!