Skip to content

Commit

Permalink
Change semantics of wildcard patterns
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
zenazn committed Nov 1, 2014
1 parent 1a390ab commit 2fe5c3e
Show file tree
Hide file tree
Showing 5 changed files with 42 additions and 41 deletions.
8 changes: 4 additions & 4 deletions example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 8 additions & 5 deletions web/mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 8 additions & 16 deletions web/pattern_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}},
Expand Down
2 changes: 1 addition & 1 deletion web/router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
})

Expand Down
36 changes: 21 additions & 15 deletions web/string_pattern.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type stringPattern struct {
pats []string
breaks []byte
literals []string
isPrefix bool
wildcard bool
}

func (s stringPattern) Prefix() string {
Expand All @@ -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]
Expand All @@ -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 {
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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,
}
}

0 comments on commit 2fe5c3e

Please sign in to comment.