Add ability to capture variables in query strings
This commit is contained in:
18
mux_test.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 {
|
||||||
|
|||||||
@@ -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
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
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user