From 65cc9b5df83a841607cf9f88c58cb6cdd8ab0421 Mon Sep 17 00:00:00 2001 From: Raphael Simon Date: Mon, 26 May 2014 15:01:42 -0700 Subject: [PATCH 1/3] Add ability to capture variables in query strings --- mux_test.go | 18 ++++++++++++++ old_test.go | 2 +- regexp.go | 37 +++++++++++++++++++++------ route.go | 72 ++++++++++++++++++++++++++++++++++++----------------- 4 files changed, 98 insertions(+), 31 deletions(-) diff --git a/mux_test.go b/mux_test.go index 0e2e480..c133d6c 100644 --- a/mux_test.go +++ b/mux_test.go @@ -471,6 +471,24 @@ 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, + }, } for _, test := range tests { diff --git a/old_test.go b/old_test.go index 4253059..4c773c6 100644 --- a/old_test.go +++ b/old_test.go @@ -735,7 +735,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 { diff --git a/regexp.go b/regexp.go index 925f268..f1d3147 100644 --- a/regexp.go +++ b/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. diff --git a/route.go b/route.go index 5cb2526..afe3e7f 100644 --- a/route.go +++ b/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,40 @@ 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)) + var buf bytes.Buffer + var queries map[string]string + buf.WriteString("") + 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 } @@ -498,8 +523,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, } } } From c9469524da00abe15b8d00bb1ee9621d85ebdfbc Mon Sep 17 00:00:00 2001 From: Raphael Simon Date: Mon, 26 May 2014 15:13:05 -0700 Subject: [PATCH 2/3] Fix old tests Remove tests that don't apply anymore Fix scheme matcher tests --- old_test.go | 45 +-------------------------------------------- 1 file changed, 1 insertion(+), 44 deletions(-) diff --git a/old_test.go b/old_test.go index 4c773c6..06ae0eb 100644 --- a/old_test.go +++ b/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) From 0a0d6a1b2a0c75b931495697ce6a2182f810ffb3 Mon Sep 17 00:00:00 2001 From: Raphael Simon Date: Mon, 26 May 2014 20:20:14 -0700 Subject: [PATCH 3/3] Add tests for regexp variables in query strings Fix how regular expression gets built for query string so that order of parameters is always preserved --- mux_test.go | 18 ++++++++++++++++++ route.go | 14 +++++++++----- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/mux_test.go b/mux_test.go index c133d6c..48506bf 100644 --- a/mux_test.go +++ b/mux_test.go @@ -489,6 +489,24 @@ func TestQueries(t *testing.T) { 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 { diff --git a/route.go b/route.go index afe3e7f..00989bf 100644 --- a/route.go +++ b/route.go @@ -339,15 +339,19 @@ func (r *Route) PathPrefix(tpl string) *Route { // - {name:pattern} matches the given regexp pattern. func (r *Route) Queries(pairs ...string) *Route { + 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 - var queries map[string]string - buf.WriteString("") - queries, r.err = mapFromPairs(pairs...) - for k, v := range queries { - buf.WriteString(fmt.Sprintf("%s=%s&", k, v)) + 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 }