Browse Source

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.
Carl Jackson 11 years ago
parent
commit
2fe5c3ee43
5 changed files with 42 additions and 41 deletions
  1. +4
    -4
      example/main.go
  2. +8
    -5
      web/mux.go
  3. +8
    -16
      web/pattern_test.go
  4. +1
    -1
      web/router_test.go
  5. +21
    -15
      web/string_pattern.go

+ 4
- 4
example/main.go View File

@ -39,10 +39,10 @@ func main() {
// can put them wherever you like. // can put them wherever you like.
goji.Use(PlainText) 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() admin := web.New()
goji.Handle("/admin/*", admin) goji.Handle("/admin/*", admin)
admin.Use(SuperSecure) admin.Use(SuperSecure)


+ 8
- 5
web/mux.go View File

@ -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 - a path segment starting with with a colon will match any
string placed at that position. e.g., "/:name" will match string placed at that position. e.g., "/:name" will match
"/carl", binding "name" to "carl". "/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 - 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 is anchored on the left (i.e., the beginning of the string). If your
regexp is not anchored on the left, a hopefully-identical regexp is not anchored on the left, a hopefully-identical


+ 8
- 16
web/pattern_test.go View File

@ -127,37 +127,29 @@ var patternTests = []struct {
}}, }},
// String prefix tests // String prefix tests
{parseStringPattern("/user/:user*"),
{parseStringPattern("/user/:user/*"),
"/user/", []patternTest{ "/user/", []patternTest{
pt("/user/bob", true, map[string]string{
pt("/user/bob/", true, map[string]string{
"user": "bob", "user": "bob",
"*": "/",
}), }),
pt("/user/bob/friends/123", true, map[string]string{ pt("/user/bob/friends/123", true, map[string]string{
"user": "bob", "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/bob", false, nil),
pt("/user/", false, nil), pt("/user/", false, nil),
pt("/user//", false, nil), pt("/user//", false, nil),
}}, }},
{parseStringPattern("/user/:user/friends*"),
{parseStringPattern("/user/:user/friends/*"),
"/user/", []patternTest{ "/user/", []patternTest{
pt("/user/bob/friends", true, map[string]string{
pt("/user/bob/friends/", true, map[string]string{
"user": "bob", "user": "bob",
"*": "/",
}), }),
pt("/user/bob/friends/123", true, map[string]string{ pt("/user/bob/friends/123", true, map[string]string{
"user": "bob", "user": "bob",
}),
// This is a little unfortunate
pt("/user/bob/friends123", true, map[string]string{
"user": "bob",
"*": "/123",
}), }),
pt("/user/bob/enemies", false, nil), pt("/user/bob/enemies", false, nil),
}}, }},


+ 1
- 1
web/router_test.go View File

@ -244,7 +244,7 @@ func TestPrefix(t *testing.T) {
m := New() m := New()
ch := make(chan string, 1) 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 ch <- r.URL.Path
}) })


+ 21
- 15
web/string_pattern.go View File

@ -13,7 +13,7 @@ type stringPattern struct {
pats []string pats []string
breaks []byte breaks []byte
literals []string literals []string
isPrefix bool
wildcard bool
} }
func (s stringPattern) Prefix() string { 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 { func (s stringPattern) match(r *http.Request, c *C, dryrun bool) bool {
path := r.URL.Path path := r.URL.Path
var matches map[string]string 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++ { for i := 0; i < len(s.pats); i++ {
sli := s.literals[i] sli := s.literals[i]
@ -56,14 +60,16 @@ func (s stringPattern) match(r *http.Request, c *C, dryrun bool) bool {
path = path[m:] path = path[m:]
} }
// There's exactly one more literal than pat. // 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 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 { 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 { 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 // "Break characters" are characters that can end patterns. They are not allowed
@ -93,11 +99,11 @@ const bc = "/.;,"
var patternRe = regexp.MustCompile(`[` + bc + `]:([^` + bc + `]+)`) var patternRe = regexp.MustCompile(`[` + bc + `]:([^` + bc + `]+)`)
func parseStringPattern(s string) 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] == '*' {
raw := s
var wildcard bool
if strings.HasSuffix(s, "/*") {
s = s[:len(s)-1] s = s[:len(s)-1]
isPrefix = true
wildcard = true
} }
matches := patternRe.FindAllStringSubmatchIndex(s, -1) matches := patternRe.FindAllStringSubmatchIndex(s, -1)
@ -118,10 +124,10 @@ func parseStringPattern(s string) stringPattern {
} }
literals[len(matches)] = s[n:] literals[len(matches)] = s[n:]
return stringPattern{ return stringPattern{
raw: s,
raw: raw,
pats: pats, pats: pats,
breaks: breaks, breaks: breaks,
literals: literals, literals: literals,
isPrefix: isPrefix,
wildcard: wildcard,
} }
} }

Loading…
Cancel
Save