Created
May 7, 2012 19:34
-
-
Save jordanorelli/2629866 to your computer and use it in GitHub Desktop.
regex router in go.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package core | |
import ( | |
"net/http" | |
) | |
// the Request struct wraps the http.Request struct, providing a slice of | |
// strings representing the positional arguments found in a url pattern, and a | |
// map[string]string called kwargs representing the named parameters captured | |
// in url parsing. | |
type Request struct { | |
*http.Request | |
*RouteMatch | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package core | |
import ( | |
"net/http" | |
"regexp" | |
) | |
// HandlerFunc is nearly the same as http.HandlerFunc, it simply takes a | |
// routes.Request object instead of an http.Request object. | |
type HandlerFunc func(http.ResponseWriter, *Request) | |
// struct Router implements http.Handler, so that it may be used with the | |
// default http library. It keeps a registry mapping regexes to functions for | |
// easier url parsing. | |
type Router struct { | |
routes []*entry | |
} | |
type entry struct { | |
route *Route | |
handlers []HandlerFunc | |
} | |
// this doesn't really do anything yet, but it probably will in the future. | |
func NewRouter() *Router { | |
return &Router{} | |
} | |
// implements the http.Handler interface, so that we may use our router with | |
// the default http package. | |
func (r *Router) ServeHTTP(w http.ResponseWriter, raw *http.Request) { | |
req, e := r.match(raw) | |
if e == nil { | |
http.NotFound(w, raw) | |
return | |
} | |
for _, fn := range e.handlers { | |
fn(w, req) | |
} | |
} | |
// checks an incoming http request against our list of known routes. If the | |
// request matches one of the routes, the request is transformed into a | |
// routes.Request, and its Args and Kwargs fields are filled in based on the | |
// url. If no match is found, returns (nil, nil) | |
func (r *Router) match(req *http.Request) (*Request, *entry) { | |
for _, e := range r.routes { | |
if match := e.route.Match(req.URL.Path); match != nil { | |
return &Request{req, match}, e | |
} | |
} | |
return nil, nil | |
} | |
// adds a regex-based route in the normal human fashion. | |
func (router *Router) AddRoute(pattern string, fns ...HandlerFunc) { | |
router.routes = append(router.routes, &entry{route: NewRoute(pattern), handlers: fns}) | |
} | |
// right now, just embeds a regex. A "name" field should also be added here. | |
type Route struct { | |
*regexp.Regexp | |
} | |
type RouteMatch struct { | |
Args []string | |
Kwargs map[string]string | |
} | |
func NewRoute(pattern string) *Route { | |
return &Route{regexp.MustCompile(pattern)} | |
} | |
func (r *Route) Match(target string) *RouteMatch { | |
submatches := r.FindStringSubmatch(target) | |
if submatches == nil { | |
return nil | |
} | |
if len(submatches) == 1 { | |
return new(RouteMatch) | |
} | |
m := new(RouteMatch) | |
submatches = submatches[1:] | |
for i, name := range r.SubexpNames()[1:] { | |
if name == "" { | |
m.Args = append(m.Args, submatches[i]) | |
} else { | |
if m.Kwargs == nil { | |
m.Kwargs = make(map[string]string) | |
} | |
m.Kwargs[name] = submatches[i] | |
} | |
} | |
return m | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package core | |
import "testing" | |
func TestNoMatch(t *testing.T) { | |
r := NewRoute("^/foo$") | |
if match := r.Match("/foo/bar"); match != nil { | |
t.Errorf("found incorrect match") | |
} | |
} | |
func TestExactMatch(t *testing.T) { | |
r := NewRoute("^/foo$") | |
match := r.Match("/foo") | |
if match == nil { | |
t.Errorf("match is %v, expected %v", match, nil) | |
} | |
if len(match.Args) > 0 { | |
t.Errorf("expected no match args. Found %v", match.Args) | |
} | |
if len(match.Kwargs) > 0 { | |
t.Errorf("expected no match kwargs. Found %v", match.Kwargs) | |
} | |
} | |
// tests positional arguments in url path parsing | |
func TestArgs(t *testing.T) { | |
r := NewRoute("^/foo/(.*)$") | |
match := r.Match("/foo/bar") | |
if match == nil { | |
t.Errorf("expected match; found none.") | |
} | |
if match.Args[0] != "bar" { | |
t.Errorf(`Found match args of %v, expected ["bar"]`, match.Args[0]) | |
} | |
if len(match.Kwargs) != 0 { | |
t.Errorf("Found some kwargs. Shouldn't have.") | |
} | |
} | |
// tests keyword arguments in url path parsing (i.e., named capture groups in | |
// the provided regex) | |
func TestKwargs(t *testing.T) { | |
r := NewRoute("^/foo/(?P<leeroy>.*)$") | |
match := r.Match("/foo/jenkins") | |
if match == nil { | |
t.Errorf("expected match; found none.") | |
} | |
if len(match.Args) > 0 { | |
t.Errorf("Found some args. Shouldn't have.") | |
} | |
if match.Kwargs == nil { | |
t.Errorf("Got nil kwargs. Shouldn't.") | |
} | |
if match.Kwargs["leeroy"] != "jenkins" { | |
t.Errorf(`Found bad kwarg. Expected "jenkins", found %v`, match.Kwargs["leeroy"]) | |
} | |
} | |
// tests a heterogenous mixture of positional and keyword parameters. | |
func TestHeterogenous(t *testing.T) { | |
r := NewRoute("^/foo/(?P<leeroy>\\w+)/(\\d+)$") | |
match := r.Match("/foo/jenkins/9000") | |
if match == nil { | |
t.Errorf("expected match; found none.") | |
} | |
if match.Args[0] != "9000" { | |
t.Errorf(`expected arg of "9000", found %v`, match.Args[0]) | |
} | |
if match.Kwargs["leeroy"] != "jenkins" { | |
t.Errorf(`expected kwarg "leeroy" of value "jenkins". Found %v`, match.Kwargs["leeroy"]) | |
} | |
badmatch := r.Match("/foo/jenkins/") | |
if badmatch != nil { | |
t.Errorf("found bad match.") | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment