Merge pull request #55 from raphael/query-vars
Add ability to capture variables in query strings
This commit is contained in:
36
mux_test.go
36
mux_test.go
@@ -471,6 +471,42 @@ func TestQueries(t *testing.T) {
|
||||
path: "",
|
||||
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,
|
||||
},
|
||||
{
|
||||
title: "Queries route with regexp pattern, match",
|
||||
route: new(Route).Queries("foo", "{v1:[0-9]+}"),
|
||||
request: newRequest("GET", "http://localhost?foo=10"),
|
||||
vars: map[string]string{"v1": "10"},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
title: "Queries route with regexp pattern, regexp does not match",
|
||||
route: new(Route).Queries("foo", "{v1:[0-9]+}"),
|
||||
request: newRequest("GET", "http://localhost?foo=a"),
|
||||
vars: map[string]string{},
|
||||
host: "",
|
||||
path: "",
|
||||
shouldMatch: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
47
old_test.go
47
old_test.go
@@ -329,35 +329,6 @@ var pathMatcherTests = []pathMatcherTest{
|
||||
},
|
||||
}
|
||||
|
||||
type queryMatcherTest struct {
|
||||
matcher queryMatcher
|
||||
url string
|
||||
result bool
|
||||
}
|
||||
|
||||
var queryMatcherTests = []queryMatcherTest{
|
||||
{
|
||||
matcher: queryMatcher(map[string]string{"foo": "bar", "baz": "ding"}),
|
||||
url: "http://localhost:8080/?foo=bar&baz=ding",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
matcher: queryMatcher(map[string]string{"foo": "", "baz": ""}),
|
||||
url: "http://localhost:8080/?foo=anything&baz=anything",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
matcher: queryMatcher(map[string]string{"foo": "ding", "baz": "bar"}),
|
||||
url: "http://localhost:8080/?foo=bar&baz=ding",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
matcher: queryMatcher(map[string]string{"bar": "foo", "ding": "baz"}),
|
||||
url: "http://localhost:8080/?foo=bar&baz=ding",
|
||||
result: false,
|
||||
},
|
||||
}
|
||||
|
||||
type schemeMatcherTest struct {
|
||||
matcher schemeMatcher
|
||||
url string
|
||||
@@ -519,23 +490,9 @@ func TestPathMatcher(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryMatcher(t *testing.T) {
|
||||
for _, v := range queryMatcherTests {
|
||||
request, _ := http.NewRequest("GET", v.url, nil)
|
||||
var routeMatch RouteMatch
|
||||
result := v.matcher.Match(request, &routeMatch)
|
||||
if result != v.result {
|
||||
if v.result {
|
||||
t.Errorf("%#v: should match %v.", v.matcher, v.url)
|
||||
} else {
|
||||
t.Errorf("%#v: should not match %v.", v.matcher, v.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchemeMatcher(t *testing.T) {
|
||||
for _, v := range queryMatcherTests {
|
||||
for _, v := range schemeMatcherTests {
|
||||
request, _ := http.NewRequest("GET", v.url, nil)
|
||||
var routeMatch RouteMatch
|
||||
result := v.matcher.Match(request, &routeMatch)
|
||||
@@ -735,7 +692,7 @@ func TestNewRegexp(t *testing.T) {
|
||||
}
|
||||
|
||||
for pattern, paths := range tests {
|
||||
p, _ = newRouteRegexp(pattern, false, false, false)
|
||||
p, _ = newRouteRegexp(pattern, false, false, false, false)
|
||||
for path, result := range paths {
|
||||
matches = p.regexp.FindStringSubmatch(path)
|
||||
if result == nil {
|
||||
|
||||
37
regexp.go
37
regexp.go
@@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
// 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
|
||||
// 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
|
||||
// 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.
|
||||
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.
|
||||
idxs, errBraces := braceIndices(tpl)
|
||||
if errBraces != nil {
|
||||
@@ -33,7 +33,10 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, strictSlash bool) (*rout
|
||||
template := tpl
|
||||
// Now let's parse it.
|
||||
defaultPattern := "[^/]+"
|
||||
if matchHost {
|
||||
if matchQuery {
|
||||
defaultPattern = "[^?]+"
|
||||
matchPrefix, strictSlash = true, false
|
||||
} else if matchHost {
|
||||
defaultPattern = "[^.]+"
|
||||
matchPrefix, strictSlash = false, false
|
||||
}
|
||||
@@ -49,6 +52,9 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, strictSlash bool) (*rout
|
||||
varsN := make([]string, len(idxs)/2)
|
||||
varsR := make([]*regexp.Regexp, len(idxs)/2)
|
||||
pattern := bytes.NewBufferString("^")
|
||||
if matchQuery {
|
||||
pattern = bytes.NewBufferString("")
|
||||
}
|
||||
reverse := bytes.NewBufferString("")
|
||||
var end int
|
||||
var err error
|
||||
@@ -100,6 +106,7 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, strictSlash bool) (*rout
|
||||
return &routeRegexp{
|
||||
template: template,
|
||||
matchHost: matchHost,
|
||||
matchQuery: matchQuery,
|
||||
strictSlash: strictSlash,
|
||||
regexp: reg,
|
||||
reverse: reverse.String(),
|
||||
@@ -113,8 +120,10 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, strictSlash bool) (*rout
|
||||
type routeRegexp struct {
|
||||
// The unmodified template.
|
||||
template string
|
||||
// True for host match, false for path match.
|
||||
// True for host match, false for path or query string match.
|
||||
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.
|
||||
strictSlash bool
|
||||
// Expanded regexp.
|
||||
@@ -130,7 +139,11 @@ type routeRegexp struct {
|
||||
// Match matches the regexp against the URL host or path.
|
||||
func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool {
|
||||
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))
|
||||
}
|
||||
@@ -196,8 +209,9 @@ func braceIndices(s string) ([]int, error) {
|
||||
|
||||
// routeRegexpGroup groups the route matchers that carry variables.
|
||||
type routeRegexpGroup struct {
|
||||
host *routeRegexp
|
||||
path *routeRegexp
|
||||
host *routeRegexp
|
||||
path *routeRegexp
|
||||
query *routeRegexp
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
||||
76
route.go
76
route.go
@@ -5,6 +5,7 @@
|
||||
package mux
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"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.
|
||||
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 {
|
||||
return r.err
|
||||
}
|
||||
r.regexp = r.getRegexpGroup()
|
||||
if !matchHost {
|
||||
if !matchHost && !matchQuery {
|
||||
if len(tpl) == 0 || tpl[0] != '/' {
|
||||
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
|
||||
}
|
||||
}
|
||||
rr, err := newRouteRegexp(tpl, matchHost, matchPrefix, r.strictSlash)
|
||||
rr, err := newRouteRegexp(tpl, matchHost, matchPrefix, matchQuery, r.strictSlash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -158,6 +159,11 @@ func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix bool) error
|
||||
return err
|
||||
}
|
||||
}
|
||||
if r.regexp.query != nil {
|
||||
if err = uniqueVars(rr.varsN, r.regexp.query.varsN); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
r.regexp.host = rr
|
||||
} else {
|
||||
if r.regexp.host != nil {
|
||||
@@ -165,7 +171,21 @@ func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix bool) error
|
||||
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)
|
||||
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
|
||||
// calling mux.Vars(request).
|
||||
func (r *Route) Host(tpl string) *Route {
|
||||
r.err = r.addRegexpMatcher(tpl, true, false)
|
||||
r.err = r.addRegexpMatcher(tpl, true, false, false)
|
||||
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
|
||||
// calling mux.Vars(request).
|
||||
func (r *Route) Path(tpl string) *Route {
|
||||
r.err = r.addRegexpMatcher(tpl, false, false)
|
||||
r.err = r.addRegexpMatcher(tpl, false, false, false)
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -294,35 +314,44 @@ func (r *Route) Path(tpl string) *Route {
|
||||
// Also note that the setting of Router.StrictSlash() has no effect on routes
|
||||
// with a PathPrefix matcher.
|
||||
func (r *Route) PathPrefix(tpl string) *Route {
|
||||
r.err = r.addRegexpMatcher(tpl, false, true)
|
||||
r.err = r.addRegexpMatcher(tpl, false, true, false)
|
||||
return r
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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.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
|
||||
// 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.
|
||||
//
|
||||
// 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 {
|
||||
if r.err == nil {
|
||||
var queries map[string]string
|
||||
queries, r.err = mapFromPairs(pairs...)
|
||||
return r.addMatcher(queryMatcher(queries))
|
||||
length := len(pairs)
|
||||
if length%2 != 0 {
|
||||
r.err = fmt.Errorf(
|
||||
"mux: number of parameters must be multiple of 2, got %v", pairs)
|
||||
return nil
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
for i := 0; i < length; i += 2 {
|
||||
buf.WriteString(fmt.Sprintf("%s=%s&", pairs[i], pairs[i+1]))
|
||||
}
|
||||
tpl := strings.TrimRight(buf.String(), "&")
|
||||
r.err = r.addRegexpMatcher(tpl, false, true, true)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -498,8 +527,9 @@ func (r *Route) getRegexpGroup() *routeRegexpGroup {
|
||||
} else {
|
||||
// Copy.
|
||||
r.regexp = &routeRegexpGroup{
|
||||
host: regexp.host,
|
||||
path: regexp.path,
|
||||
host: regexp.host,
|
||||
path: regexp.path,
|
||||
query: regexp.query,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user