From 35af451420a3feb90c49ebabd1bf1f1775d02755 Mon Sep 17 00:00:00 2001 From: Carl Jackson Date: Sun, 30 Mar 2014 16:34:09 -0700 Subject: [PATCH] Replace Sub() with trailing * in patterns This change replaces a bit of API surface area (the Sub() method on Muxes) with a slightly more expressive pattern syntax. I'm mostly doing this because it seems cleaner: the "*" gets to take on a meaning very similar to what it means in Sinatra (without growing regexp-like middle-of-a-path globbing, which sounds terrifying and not particularly useful), and we get to nuke a useless function from the API. --- default.go | 6 ------ example/main.go | 4 +++- web/mux.go | 17 +++++++++++++---- web/pattern.go | 9 ++++++++- web/pattern_test.go | 25 +++++++++++++++++++------ web/router.go | 44 +++++++++++++++----------------------------- web/router_test.go | 4 ++-- 7 files changed, 60 insertions(+), 49 deletions(-) diff --git a/default.go b/default.go index 6ec68ed..70b8ed3 100644 --- a/default.go +++ b/default.go @@ -101,12 +101,6 @@ func Trace(pattern interface{}, handler interface{}) { DefaultMux.Trace(pattern, handler) } -// Add a sub-route to the default Mux. See the documentation for web.Mux.Sub -// for more information. -func Sub(pattern string, handler interface{}) { - DefaultMux.Sub(pattern, handler) -} - // Set the NotFound handler for the default Mux. See the documentation for // web.Mux.NotFound for more information. func NotFound(handler interface{}) { diff --git a/example/main.go b/example/main.go index 31329b9..3499aa2 100644 --- a/example/main.go +++ b/example/main.go @@ -38,10 +38,12 @@ func main() { // can put them wherever you like. goji.Use("PlainText", PlainText) + // If the last character of a pattern is an asterisk, the path is + // treated as a prefix, and can be used to implement sub-routes. // Sub-routes can be used to set custom middleware on sub-applications. // Goji's interfaces are completely composable. admin := web.New() - goji.Sub("/admin", admin) + goji.Handle("/admin/*", admin) admin.Use("SuperSecure", SuperSecure) // Set up admin routes. Note that sub-routes do *not* mutate the path in diff --git a/web/mux.go b/web/mux.go index 00b5d68..33f92c5 100644 --- a/web/mux.go +++ b/web/mux.go @@ -30,11 +30,20 @@ following types: - func(c *web.C, http.Handler) http.Handler All of the route-adding functions on Mux take two untyped parameters: pattern and handler. Pattern must be one of the following types: - - string (interpreted as a Sinatra pattern) + - string. It will be interpreted as a Sinatra-like pattern. In + particular, the following syntax is recognized: + - a path segment starting with with a colon will match any + string placed at that position. e.g., "/:name" will match + "/carl", binding "name" to "carl". + - a pattern ending with an asterisk will match any prefix of + that route. For instance, "/admin/*" will match "/admin/" and + "/admin/secret/lair". This is similar to Sinatra's wildcard, + but may only appear at the very end of the string and is + therefore significantly less powerful. - regexp.Regexp. The library assumes that it is a Perl-style regexp that - is anchored on the left (i.e., the beginning of the string). If your - regexp is not anchored on the left, a hopefully-identical left-anchored - regexp will be created and used instead. + is anchored on the left (i.e., the beginning of the string). If your + regexp is not anchored on the left, a hopefully-identical + left-anchored regexp will be created and used instead. - web.Pattern Handler must be one of the following types: - http.Handler diff --git a/web/pattern.go b/web/pattern.go index 4916827..401363d 100644 --- a/web/pattern.go +++ b/web/pattern.go @@ -200,7 +200,14 @@ func (s stringPattern) String() string { var patternRe = regexp.MustCompile(`/:([^/]+)`) -func parseStringPattern(s string, isPrefix bool) stringPattern { +func parseStringPattern(s string) stringPattern { + var isPrefix bool + // Routes that end in an asterisk ("*") are prefix routes + if len(s) > 0 && s[len(s)-1] == '*' { + s = s[:len(s)-1] + isPrefix = true + } + matches := patternRe.FindAllStringSubmatchIndex(s, -1) pats := make([]string, len(matches)) literals := make([]string, len(matches)+1) diff --git a/web/pattern_test.go b/web/pattern_test.go index b6911d7..0377ab0 100644 --- a/web/pattern_test.go +++ b/web/pattern_test.go @@ -81,14 +81,14 @@ var patternTests = []struct { }}, // String pattern tests - {parseStringPattern("/hello", false), + {parseStringPattern("/hello"), "/hello", []patternTest{ pt("/hello", true, nil), pt("/hell", false, nil), pt("/hello/", false, nil), pt("/hello/world", false, nil), }}, - {parseStringPattern("/hello/:name", false), + {parseStringPattern("/hello/:name"), "/hello/", []patternTest{ pt("/hello/world", true, map[string]string{ "name": "world", @@ -97,7 +97,7 @@ var patternTests = []struct { pt("/hello/", false, nil), pt("/hello/my/love", false, nil), }}, - {parseStringPattern("/a/:a/b/:b", false), + {parseStringPattern("/a/:a/b/:b"), "/a/", []patternTest{ pt("/a/1/b/2", true, map[string]string{ "a": "1", @@ -108,8 +108,8 @@ var patternTests = []struct { pt("/a/1/b/2/3", false, nil), }}, - // String sub-pattern tests - {parseStringPattern("/user/:user", true), + // String prefix tests + {parseStringPattern("/user/:user*"), "/user/", []patternTest{ pt("/user/bob", true, map[string]string{ "user": "bob", @@ -120,7 +120,16 @@ var patternTests = []struct { pt("/user/", false, nil), pt("/user//", false, nil), }}, - {parseStringPattern("/user/:user/friends", true), + {parseStringPattern("/user/:user/*"), + "/user/", []patternTest{ + pt("/user/bob/friends/123", true, map[string]string{ + "user": "bob", + }), + pt("/user/bob", false, nil), + pt("/user/", false, nil), + pt("/user//", false, nil), + }}, + {parseStringPattern("/user/:user/friends*"), "/user/", []patternTest{ pt("/user/bob/friends", true, map[string]string{ "user": "bob", @@ -128,6 +137,10 @@ var patternTests = []struct { pt("/user/bob/friends/123", true, map[string]string{ "user": "bob", }), + // This is a little unfortunate + pt("/user/bob/friends123", true, map[string]string{ + "user": "bob", + }), pt("/user/bob/enemies", false, nil), }}, } diff --git a/web/router.go b/web/router.go index 770f2d9..b91dce6 100644 --- a/web/router.go +++ b/web/router.go @@ -83,14 +83,14 @@ type Pattern interface { Match(r *http.Request, c *C, dryrun bool) bool } -func parsePattern(p interface{}, isPrefix bool) Pattern { +func parsePattern(p interface{}) Pattern { switch p.(type) { case Pattern: return p.(Pattern) case *regexp.Regexp: return parseRegexpPattern(p.(*regexp.Regexp)) case string: - return parseStringPattern(p.(string), isPrefix) + return parseStringPattern(p.(string)) default: log.Fatalf("Unknown pattern type %v. Expected a web.Pattern, "+ "regexp.Regexp, or a string.", p) @@ -179,7 +179,7 @@ func (rt *router) route(c C, w http.ResponseWriter, r *http.Request) { } func (rt *router) handleUntyped(p interface{}, m method, h interface{}) { - pat := parsePattern(p, false) + pat := parsePattern(p) rt.handle(pat, m, parseHandler(h)) } @@ -201,9 +201,18 @@ func (rt *router) handle(p Pattern, m method, h Handler) { // functions here "m" instead of the standard "rt", since they will eventually // be shown on the documentation for the Mux that they are included in. -// Dispatch to the given handler when the pattern matches, regardless of HTTP -// method. See the documentation for type Mux for a description of what types -// are accepted for pattern and handler. +/* +Dispatch to the given handler when the pattern matches, regardless of HTTP +method. See the documentation for type Mux for a description of what types are +accepted for pattern and handler. + +This method is commonly used to implement sub-routing: an admin application, for +instance, can expose a single handler that is attached to the main Mux by +calling Handle("/admin*", adminHandler) or similar. Note that this function +doesn't strip this prefix from the path before forwarding it on (e.g., the +handler will see the full path, including the "/admin" part), but this +functionality can easily be performed by an extra middleware layer. +*/ func (m *router) Handle(pattern interface{}, handler interface{}) { m.handleUntyped(pattern, mALL, handler) } @@ -276,29 +285,6 @@ func (m *router) Trace(pattern interface{}, handler interface{}) { m.handleUntyped(pattern, mTRACE, handler) } -/* -Dispatch to the given handler when the given (Sinatra-like) pattern matches a -prefix of the path. This function explicitly takes a string parameter since you -can implement this behavior with a regular expression using the standard -Handle() function. - -This function is probably most helpful when implementing sub-routing: an admin -application, for instance, can expose a single handler, and can be hooked up all -at once by attaching a sub-route at "/admin". - -Notably, this function does not strip the matched prefix from downstream -handlers, so in the above example the handler would recieve requests with path -"/admin/foo/bar", for example, instead of just "/foo/bar". Luckily, this is a -problem easily surmountable by middleware. - -See the documentation for type Mux for a description of what types are accepted -for handler. -*/ -func (m *router) Sub(pattern string, handler interface{}) { - pat := parsePattern(pattern, true) - m.handle(pat, mALL, parseHandler(handler)) -} - // Set the fallback (i.e., 404) handler for this mux. See the documentation for // type Mux for a description of what types are accepted for handler. // diff --git a/web/router_test.go b/web/router_test.go index 84c0f62..5e4d7cd 100644 --- a/web/router_test.go +++ b/web/router_test.go @@ -155,12 +155,12 @@ func TestNotFound(t *testing.T) { } } -func TestSub(t *testing.T) { +func TestPrefix(t *testing.T) { t.Parallel() rt := makeRouter() ch := make(chan string, 1) - rt.Sub("/hello", func(w http.ResponseWriter, r *http.Request) { + rt.Handle("/hello*", func(w http.ResponseWriter, r *http.Request) { ch <- r.URL.Path })