diff --git a/router.go b/router.go index ab9b2f5..0f5bfe2 100644 --- a/router.go +++ b/router.go @@ -18,7 +18,17 @@ type segment struct { children map[string]*segment } -// Router is the main router object that keeps track of an looks up routes. +// Router is a replacement for the net/http DefaultServerMux. This version includes the +// ability to add path parameter in the given path. +// +// Paths are registered relative to their base path, WITHOUT a hostname, something that +// is allowed in the DefaultServerMux but is not allowed in this one. Each callback needs +// to be given a unique combination of method and path. +// +// Path parameters can be registered by prefacing any section of the path with a ":", so +// "/items/:itemid" would register ":itemid" as a wildcard which will be turned into a path +// parameter called "itemid". A request path with "/items/" followed by a string of legal http +// characters, not including a slash, would match this path. type Router struct { routes []route lookup *segment @@ -29,7 +39,8 @@ func NewRouter() (r Router) { return } -// AddRoute adds a new route with a corresponding callback to the router. +// AddRoute registers a new handler function to a path and http.HandlerFunc. If a path and +// method already have a callback registered to them, and error is returned. func (r *Router) AddRoute(method string, path string, callback http.HandlerFunc) (err error) { keys := setupKeys(strings.Split(path, "/")) if r.lookup == nil { @@ -69,6 +80,49 @@ func (r *Router) AddRoute(method string, path string, callback http.HandlerFunc) return } +// Handler returns the handler to use for the given request, +// consulting r.Method, r.URL.Path. It always returns +// a non-nil handler. +// +// Handler also returns the registered pattern that matches the +// request. +// +// If there is no registered handler that applies to the request, +// Handler returns a ``page not found'' handler and an empty pattern. +func (r *Router) Handler(req *http.Request) (h http.Handler, pattern string) { + method := req.Method + path := req.URL.Path + root := r.lookup + curr := root + + segments := strings.Split(path, "/") + keys := setupKeys(segments) + + // TODO: make this a named function somewhere. Maybe allow a custom version. + h = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(404) + }) + + for _, v := range keys { + if v == "/" { + continue + } + + if seg, ok := curr.children[v]; ok { + curr = seg + } else { + return + } + } + + if cb, ok := curr.methods[method]; ok { + h = cb + pattern = curr.path + } + + return +} + func addSegment(curr *segment, keys []string) (seg *segment) { for _, key := range keys { if child, ok := curr.children[key]; !ok { diff --git a/router_test.go b/router_test.go index acf213d..a52b5f0 100644 --- a/router_test.go +++ b/router_test.go @@ -2,45 +2,87 @@ package router import ( "fmt" + "io/ioutil" "net/http" + "net/http/httptest" "testing" ) func TestAddRouter(t *testing.T) { - r := Router{} + router := Router{} routeCounter := 0 - err := addAndCheckRoute(&r, http.MethodGet, "/", func(http.ResponseWriter, *http.Request) {}, &routeCounter) + err := addAndCheckRoute(&router, http.MethodGet, "/", func(http.ResponseWriter, *http.Request) {}, &routeCounter) if err != nil { t.Error("The route was not correctly added to the router: ", err) } - err = addAndCheckRoute(&r, http.MethodPost, "/", func(http.ResponseWriter, *http.Request) {}, &routeCounter) + err = addAndCheckRoute(&router, http.MethodPost, "/", func(http.ResponseWriter, *http.Request) {}, &routeCounter) if err != nil { t.Error("The route was not correctly added to the router: ", err) } - err = addAndCheckRoute(&r, http.MethodPatch, "/items", func(http.ResponseWriter, *http.Request) {}, &routeCounter) + err = addAndCheckRoute(&router, http.MethodPatch, "/items", func(http.ResponseWriter, *http.Request) {}, &routeCounter) if err != nil { t.Error("The route was not correctly added to the router: ", err) } - err = addAndCheckRoute(&r, http.MethodDelete, "/items/thing/man/bird/horse/poop", func(http.ResponseWriter, *http.Request) {}, &routeCounter) + err = addAndCheckRoute(&router, http.MethodDelete, "/items/thing/man/bird/horse/poop", func(http.ResponseWriter, *http.Request) {}, &routeCounter) if err != nil { t.Error("The route was not correctly added to the router: ", err) } - err = addAndCheckRoute(&r, http.MethodDelete, "/items/thing/man/bird/cat/poop", func(http.ResponseWriter, *http.Request) {}, &routeCounter) + err = addAndCheckRoute(&router, http.MethodDelete, "/items/thing/man/bird/cat/poop", func(http.ResponseWriter, *http.Request) {}, &routeCounter) if err != nil { t.Error("The route was not correctly added to the router: ", err) } - checkLookup(r.lookup) + // checkLookup(router.lookup) +} + +func TestHandler(t *testing.T) { + router := Router{} + request, err := http.NewRequest(http.MethodGet, "http://example.com/items", nil) + rr := httptest.NewRecorder() + expectedBody := "I am /items" + + if err != nil { + t.Error("Could not create request") + } + + router.AddRoute(http.MethodGet, "/items", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Write([]byte(expectedBody)) + }) + + checkLookup(router.lookup) + + h, pattern := router.Handler(request) + + if pattern != "/items" { + t.Errorf("The recovered patter does not match: %s", pattern) + } + + h.ServeHTTP(rr, request) + + if rr.Code != 200 { + t.Errorf("The returned callback did not write 200 to the header. Found %d", rr.Code) + } + + body, _ := ioutil.ReadAll(rr.Body) + + if string(body) != string([]byte(expectedBody)) { + t.Errorf( + "The returned callback did not write the expected body. Expected: %s. Actual: %s", + expectedBody, + string(body), + ) + } } func checkLookup(curr *segment) {