Skip to content

Instantly share code, notes, and snippets.

@jordanorelli
Created May 7, 2012 19:34
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jordanorelli/2629866 to your computer and use it in GitHub Desktop.
Save jordanorelli/2629866 to your computer and use it in GitHub Desktop.
regex router in go.
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
}
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
}
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