From 2fe5c3ee437887e03c7a068e2feb0a21b89886c6 Mon Sep 17 00:00:00 2001 From: Carl Jackson Date: Sat, 1 Nov 2014 15:46:02 -0700 Subject: [PATCH] Change semantics of wildcard patterns This is a breaking API change that changes how wildcard patterns are treated. In particular, wildcards are no longer allowed to appear at arbitrary places in the URL, and are only allowed to appear immediately after a path separator. This change effectively changes the wildcard sigil from "*" to "/*". Users who use wildcard routes like "/hello*" will have to switch to regular expression based routes to preserve the old semantics. The motivation for this change is that it allows the router to publish a special "tail" key which represents the unmatched portion of the URL. This is placed into URLParams under the key "*", and includes a leading "/" to make it easier to write sub-routers. --- example/main.go | 8 ++++---- web/mux.go | 13 ++++++++----- web/pattern_test.go | 24 ++++++++---------------- web/router_test.go | 2 +- web/string_pattern.go | 36 +++++++++++++++++++++--------------- 5 files changed, 42 insertions(+), 41 deletions(-) diff --git a/example/main.go b/example/main.go index 9315af6..ccfae49 100644 --- a/example/main.go +++ b/example/main.go @@ -39,10 +39,10 @@ func main() { // can put them wherever you like. goji.Use(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. + // If the patterns ends with "/*", 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.Handle("/admin/*", admin) admin.Use(SuperSecure) diff --git a/web/mux.go b/web/mux.go index ee0d8b4..0004bae 100644 --- a/web/mux.go +++ b/web/mux.go @@ -35,11 +35,14 @@ and handler. Pattern must be one of the following types: - 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. + - a pattern ending with "/*" will match any route with that + prefix. For instance, the pattern "/u/:name/*" will match + "/u/carl/" and "/u/carl/projects/123", but not "/u/carl" + (because there is no trailing slash). In addition to any names + bound in the pattern, the special name "*" is bound to the + unmatched tail of the match, but including the leading "/". So + for the two matching examples above, "*" would be bound to "/" + and "/projects/123" respectively. - 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 diff --git a/web/pattern_test.go b/web/pattern_test.go index 3794270..7fb3c7a 100644 --- a/web/pattern_test.go +++ b/web/pattern_test.go @@ -127,37 +127,29 @@ var patternTests = []struct { }}, // String prefix tests - {parseStringPattern("/user/:user*"), + {parseStringPattern("/user/:user/*"), "/user/", []patternTest{ - pt("/user/bob", true, map[string]string{ + pt("/user/bob/", true, map[string]string{ "user": "bob", + "*": "/", }), pt("/user/bob/friends/123", true, map[string]string{ "user": "bob", - }), - pt("/user/", false, nil), - pt("/user//", false, nil), - }}, - {parseStringPattern("/user/:user/*"), - "/user/", []patternTest{ - pt("/user/bob/friends/123", true, map[string]string{ - "user": "bob", + "*": "/friends/123", }), pt("/user/bob", false, nil), pt("/user/", false, nil), pt("/user//", false, nil), }}, - {parseStringPattern("/user/:user/friends*"), + {parseStringPattern("/user/:user/friends/*"), "/user/", []patternTest{ - pt("/user/bob/friends", true, map[string]string{ + pt("/user/bob/friends/", true, map[string]string{ "user": "bob", + "*": "/", }), 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", + "*": "/123", }), pt("/user/bob/enemies", false, nil), }}, diff --git a/web/router_test.go b/web/router_test.go index b4c9730..b80862d 100644 --- a/web/router_test.go +++ b/web/router_test.go @@ -244,7 +244,7 @@ func TestPrefix(t *testing.T) { m := New() ch := make(chan string, 1) - m.Handle("/hello*", func(w http.ResponseWriter, r *http.Request) { + m.Handle("/hello/*", func(w http.ResponseWriter, r *http.Request) { ch <- r.URL.Path }) diff --git a/web/string_pattern.go b/web/string_pattern.go index 1dcb3c7..197c7c9 100644 --- a/web/string_pattern.go +++ b/web/string_pattern.go @@ -13,7 +13,7 @@ type stringPattern struct { pats []string breaks []byte literals []string - isPrefix bool + wildcard bool } func (s stringPattern) Prefix() string { @@ -28,8 +28,12 @@ func (s stringPattern) Run(r *http.Request, c *C) { func (s stringPattern) match(r *http.Request, c *C, dryrun bool) bool { path := r.URL.Path var matches map[string]string - if !dryrun && len(s.pats) > 0 { - matches = make(map[string]string, len(s.pats)) + if !dryrun { + if s.wildcard { + matches = make(map[string]string, len(s.pats)+1) + } else if len(s.pats) != 0 { + matches = make(map[string]string, len(s.pats)) + } } for i := 0; i < len(s.pats); i++ { sli := s.literals[i] @@ -56,14 +60,16 @@ func (s stringPattern) match(r *http.Request, c *C, dryrun bool) bool { path = path[m:] } // There's exactly one more literal than pat. - if s.isPrefix { - if !strings.HasPrefix(path, s.literals[len(s.pats)]) { + tail := s.literals[len(s.pats)] + if s.wildcard { + if !strings.HasPrefix(path, tail) { return false } - } else { - if path != s.literals[len(s.pats)] { - return false + if !dryrun { + matches["*"] = path[len(tail)-1:] } + } else if path != tail { + return false } if c == nil || dryrun { @@ -81,7 +87,7 @@ func (s stringPattern) match(r *http.Request, c *C, dryrun bool) bool { } func (s stringPattern) String() string { - return fmt.Sprintf("stringPattern(%q, %v)", s.raw, s.isPrefix) + return fmt.Sprintf("stringPattern(%q)", s.raw) } // "Break characters" are characters that can end patterns. They are not allowed @@ -93,11 +99,11 @@ const bc = "/.;," var patternRe = regexp.MustCompile(`[` + bc + `]:([^` + bc + `]+)`) 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] == '*' { + raw := s + var wildcard bool + if strings.HasSuffix(s, "/*") { s = s[:len(s)-1] - isPrefix = true + wildcard = true } matches := patternRe.FindAllStringSubmatchIndex(s, -1) @@ -118,10 +124,10 @@ func parseStringPattern(s string) stringPattern { } literals[len(matches)] = s[n:] return stringPattern{ - raw: s, + raw: raw, pats: pats, breaks: breaks, literals: literals, - isPrefix: isPrefix, + wildcard: wildcard, } }