Browse Source

Add ability to capture variables in query strings

pull/55/head
Raphael Simon 12 years ago
parent
commit
65cc9b5df8
  1. 18
      mux_test.go
  2. 2
      old_test.go
  3. 37
      regexp.go
  4. 72
      route.go

18
mux_test.go

@ -471,6 +471,24 @@ func TestQueries(t *testing.T) {
path: "", path: "",
shouldMatch: false, shouldMatch: false,
}, },
{
title: "Queries route with pattern, match",
route: new(Route).Queries("foo", "{v1}"),
request: newRequest("GET", "http://localhost?foo=bar"),
vars: map[string]string{"v1": "bar"},
host: "",
path: "",
shouldMatch: true,
},
{
title: "Queries route with multiple patterns, match",
route: new(Route).Queries("foo", "{v1}", "baz", "{v2}"),
request: newRequest("GET", "http://localhost?foo=bar&baz=ding"),
vars: map[string]string{"v1": "bar", "v2": "ding"},
host: "",
path: "",
shouldMatch: true,
},
} }
for _, test := range tests { for _, test := range tests {

2
old_test.go

@ -735,7 +735,7 @@ func TestNewRegexp(t *testing.T) {
} }
for pattern, paths := range tests { for pattern, paths := range tests {
p, _ = newRouteRegexp(pattern, false, false, false) p, _ = newRouteRegexp(pattern, false, false, false, false)
for path, result := range paths { for path, result := range paths {
matches = p.regexp.FindStringSubmatch(path) matches = p.regexp.FindStringSubmatch(path)
if result == nil { if result == nil {

37
regexp.go

@ -14,7 +14,7 @@ import (
) )
// newRouteRegexp parses a route template and returns a routeRegexp, // newRouteRegexp parses a route template and returns a routeRegexp,
// used to match a host or path. // used to match a host, a path or a query string.
// //
// It will extract named variables, assemble a regexp to be matched, create // It will extract named variables, assemble a regexp to be matched, create
// a "reverse" template to build URLs and compile regexps to validate variable // a "reverse" template to build URLs and compile regexps to validate variable
@ -23,7 +23,7 @@ import (
// Previously we accepted only Python-like identifiers for variable // Previously we accepted only Python-like identifiers for variable
// names ([a-zA-Z_][a-zA-Z0-9_]*), but currently the only restriction is that // names ([a-zA-Z_][a-zA-Z0-9_]*), but currently the only restriction is that
// name and pattern can't be empty, and names can't contain a colon. // name and pattern can't be empty, and names can't contain a colon.
func newRouteRegexp(tpl string, matchHost, matchPrefix, strictSlash bool) (*routeRegexp, error) { func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash bool) (*routeRegexp, error) {
// Check if it is well-formed. // Check if it is well-formed.
idxs, errBraces := braceIndices(tpl) idxs, errBraces := braceIndices(tpl)
if errBraces != nil { if errBraces != nil {
@ -33,7 +33,10 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, strictSlash bool) (*rout
template := tpl template := tpl
// Now let's parse it. // Now let's parse it.
defaultPattern := "[^/]+" defaultPattern := "[^/]+"
if matchHost { if matchQuery {
defaultPattern = "[^?]+"
matchPrefix, strictSlash = true, false
} else if matchHost {
defaultPattern = "[^.]+" defaultPattern = "[^.]+"
matchPrefix, strictSlash = false, false matchPrefix, strictSlash = false, false
} }
@ -49,6 +52,9 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, strictSlash bool) (*rout
varsN := make([]string, len(idxs)/2) varsN := make([]string, len(idxs)/2)
varsR := make([]*regexp.Regexp, len(idxs)/2) varsR := make([]*regexp.Regexp, len(idxs)/2)
pattern := bytes.NewBufferString("^") pattern := bytes.NewBufferString("^")
if matchQuery {
pattern = bytes.NewBufferString("")
}
reverse := bytes.NewBufferString("") reverse := bytes.NewBufferString("")
var end int var end int
var err error var err error
@ -100,6 +106,7 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, strictSlash bool) (*rout
return &routeRegexp{ return &routeRegexp{
template: template, template: template,
matchHost: matchHost, matchHost: matchHost,
matchQuery: matchQuery,
strictSlash: strictSlash, strictSlash: strictSlash,
regexp: reg, regexp: reg,
reverse: reverse.String(), reverse: reverse.String(),
@ -113,8 +120,10 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, strictSlash bool) (*rout
type routeRegexp struct { type routeRegexp struct {
// The unmodified template. // The unmodified template.
template string template string
// True for host match, false for path match. // True for host match, false for path or query string match.
matchHost bool matchHost bool
// True for query string match, false for path and host match.
matchQuery bool
// The strictSlash value defined on the route, but disabled if PathPrefix was used. // The strictSlash value defined on the route, but disabled if PathPrefix was used.
strictSlash bool strictSlash bool
// Expanded regexp. // Expanded regexp.
@ -130,7 +139,11 @@ type routeRegexp struct {
// Match matches the regexp against the URL host or path. // Match matches the regexp against the URL host or path.
func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool { func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool {
if !r.matchHost { if !r.matchHost {
return r.regexp.MatchString(req.URL.Path) if r.matchQuery {
return r.regexp.MatchString(req.URL.RawQuery)
} else {
return r.regexp.MatchString(req.URL.Path)
}
} }
return r.regexp.MatchString(getHost(req)) return r.regexp.MatchString(getHost(req))
} }
@ -196,8 +209,9 @@ func braceIndices(s string) ([]int, error) {
// routeRegexpGroup groups the route matchers that carry variables. // routeRegexpGroup groups the route matchers that carry variables.
type routeRegexpGroup struct { type routeRegexpGroup struct {
host *routeRegexp host *routeRegexp
path *routeRegexp path *routeRegexp
query *routeRegexp
} }
// setMatch extracts the variables from the URL once a route matches. // setMatch extracts the variables from the URL once a route matches.
@ -234,6 +248,15 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route)
} }
} }
} }
// Store query string variables.
if v.query != nil {
queryVars := v.query.regexp.FindStringSubmatch(req.URL.RawQuery)
if queryVars != nil {
for k, v := range v.query.varsN {
m.Vars[v] = queryVars[k+1]
}
}
}
} }
// getHost tries its best to return the request host. // getHost tries its best to return the request host.

72
route.go

@ -5,6 +5,7 @@
package mux package mux
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
@ -135,12 +136,12 @@ func (r *Route) addMatcher(m matcher) *Route {
} }
// addRegexpMatcher adds a host or path matcher and builder to a route. // addRegexpMatcher adds a host or path matcher and builder to a route.
func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix bool) error { func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix, matchQuery bool) error {
if r.err != nil { if r.err != nil {
return r.err return r.err
} }
r.regexp = r.getRegexpGroup() r.regexp = r.getRegexpGroup()
if !matchHost { if !matchHost && !matchQuery {
if len(tpl) == 0 || tpl[0] != '/' { if len(tpl) == 0 || tpl[0] != '/' {
return fmt.Errorf("mux: path must start with a slash, got %q", tpl) return fmt.Errorf("mux: path must start with a slash, got %q", tpl)
} }
@ -148,7 +149,7 @@ func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix bool) error
tpl = strings.TrimRight(r.regexp.path.template, "/") + tpl tpl = strings.TrimRight(r.regexp.path.template, "/") + tpl
} }
} }
rr, err := newRouteRegexp(tpl, matchHost, matchPrefix, r.strictSlash) rr, err := newRouteRegexp(tpl, matchHost, matchPrefix, matchQuery, r.strictSlash)
if err != nil { if err != nil {
return err return err
} }
@ -158,6 +159,11 @@ func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix bool) error
return err return err
} }
} }
if r.regexp.query != nil {
if err = uniqueVars(rr.varsN, r.regexp.query.varsN); err != nil {
return err
}
}
r.regexp.host = rr r.regexp.host = rr
} else { } else {
if r.regexp.host != nil { if r.regexp.host != nil {
@ -165,7 +171,21 @@ func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix bool) error
return err return err
} }
} }
r.regexp.path = rr if matchQuery {
if r.regexp.path != nil {
if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil {
return err
}
}
r.regexp.query = rr
} else {
if r.regexp.query != nil {
if err = uniqueVars(rr.varsN, r.regexp.query.varsN); err != nil {
return err
}
}
r.regexp.path = rr
}
} }
r.addMatcher(rr) r.addMatcher(rr)
return nil return nil
@ -219,7 +239,7 @@ func (r *Route) Headers(pairs ...string) *Route {
// Variable names must be unique in a given route. They can be retrieved // Variable names must be unique in a given route. They can be retrieved
// calling mux.Vars(request). // calling mux.Vars(request).
func (r *Route) Host(tpl string) *Route { func (r *Route) Host(tpl string) *Route {
r.err = r.addRegexpMatcher(tpl, true, false) r.err = r.addRegexpMatcher(tpl, true, false, false)
return r return r
} }
@ -278,7 +298,7 @@ func (r *Route) Methods(methods ...string) *Route {
// Variable names must be unique in a given route. They can be retrieved // Variable names must be unique in a given route. They can be retrieved
// calling mux.Vars(request). // calling mux.Vars(request).
func (r *Route) Path(tpl string) *Route { func (r *Route) Path(tpl string) *Route {
r.err = r.addRegexpMatcher(tpl, false, false) r.err = r.addRegexpMatcher(tpl, false, false, false)
return r return r
} }
@ -294,35 +314,40 @@ func (r *Route) Path(tpl string) *Route {
// Also note that the setting of Router.StrictSlash() has no effect on routes // Also note that the setting of Router.StrictSlash() has no effect on routes
// with a PathPrefix matcher. // with a PathPrefix matcher.
func (r *Route) PathPrefix(tpl string) *Route { func (r *Route) PathPrefix(tpl string) *Route {
r.err = r.addRegexpMatcher(tpl, false, true) r.err = r.addRegexpMatcher(tpl, false, true, false)
return r return r
} }
// Query ---------------------------------------------------------------------- // Query ----------------------------------------------------------------------
// queryMatcher matches the request against URL queries.
type queryMatcher map[string]string
func (m queryMatcher) Match(r *http.Request, match *RouteMatch) bool {
return matchMap(m, r.URL.Query(), false)
}
// Queries adds a matcher for URL query values. // Queries adds a matcher for URL query values.
// It accepts a sequence of key/value pairs. For example: // It accepts a sequence of key/value pairs. Values may define variables.
// For example:
// //
// r := mux.NewRouter() // r := mux.NewRouter()
// r.Queries("foo", "bar", "baz", "ding") // r.Queries("foo", "bar", "id", "{id:[0-9]+}")
// //
// The above route will only match if the URL contains the defined queries // The above route will only match if the URL contains the defined queries
// values, e.g.: ?foo=bar&baz=ding. // values, e.g.: ?foo=bar&id=42.
// //
// It the value is an empty string, it will match any value if the key is set. // It the value is an empty string, it will match any value if the key is set.
//
// Variables can define an optional regexp pattern to me matched:
//
// - {name} matches anything until the next slash.
//
// - {name:pattern} matches the given regexp pattern.
func (r *Route) Queries(pairs ...string) *Route { func (r *Route) Queries(pairs ...string) *Route {
if r.err == nil { var buf bytes.Buffer
var queries map[string]string var queries map[string]string
queries, r.err = mapFromPairs(pairs...) buf.WriteString("")
return r.addMatcher(queryMatcher(queries)) queries, r.err = mapFromPairs(pairs...)
for k, v := range queries {
buf.WriteString(fmt.Sprintf("%s=%s&", k, v))
} }
tpl := strings.TrimRight(buf.String(), "&")
r.err = r.addRegexpMatcher(tpl, false, true, true)
return r return r
} }
@ -498,8 +523,9 @@ func (r *Route) getRegexpGroup() *routeRegexpGroup {
} else { } else {
// Copy. // Copy.
r.regexp = &routeRegexpGroup{ r.regexp = &routeRegexpGroup{
host: regexp.host, host: regexp.host,
path: regexp.path, path: regexp.path,
query: regexp.query,
} }
} }
} }

Loading…
Cancel
Save