Implement explicit API for creating a router and adding routes. #1

Open
wants to merge 4 commits into
from
View
@@ -7,29 +7,33 @@ import (
func main() {
- router := urlrouter.Router{
- Routes: []urlrouter.Route{
- urlrouter.Route{
- PathExp: "/resources/:id",
- Dest: "one_resource",
- },
- urlrouter.Route{
- PathExp: "/resources",
- Dest: "all_resources",
- },
+ router := urlrouter.NewRouter()
+
+ err := router.AddRoutes([]urlrouter.Route{
+ urlrouter.Route{
+ Path: "/resources/:id",
+ Dest: "one_resource",
+ },
+ urlrouter.Route{
+ Path: "/resources",
+ Dest: "all_resources",
},
+ })
+
+ if err != nil {
+ panic(err)
}
- err := router.Start()
+ err = router.Start()
if err != nil {
panic(err)
}
input := "http://example.org/resources/123"
- route, params, err := router.FindRoute(input)
+ route, params, err := router.FindRoute(input, "GET")
if err != nil {
panic(err)
}
- fmt.Print(route.Dest) // one_resource
- fmt.Print(params["id"]) // 123
+ fmt.Println(route.Dest) // one_resource
+ fmt.Println(params["id"]) // 123
}
View
@@ -17,23 +17,31 @@ func Bonjour(w http.ResponseWriter, req *http.Request, params map[string]string)
func main() {
- router := urlrouter.Router{
- Routes: []urlrouter.Route{
+ router := urlrouter.NewRouter()
+
+ error := router.AddRoutes(
+ []urlrouter.Route{
urlrouter.Route{
- PathExp: "/hello/:name",
- Dest: Hello,
+ Path: "/hello/:name",
+ Dest: Hello,
+ HttpMethod: "GET",
},
urlrouter.Route{
- PathExp: "/bonjour/:name",
- Dest: Bonjour,
+ Path: "/bonjour/:name",
+ Dest: Bonjour,
+ HttpMethod: "GET",
},
},
+ )
+
+ if error != nil {
+ panic(error)
}
router.Start()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- route, params := router.FindRouteFromURL(r.URL)
+ route, params := router.FindRouteFromURL(r.URL, r.Method)
handler := route.Dest.(func(http.ResponseWriter, *http.Request, map[string]string))
handler(w, r, params)
})
View
140 router.go
@@ -7,8 +7,8 @@
// It supports the :param and *splat placeholders in the route strings.
//
// Example:
-// router := urlrouter.Router{
-// Routes: []urlrouter.Route{
+// router := urlrouter.NewRouter()
+// err := router.AddRoutes([]urlrouter.Route{
// urlrouter.Route{
// PathExp: "/resources/:id",
// Dest: "one_resource",
@@ -18,9 +18,12 @@
// Dest: "all_resources",
// },
// },
-// }
+// })
//
-// err := router.Start()
+// if err != nil {
+// panic(err)
+// }
+// err = router.Start()
// if err != nil {
// panic(err)
// }
@@ -38,8 +41,11 @@ package urlrouter
import (
"errors"
+ "fmt"
"github.com/ant0ine/go-urlrouter/trie"
+ "net/http"
"net/url"
+ "strings"
)
// TODO
@@ -51,44 +57,79 @@ type Route struct {
// Placeholders supported are:
// :param that matches any char to the first '/' or '.'
// *splat that matches everything to the end of the string
- PathExp string
+ // PathExp string
// Can be anything useful to point to the code to run for this route.
- Dest interface{}
+ Dest interface{}
+ HttpMethod string
+ Path string
}
type Router struct {
// list of Routes, the order matters, if multiple Routes match, the first defined will be used.
Routes []Route
disableTrieCompression bool
+ // By default, support for the method OPTIONS is added to all the routes
+ // and add a 405 Method Not Allowed response if the method is not existing
+ // for the path. By setting this option to false, the routes are not added.
+ disableDefaultOptions bool
index map[*Route]int
trie *trie.Trie
+ routesIndex map[string]bool
+}
+
+func (self *Router) AddRoutes(routes []Route) error {
+ for _, route := range routes {
+ error := self.AddRoute(route)
+ if error != nil {
+ return error
+ }
+ }
+ return nil
+}
+
+// Add a new route to the router. If there's already a router with for this
+// path and HTTP method, an error is returned.
+func (self *Router) AddRoute(route Route) error {
+ PathExp := route.HttpMethod + route.Path
+ if self.routesIndex[PathExp] == true {
+ return errors.New(fmt.Sprintf("Duplicated PathExp: %s", PathExp))
+ }
+ self.Routes = append(self.Routes, route)
+ self.routesIndex[PathExp] = true
+ return nil
+}
+
+// Returns a new router.
+func NewRouter() Router {
+ return Router{
+ Routes: []Route{},
+ routesIndex: map[string]bool{},
+ }
}
// 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.
+/// 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)
+ err := self.trie.AddRoute(route.Path, route.HttpMethod, route)
if err != nil {
return err
}
}
+ if self.disableDefaultOptions == false {
+ self.AddNotAllowed()
+ }
+
if self.disableTrieCompression == false {
self.trie.Compress()
}
@@ -99,12 +140,75 @@ func (self *Router) Start() error {
return nil
}
+func (self *Router) addAllowedRoutes(path string, allowed []string) {
+ optionsFunc := func(w http.ResponseWriter, req *http.Request, params map[string]string) {
+ w.Header().Set("Allow", strings.Join(allowed, ", "))
+ w.WriteHeader(204)
+ r := []byte{}
+ w.Write(r)
+ }
+ route := Route{
+ Path: path,
+ HttpMethod: "OPTIONS",
+ Dest: optionsFunc,
+ }
+ self.trie.AddRoute(route.Path, route.HttpMethod, &route)
+
+}
+
+func (self *Router) addDisallowedRoutes(path string, allowed []string, unallowed []string) {
+ unallowedFunc := func(w http.ResponseWriter, req *http.Request, params map[string]string) {
+ w.Header().Set("Allow", strings.Join(allowed, ", "))
+ w.Header().Set("Content-Type", "text/plain")
+ w.WriteHeader(405)
+ r := []byte{}
+ w.Write(r)
+ }
+ for _, m := range unallowed {
+ route := Route{
+ Path: path,
+ HttpMethod: m,
+ Dest: unallowedFunc,
+ }
+ self.trie.AddRoute(route.Path, route.HttpMethod, &route)
+ }
+}
+
+func (self *Router) addOptions(path string, methods map[string]bool) {
+ allowed := []string{}
+ unallowed := []string{}
+
+ for _, dm := range trie.HttpDefaultMethods {
+ if methods[dm] == false {
+ unallowed = append(unallowed, dm)
+ } else {
+ allowed = append(allowed, dm)
+ }
+ }
+ self.addAllowedRoutes(path, allowed)
+ self.addDisallowedRoutes(path, allowed, unallowed)
+}
+
+func (self *Router) AddNotAllowed() {
+ for path, methods := range self.trie.AllRoutes() {
+ // I would like to also add HEAD here, but this means a few changes:
+ // - functions should not write the response, but returns the status, headers and body
+ // - the main handler write the response
+ // - the HEAD function will call the function associated with GET and drop the body,
+ // and write only the HEADERS + status (as per the RFC).
+ // Since this is a bigger change, I'm not going to apply that patch for now.
+ if methods["OPTIONS"] == false {
+ self.addOptions(path, methods)
+ }
+ }
+}
+
// 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) {
+func (self *Router) FindRouteFromURL(urlObj *url.URL, method string) (*Route, map[string]string) {
// lookup the routes in the Trie
// TODO verify url encoding
- matches := self.trie.FindRoutes(urlObj.Path)
+ matches := self.trie.FindRoutes(urlObj.Path, method)
// only return the first Route that matches
minIndex := -1
@@ -131,14 +235,14 @@ func (self *Router) FindRouteFromURL(urlObj *url.URL) (*Route, map[string]string
}
// 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) {
+func (self *Router) FindRoute(urlStr string, method 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)
+ route, params := self.FindRouteFromURL(urlObj, method)
return route, params, nil
}
View
@@ -29,7 +29,7 @@ func routes() []Route {
routes := []Route{}
for _, path := range routePaths {
- routes = append(routes, Route{PathExp: path, Dest: path})
+ routes = append(routes, Route{Path: path, Dest: path})
}
return routes
}
@@ -65,7 +65,7 @@ func BenchmarkNoCompression(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, urlObj := range urlObjs {
- router.FindRouteFromURL(urlObj)
+ router.FindRouteFromURL(urlObj, "GET")
}
}
}
@@ -84,7 +84,7 @@ func BenchmarkCompression(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, urlObj := range urlObjs {
- router.FindRouteFromURL(urlObj)
+ router.FindRouteFromURL(urlObj, "GET")
}
}
}
@@ -110,7 +110,7 @@ func BenchmarkRegExpLoop(b *testing.B) {
for _, route := range routes {
// generate the regexp string
- regStr := r2.ReplaceAllString(route.PathExp, "([^/\\.]+)")
+ regStr := r2.ReplaceAllString(route.Path, "([^/\\.]+)")
regStr = r1.ReplaceAllString(regStr, "(.+)")
regStr = "^" + regStr + "$"
Oops, something went wrong.