-
Notifications
You must be signed in to change notification settings - Fork 8
/
router.go
144 lines (124 loc) · 3.8 KB
/
router.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
// Efficient URL routing using a Trie data structure.
//
// This Package implements a URL Router, but instead of using the usual
// "evaluate all the routes and return the first regexp that matches" strategy,
// it uses a Trie data structure to perform the routing. This is more efficient,
// and scales better for a large number of routes.
// It supports the :param and *splat placeholders in the route strings.
//
// Example:
// router := urlrouter.Router{
// Routes: []urlrouter.Route{
// urlrouter.Route{
// PathExp: "/resources/:id",
// Dest: "one_resource",
// },
// urlrouter.Route{
// PathExp: "/resources",
// Dest: "all_resources",
// },
// },
// }
//
// err := router.Start()
// if err != nil {
// panic(err)
// }
//
// input := "http://example.org/resources/123"
// route, params, err := router.FindRoute(input)
// if err != nil {
// panic(err)
// }
// fmt.Print(route.Dest) // one_resource
// fmt.Print(params["id"]) // 123
//
// (Blog Post: https://www.ant0ine.com/post/better-url-routing-golang.html)
package urlrouter
import (
"errors"
"github.com/ant0ine/go-urlrouter/trie"
"net/url"
)
// TODO
// support for #param placeholder ?
// replace map[string]string by a PathParams object for more flexibility, and support for multiple param with the same names, eg: /users/:id/blogs/:id ?
type Route struct {
// A string defining the route, like "/resource/:id.json".
// Placeholders supported are:
// :param that matches any char to the first '/' or '.'
// *splat that matches everything to the end of the string
PathExp string
// Can be anything useful to point to the code to run for this route.
Dest interface{}
}
type Router struct {
// list of Routes, the order matters, if multiple Routes match, the first defined will be used.
Routes []Route
disableTrieCompression bool
index map[*Route]int
trie *trie.Trie
}
// This validates the Routes and prepares the Trie data structure.
// It must be called once the Routes are defined and before trying to find Routes.
func (self *Router) Start() error {
self.trie = trie.New()
self.index = map[*Route]int{}
unique := map[string]bool{}
for i, _ := range self.Routes {
// pointer to the Route
route := &self.Routes[i]
// unique
if unique[route.PathExp] == true {
return errors.New("duplicated PathExp")
}
unique[route.PathExp] = true
// index
self.index[route] = i
// insert in the Trie
err := self.trie.AddRoute(route.PathExp, route)
if err != nil {
return err
}
}
if self.disableTrieCompression == false {
self.trie.Compress()
}
// TODO validation of the PathExp ? start with a /
// TODO url encoding
return nil
}
// Return the first matching Route and the corresponding parameters for a given URL object.
func (self *Router) FindRouteFromURL(urlObj *url.URL) (*Route, map[string]string) {
// lookup the routes in the Trie
// TODO verify url encoding
matches := self.trie.FindRoutes(urlObj.Path)
// only return the first Route that matches
minIndex := -1
matchesByIndex := map[int]*trie.Match{}
for _, match := range matches {
route := match.Route.(*Route)
routeIndex := self.index[route]
matchesByIndex[routeIndex] = match
if minIndex == -1 || routeIndex < minIndex {
minIndex = routeIndex
}
}
if minIndex == -1 {
// no route found
return nil, nil
}
// and the corresponding params
match := matchesByIndex[minIndex]
return match.Route.(*Route), match.Params
}
// Parse the url string (complete or just the path) and return the first matching Route and the corresponding parameters.
func (self *Router) FindRoute(urlStr string) (*Route, map[string]string, error) {
// parse the url
urlObj, err := url.Parse(urlStr)
if err != nil {
return nil, nil, err
}
route, params := self.FindRouteFromURL(urlObj)
return route, params, nil
}