From a063f14812c8ea1ae2a62a8d9b2307afe23348a7 Mon Sep 17 00:00:00 2001 From: Craig Jellick Date: Fri, 29 May 2015 14:16:30 -0700 Subject: [PATCH 01/35] Fix typo Issue #16: Added regex support for matching headers Issue #16 : Addressed code review and refactored support for regex into a separate function Added compiled regex to route matcher --- doc.go | 7 +++++++ mux.go | 60 ++++++++++++++++++++++++++++++++++++++++++++++++----- mux_test.go | 18 ++++++++++++++++ route.go | 44 +++++++++++++++++++++++++++++++++------ 4 files changed, 118 insertions(+), 11 deletions(-) diff --git a/doc.go b/doc.go index 9a5e381..442baba 100644 --- a/doc.go +++ b/doc.go @@ -172,6 +172,13 @@ conform to the corresponding patterns. These requirements guarantee that a generated URL will always match a registered route -- the only exception is for explicitly defined "build-only" routes which never match. +Regex support also exists for matching Headers within a route. For example, we could do: + + r.HeadersRegexp("Content-Type", "application/(text|json)") + +...and the route will match both requests with a Content-Type of `application/json` as well as +`application/text` + There's also a way to build only the URL host or path for a route: use the methods URLHost() or URLPath() instead. For the previous route, we would do: diff --git a/mux.go b/mux.go index af31d23..f7e3c22 100644 --- a/mux.go +++ b/mux.go @@ -8,6 +8,7 @@ import ( "fmt" "net/http" "path" + "regexp" "github.com/gorilla/context" ) @@ -313,13 +314,21 @@ func uniqueVars(s1, s2 []string) error { return nil } -// mapFromPairs converts variadic string parameters to a string map. -func mapFromPairs(pairs ...string) (map[string]string, error) { +func checkPairs(pairs ...string) (int, error) { length := len(pairs) if length%2 != 0 { - return nil, fmt.Errorf( + return length, fmt.Errorf( "mux: number of parameters must be multiple of 2, got %v", pairs) } + return length, nil +} + +// mapFromPairs converts variadic string parameters to a string map. +func mapFromPairsToString(pairs ...string) (map[string]string, error) { + length, err := checkPairs(pairs...) + if err != nil { + return nil, err + } m := make(map[string]string, length/2) for i := 0; i < length; i += 2 { m[pairs[i]] = pairs[i+1] @@ -327,6 +336,19 @@ func mapFromPairs(pairs ...string) (map[string]string, error) { return m, nil } +func mapFromPairsToRegex(pairs ...string) (map[string]*regexp.Regexp, error) { + length, err := checkPairs(pairs...) + if err != nil { + return nil, err + } + m := make(map[string]*regexp.Regexp, length/2) + for i := 0; i < length; i += 2 { + regex, _ := regexp.Compile(pairs[i+1]) + m[pairs[i]] = regex + } + return m, nil +} + // matchInArray returns true if the given string value is in the array. func matchInArray(arr []string, value string) bool { for _, v := range arr { @@ -337,9 +359,10 @@ func matchInArray(arr []string, value string) bool { return false } +type equals func(interface{}, interface{}) bool + // matchMap returns true if the given key/value pairs exist in a given map. -func matchMap(toCheck map[string]string, toMatch map[string][]string, - canonicalKey bool) bool { +func matchMapWithString(toCheck map[string]string, toMatch map[string][]string, canonicalKey bool) bool { for k, v := range toCheck { // Check if key exists. if canonicalKey { @@ -364,3 +387,30 @@ func matchMap(toCheck map[string]string, toMatch map[string][]string, } return true } + +// matchMap returns true if the given key/value pairs exist in a given map. +func matchMapWithRegex(toCheck map[string]*regexp.Regexp, toMatch map[string][]string, canonicalKey bool) bool { + for k, v := range toCheck { + // Check if key exists. + if canonicalKey { + k = http.CanonicalHeaderKey(k) + } + if values := toMatch[k]; values == nil { + return false + } else if v != nil { + // If value was defined as an empty string we only check that the + // key exists. Otherwise we also check for equality. + valueExists := false + for _, value := range values { + if v.MatchString(value) { + valueExists = true + break + } + } + if !valueExists { + return false + } + } + } + return true +} diff --git a/mux_test.go b/mux_test.go index 6b2c1d2..67f13e4 100644 --- a/mux_test.go +++ b/mux_test.go @@ -434,6 +434,24 @@ func TestHeaders(t *testing.T) { path: "", shouldMatch: false, }, + { + title: "Headers route, regex header values to match", + route: new(Route).Headers("foo", "ba[zr]"), + request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "bar"}), + vars: map[string]string{}, + host: "", + path: "", + shouldMatch: false, + }, + { + title: "Headers route, regex header values to match", + route: new(Route).HeadersRegexp("foo", "ba[zr]"), + request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "baz"}), + vars: map[string]string{}, + host: "", + path: "", + shouldMatch: true, + }, } for _, test := range tests { diff --git a/route.go b/route.go index d4f0146..e81723e 100644 --- a/route.go +++ b/route.go @@ -9,6 +9,7 @@ import ( "fmt" "net/http" "net/url" + "regexp" "strings" ) @@ -188,7 +189,7 @@ func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix, matchQuery type headerMatcher map[string]string func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool { - return matchMap(m, r.Header, true) + return matchMapWithString(m, r.Header, true) } // Headers adds a matcher for request header values. @@ -199,22 +200,53 @@ func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool { // "X-Requested-With", "XMLHttpRequest") // // The above route will only match if both request header values match. +// Alternatively, you can provide a regular expression and match the header as follows: +// +// r.Headers("Content-Type", "application/(text|json)", +// "X-Requested-With", "XMLHttpRequest") +// +// The above route will the same as the previous example, with the addition of matching +// application/text as well. // // It the value is an empty string, it will match any value if the key is set. func (r *Route) Headers(pairs ...string) *Route { if r.err == nil { var headers map[string]string - headers, r.err = mapFromPairs(pairs...) + headers, r.err = mapFromPairsToString(pairs...) return r.addMatcher(headerMatcher(headers)) } return r } +// headerRegexMatcher matches the request against the route given a regex for the header +type headerRegexMatcher map[string]*regexp.Regexp + +func (m headerRegexMatcher) Match(r *http.Request, match *RouteMatch) bool { + return matchMapWithRegex(m, r.Header, true) +} + +// Regular expressions can be used with headers as well. +// It accepts a sequence of key/value pairs, where the value has regex support. For example +// r := mux.NewRouter() +// r.HeadersRegexp("Content-Type", "application/(text|json)", +// "X-Requested-With", "XMLHttpRequest") +// +// The above route will only match if both the request header matches both regular expressions. +// It the value is an empty string, it will match any value if the key is set. +func (r *Route) HeadersRegexp(pairs ...string) *Route { + if r.err == nil { + var headers map[string]*regexp.Regexp + headers, r.err = mapFromPairsToRegex(pairs...) + return r.addMatcher(headerRegexMatcher(headers)) + } + return r +} + // Host ----------------------------------------------------------------------- // Host adds a matcher for the URL host. // It accepts a template with zero or more URL variables enclosed by {}. -// Variables can define an optional regexp pattern to me matched: +// Variables can define an optional regexp pattern to be matched: // // - {name} matches anything until the next dot. // @@ -272,7 +304,7 @@ func (r *Route) Methods(methods ...string) *Route { // Path adds a matcher for the URL path. // It accepts a template with zero or more URL variables enclosed by {}. The // template must start with a "/". -// Variables can define an optional regexp pattern to me matched: +// Variables can define an optional regexp pattern to be matched: // // - {name} matches anything until the next slash. // @@ -323,7 +355,7 @@ func (r *Route) PathPrefix(tpl string) *Route { // // 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: +// Variables can define an optional regexp pattern to be matched: // // - {name} matches anything until the next slash. // @@ -511,7 +543,7 @@ func (r *Route) URLPath(pairs ...string) (*url.URL, error) { // prepareVars converts the route variable pairs into a map. If the route has a // BuildVarsFunc, it is invoked. func (r *Route) prepareVars(pairs ...string) (map[string]string, error) { - m, err := mapFromPairs(pairs...) + m, err := mapFromPairsToString(pairs...) if err != nil { return nil, err } From c0a5cbce5acc7d44030541b8b383b1ebcdfcc96f Mon Sep 17 00:00:00 2001 From: Craig Jellick Date: Fri, 29 May 2015 14:16:30 -0700 Subject: [PATCH 02/35] Fix typo Issue #16: Added regex support for matching headers Issue #16 : Addressed code review and refactored support for regex into a separate function Added compiled regex to route matcher --- doc.go | 7 +++++++ mux.go | 60 ++++++++++++++++++++++++++++++++++++++++++++++++----- mux_test.go | 18 ++++++++++++++++ route.go | 44 +++++++++++++++++++++++++++++++++------ 4 files changed, 118 insertions(+), 11 deletions(-) diff --git a/doc.go b/doc.go index 9a5e381..442baba 100644 --- a/doc.go +++ b/doc.go @@ -172,6 +172,13 @@ conform to the corresponding patterns. These requirements guarantee that a generated URL will always match a registered route -- the only exception is for explicitly defined "build-only" routes which never match. +Regex support also exists for matching Headers within a route. For example, we could do: + + r.HeadersRegexp("Content-Type", "application/(text|json)") + +...and the route will match both requests with a Content-Type of `application/json` as well as +`application/text` + There's also a way to build only the URL host or path for a route: use the methods URLHost() or URLPath() instead. For the previous route, we would do: diff --git a/mux.go b/mux.go index af31d23..f7e3c22 100644 --- a/mux.go +++ b/mux.go @@ -8,6 +8,7 @@ import ( "fmt" "net/http" "path" + "regexp" "github.com/gorilla/context" ) @@ -313,13 +314,21 @@ func uniqueVars(s1, s2 []string) error { return nil } -// mapFromPairs converts variadic string parameters to a string map. -func mapFromPairs(pairs ...string) (map[string]string, error) { +func checkPairs(pairs ...string) (int, error) { length := len(pairs) if length%2 != 0 { - return nil, fmt.Errorf( + return length, fmt.Errorf( "mux: number of parameters must be multiple of 2, got %v", pairs) } + return length, nil +} + +// mapFromPairs converts variadic string parameters to a string map. +func mapFromPairsToString(pairs ...string) (map[string]string, error) { + length, err := checkPairs(pairs...) + if err != nil { + return nil, err + } m := make(map[string]string, length/2) for i := 0; i < length; i += 2 { m[pairs[i]] = pairs[i+1] @@ -327,6 +336,19 @@ func mapFromPairs(pairs ...string) (map[string]string, error) { return m, nil } +func mapFromPairsToRegex(pairs ...string) (map[string]*regexp.Regexp, error) { + length, err := checkPairs(pairs...) + if err != nil { + return nil, err + } + m := make(map[string]*regexp.Regexp, length/2) + for i := 0; i < length; i += 2 { + regex, _ := regexp.Compile(pairs[i+1]) + m[pairs[i]] = regex + } + return m, nil +} + // matchInArray returns true if the given string value is in the array. func matchInArray(arr []string, value string) bool { for _, v := range arr { @@ -337,9 +359,10 @@ func matchInArray(arr []string, value string) bool { return false } +type equals func(interface{}, interface{}) bool + // matchMap returns true if the given key/value pairs exist in a given map. -func matchMap(toCheck map[string]string, toMatch map[string][]string, - canonicalKey bool) bool { +func matchMapWithString(toCheck map[string]string, toMatch map[string][]string, canonicalKey bool) bool { for k, v := range toCheck { // Check if key exists. if canonicalKey { @@ -364,3 +387,30 @@ func matchMap(toCheck map[string]string, toMatch map[string][]string, } return true } + +// matchMap returns true if the given key/value pairs exist in a given map. +func matchMapWithRegex(toCheck map[string]*regexp.Regexp, toMatch map[string][]string, canonicalKey bool) bool { + for k, v := range toCheck { + // Check if key exists. + if canonicalKey { + k = http.CanonicalHeaderKey(k) + } + if values := toMatch[k]; values == nil { + return false + } else if v != nil { + // If value was defined as an empty string we only check that the + // key exists. Otherwise we also check for equality. + valueExists := false + for _, value := range values { + if v.MatchString(value) { + valueExists = true + break + } + } + if !valueExists { + return false + } + } + } + return true +} diff --git a/mux_test.go b/mux_test.go index 6b2c1d2..67f13e4 100644 --- a/mux_test.go +++ b/mux_test.go @@ -434,6 +434,24 @@ func TestHeaders(t *testing.T) { path: "", shouldMatch: false, }, + { + title: "Headers route, regex header values to match", + route: new(Route).Headers("foo", "ba[zr]"), + request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "bar"}), + vars: map[string]string{}, + host: "", + path: "", + shouldMatch: false, + }, + { + title: "Headers route, regex header values to match", + route: new(Route).HeadersRegexp("foo", "ba[zr]"), + request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "baz"}), + vars: map[string]string{}, + host: "", + path: "", + shouldMatch: true, + }, } for _, test := range tests { diff --git a/route.go b/route.go index d4f0146..e81723e 100644 --- a/route.go +++ b/route.go @@ -9,6 +9,7 @@ import ( "fmt" "net/http" "net/url" + "regexp" "strings" ) @@ -188,7 +189,7 @@ func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix, matchQuery type headerMatcher map[string]string func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool { - return matchMap(m, r.Header, true) + return matchMapWithString(m, r.Header, true) } // Headers adds a matcher for request header values. @@ -199,22 +200,53 @@ func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool { // "X-Requested-With", "XMLHttpRequest") // // The above route will only match if both request header values match. +// Alternatively, you can provide a regular expression and match the header as follows: +// +// r.Headers("Content-Type", "application/(text|json)", +// "X-Requested-With", "XMLHttpRequest") +// +// The above route will the same as the previous example, with the addition of matching +// application/text as well. // // It the value is an empty string, it will match any value if the key is set. func (r *Route) Headers(pairs ...string) *Route { if r.err == nil { var headers map[string]string - headers, r.err = mapFromPairs(pairs...) + headers, r.err = mapFromPairsToString(pairs...) return r.addMatcher(headerMatcher(headers)) } return r } +// headerRegexMatcher matches the request against the route given a regex for the header +type headerRegexMatcher map[string]*regexp.Regexp + +func (m headerRegexMatcher) Match(r *http.Request, match *RouteMatch) bool { + return matchMapWithRegex(m, r.Header, true) +} + +// Regular expressions can be used with headers as well. +// It accepts a sequence of key/value pairs, where the value has regex support. For example +// r := mux.NewRouter() +// r.HeadersRegexp("Content-Type", "application/(text|json)", +// "X-Requested-With", "XMLHttpRequest") +// +// The above route will only match if both the request header matches both regular expressions. +// It the value is an empty string, it will match any value if the key is set. +func (r *Route) HeadersRegexp(pairs ...string) *Route { + if r.err == nil { + var headers map[string]*regexp.Regexp + headers, r.err = mapFromPairsToRegex(pairs...) + return r.addMatcher(headerRegexMatcher(headers)) + } + return r +} + // Host ----------------------------------------------------------------------- // Host adds a matcher for the URL host. // It accepts a template with zero or more URL variables enclosed by {}. -// Variables can define an optional regexp pattern to me matched: +// Variables can define an optional regexp pattern to be matched: // // - {name} matches anything until the next dot. // @@ -272,7 +304,7 @@ func (r *Route) Methods(methods ...string) *Route { // Path adds a matcher for the URL path. // It accepts a template with zero or more URL variables enclosed by {}. The // template must start with a "/". -// Variables can define an optional regexp pattern to me matched: +// Variables can define an optional regexp pattern to be matched: // // - {name} matches anything until the next slash. // @@ -323,7 +355,7 @@ func (r *Route) PathPrefix(tpl string) *Route { // // 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: +// Variables can define an optional regexp pattern to be matched: // // - {name} matches anything until the next slash. // @@ -511,7 +543,7 @@ func (r *Route) URLPath(pairs ...string) (*url.URL, error) { // prepareVars converts the route variable pairs into a map. If the route has a // BuildVarsFunc, it is invoked. func (r *Route) prepareVars(pairs ...string) (map[string]string, error) { - m, err := mapFromPairs(pairs...) + m, err := mapFromPairsToString(pairs...) if err != nil { return nil, err } From c21431a6cd004f2e4ac692b2ec8c56f3a4ebd036 Mon Sep 17 00:00:00 2001 From: Clint Ryan Date: Sun, 5 Jul 2015 20:18:38 +1000 Subject: [PATCH 03/35] Fixed up commenting and removed unused code --- mux.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mux.go b/mux.go index f7e3c22..aa2bedf 100644 --- a/mux.go +++ b/mux.go @@ -359,9 +359,7 @@ func matchInArray(arr []string, value string) bool { return false } -type equals func(interface{}, interface{}) bool - -// matchMap returns true if the given key/value pairs exist in a given map. +// matchMapWithString returns true if the given key/value pairs exist in a given map. func matchMapWithString(toCheck map[string]string, toMatch map[string][]string, canonicalKey bool) bool { for k, v := range toCheck { // Check if key exists. @@ -388,7 +386,8 @@ func matchMapWithString(toCheck map[string]string, toMatch map[string][]string, return true } -// matchMap returns true if the given key/value pairs exist in a given map. +// matchMapWithRegex returns true if the given key/value pairs exist in a given map compiled against +// the given regex func matchMapWithRegex(toCheck map[string]*regexp.Regexp, toMatch map[string][]string, canonicalKey bool) bool { for k, v := range toCheck { // Check if key exists. From 92ae1d67265b4cf52d659a340435398e8da2ad05 Mon Sep 17 00:00:00 2001 From: Dj Gilcrease Date: Thu, 9 Jul 2015 11:46:53 -0700 Subject: [PATCH 04/35] Update the walk method to walk matchers so it walks the full list of routers and child routers --- mux.go | 47 +++++++++++++++++++++++++++++++++ mux_test.go | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/mux.go b/mux.go index af31d23..ec1ef37 100644 --- a/mux.go +++ b/mux.go @@ -5,6 +5,7 @@ package mux import ( + "errors" "fmt" "net/http" "path" @@ -237,6 +238,52 @@ func (r *Router) BuildVarsFunc(f BuildVarsFunc) *Route { return r.NewRoute().BuildVarsFunc(f) } +// Walk walks the router and all its sub-routers, calling walkFn for each route +// in the tree. The routes are walked in the order they were added. Sub-routers +// are explored depth-first. +func (r *Router) Walk(walkFn WalkFunc) error { + return r.walk(walkFn, []*Route{}) +} + +// SkipRouter is used as a return value from WalkFuncs to indicate that the +// router that walk is about to descend down to should be skipped. +var SkipRouter = errors.New("skip this router") + +// WalkFunc is the type of the function called for each route visited by Walk. +// At every invocation, it is given the current route, and the current router, +// and a list of ancestor routes that lead to the current route. +type WalkFunc func(route *Route, router *Router, ancestors []*Route) error + +func (r *Router) walk(walkFn WalkFunc, ancestors []*Route) error { + for _, t := range r.routes { + if t.regexp == nil || t.regexp.path == nil || t.regexp.path.template == "" { + continue + } + + err := walkFn(t, r, ancestors) + if err == SkipRouter { + continue + } + for _, sr := range t.matchers { + if h, ok := sr.(*Router); ok { + err := h.walk(walkFn, ancestors) + if err != nil { + return err + } + } + } + if h, ok := t.handler.(*Router); ok { + ancestors = append(ancestors, t) + err := h.walk(walkFn, ancestors) + if err != nil { + return err + } + ancestors = ancestors[:len(ancestors)-1] + } + } + return nil +} + // ---------------------------------------------------------------------------- // Context // ---------------------------------------------------------------------------- diff --git a/mux_test.go b/mux_test.go index 8118aab..d8dc551 100644 --- a/mux_test.go +++ b/mux_test.go @@ -837,6 +837,81 @@ func TestStrictSlash(t *testing.T) { } } +func TestWalkSingleDepth(t *testing.T) { + r0 := NewRouter() + r1 := NewRouter() + r2 := NewRouter() + + r0.Path("/g") + r0.Path("/o") + r0.Path("/d").Handler(r1) + r0.Path("/r").Handler(r2) + r0.Path("/a") + + r1.Path("/z") + r1.Path("/i") + r1.Path("/l") + r1.Path("/l") + + r2.Path("/i") + r2.Path("/l") + r2.Path("/l") + + paths := []string{"g", "o", "r", "i", "l", "l", "a"} + depths := []int{0, 0, 0, 1, 1, 1, 0} + i := 0 + err := r0.Walk(func(route *Route, router *Router, ancestors []*Route) error { + matcher := route.matchers[0].(*routeRegexp) + if matcher.template == "/d" { + return SkipRouter + } + if len(ancestors) != depths[i] { + t.Errorf(`Expected depth of %d at i = %d; got "%s"`, depths[i], i, len(ancestors)) + } + if matcher.template != "/"+paths[i] { + t.Errorf(`Expected "/%s" at i = %d; got "%s"`, paths[i], i, matcher.template) + } + i++ + return nil + }) + if err != nil { + panic(err) + } + if i != len(paths) { + t.Errorf("Expected %d routes, found %d", len(paths), i) + } +} + +func TestWalkNested(t *testing.T) { + router := NewRouter() + + g := router.Path("/g").Subrouter() + o := g.PathPrefix("/o").Subrouter() + r := o.PathPrefix("/r").Subrouter() + i := r.PathPrefix("/i").Subrouter() + l1 := i.PathPrefix("/l").Subrouter() + l2 := l1.PathPrefix("/l").Subrouter() + l2.Path("/a") + + paths := []string{"/g", "/g/o", "/g/o/r", "/g/o/r/i", "/g/o/r/i/l", "/g/o/r/i/l/l", "/g/o/r/i/l/l/a"} + idx := 0 + err := router.Walk(func(route *Route, router *Router, ancestors []*Route) error { + path := paths[idx] + tpl := route.regexp.path.template + if tpl != path { + t.Errorf(`Expected %s got %s`, path, tpl) + } + idx++ + return nil + }) + if err != nil { + panic(err) + } + if idx != len(paths) { + t.Errorf("Expected %d routes, found %d", len(paths), idx) + } +} + // ---------------------------------------------------------------------------- // Helpers // ---------------------------------------------------------------------------- From 2b32409792406cc61512ca3aa35706bb9ce8c7dd Mon Sep 17 00:00:00 2001 From: Bay Dodd Date: Thu, 16 Jul 2015 10:52:01 +0100 Subject: [PATCH 05/35] fix for empty query --- mux_test.go | 9 +++++++++ regexp.go | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/mux_test.go b/mux_test.go index 8118aab..0f61da8 100644 --- a/mux_test.go +++ b/mux_test.go @@ -588,6 +588,15 @@ func TestQueries(t *testing.T) { path: "", shouldMatch: false, }, + { + title: "Queries route with empty value, should match", + route: new(Route).Queries("foo", ""), + request: newRequest("GET", "http://localhost?foo=bar"), + vars: map[string]string{}, + host: "", + path: "", + shouldMatch: true, + }, } for _, test := range tests { diff --git a/regexp.go b/regexp.go index d98575e..8ecf270 100644 --- a/regexp.go +++ b/regexp.go @@ -34,7 +34,7 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash // Now let's parse it. defaultPattern := "[^/]+" if matchQuery { - defaultPattern = "[^?&]+" + defaultPattern = "[^?&]*" } else if matchHost { defaultPattern = "[^.]+" matchPrefix = false @@ -89,6 +89,9 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash if strictSlash { pattern.WriteString("[/]?") } + if matchQuery && len(idxs) == 0 { + pattern.WriteString(defaultPattern) + } if !matchPrefix { pattern.WriteByte('$') } From 19f0a91c4e299b8253f142001ad546110f3c1e84 Mon Sep 17 00:00:00 2001 From: Bay Dodd Date: Thu, 16 Jul 2015 12:45:51 +0100 Subject: [PATCH 06/35] adding test and updating condition --- mux_test.go | 9 +++++++++ regexp.go | 7 +++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/mux_test.go b/mux_test.go index 0f61da8..9bed205 100644 --- a/mux_test.go +++ b/mux_test.go @@ -597,6 +597,15 @@ func TestQueries(t *testing.T) { path: "", shouldMatch: true, }, + { + title: "Queries route with overlapping value, should not match", + route: new(Route).Queries("foo", "bar"), + request: newRequest("GET", "http://localhost?foo=barfoo"), + vars: map[string]string{}, + host: "", + path: "", + shouldMatch: false, + }, } for _, test := range tests { diff --git a/regexp.go b/regexp.go index 8ecf270..a1f6eb6 100644 --- a/regexp.go +++ b/regexp.go @@ -89,8 +89,11 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash if strictSlash { pattern.WriteString("[/]?") } - if matchQuery && len(idxs) == 0 { - pattern.WriteString(defaultPattern) + if matchQuery { + // Add the default pattern if the query value is empty + if queryVal := strings.SplitN(template, "=", 2)[1]; queryVal == "" { + pattern.WriteString(defaultPattern) + } } if !matchPrefix { pattern.WriteByte('$') From fe40f0d05612a52a42bbbcd9d28cb81ebe4e6d36 Mon Sep 17 00:00:00 2001 From: Bay Dodd Date: Fri, 17 Jul 2015 08:40:28 +0100 Subject: [PATCH 07/35] updating query match string --- mux_test.go | 9 +++++++++ regexp.go | 10 +++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/mux_test.go b/mux_test.go index 9bed205..79d1651 100644 --- a/mux_test.go +++ b/mux_test.go @@ -606,6 +606,15 @@ func TestQueries(t *testing.T) { path: "", shouldMatch: false, }, + { + title: "Queries route with no parameter in request , should not match", + route: new(Route).Queries("foo", "{bar}"), + request: newRequest("GET", "http://localhost"), + vars: map[string]string{}, + host: "", + path: "", + shouldMatch: false, + }, } for _, test := range tests { diff --git a/regexp.go b/regexp.go index a1f6eb6..b300730 100644 --- a/regexp.go +++ b/regexp.go @@ -186,9 +186,13 @@ func (r *routeRegexp) getUrlQuery(req *http.Request) string { if !r.matchQuery { return "" } - key := strings.Split(r.template, "=")[0] - val := req.URL.Query().Get(key) - return key + "=" + val + templateKey := strings.Split(r.template, "=")[0] + for key, vals := range req.URL.Query() { + if key == templateKey && len(vals) > 0 { + return key + "=" + vals[0] + } + } + return "" } func (r *routeRegexp) matchQueryString(req *http.Request) bool { From 3339267a853e3a855d6e415b96d6f3447b60a824 Mon Sep 17 00:00:00 2001 From: Bay Dodd Date: Fri, 17 Jul 2015 10:52:37 +0100 Subject: [PATCH 08/35] adding tests --- mux_test.go | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/mux_test.go b/mux_test.go index 79d1651..1a66ad5 100644 --- a/mux_test.go +++ b/mux_test.go @@ -597,6 +597,24 @@ func TestQueries(t *testing.T) { path: "", shouldMatch: true, }, + { + title: "Queries route with empty value and no parameter in request, should not match", + route: new(Route).Queries("foo", ""), + request: newRequest("GET", "http://localhost"), + vars: map[string]string{}, + host: "", + path: "", + shouldMatch: false, + }, + { + title: "Queries route with empty value and empty parameter in request, should match", + route: new(Route).Queries("foo", ""), + request: newRequest("GET", "http://localhost?foo="), + vars: map[string]string{}, + host: "", + path: "", + shouldMatch: true, + }, { title: "Queries route with overlapping value, should not match", route: new(Route).Queries("foo", "bar"), @@ -607,7 +625,7 @@ func TestQueries(t *testing.T) { shouldMatch: false, }, { - title: "Queries route with no parameter in request , should not match", + title: "Queries route with no parameter in request, should not match", route: new(Route).Queries("foo", "{bar}"), request: newRequest("GET", "http://localhost"), vars: map[string]string{}, @@ -615,6 +633,15 @@ func TestQueries(t *testing.T) { path: "", shouldMatch: false, }, + { + title: "Queries route with empty parameter in request, should match", + route: new(Route).Queries("foo", "{bar}"), + request: newRequest("GET", "http://localhost?foo="), + vars: map[string]string{"foo": ""}, + host: "", + path: "", + shouldMatch: true, + }, } for _, test := range tests { From ba336c9cfb43552c90de6cb2ceedd3271c747558 Mon Sep 17 00:00:00 2001 From: Kamil Kisiel Date: Fri, 17 Jul 2015 08:03:03 -0700 Subject: [PATCH 09/35] getUrlQuery: Use SplitN with a max of 2. --- regexp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/regexp.go b/regexp.go index b300730..7c636d0 100644 --- a/regexp.go +++ b/regexp.go @@ -186,7 +186,7 @@ func (r *routeRegexp) getUrlQuery(req *http.Request) string { if !r.matchQuery { return "" } - templateKey := strings.Split(r.template, "=")[0] + templateKey := strings.SplitN(r.template, "=", 2)[0] for key, vals := range req.URL.Query() { if key == templateKey && len(vals) > 0 { return key + "=" + vals[0] From 98fb535d771e43021d337c156c78ab13d1a7f506 Mon Sep 17 00:00:00 2001 From: Clint Ryan Date: Sun, 19 Jul 2015 18:57:47 +1000 Subject: [PATCH 10/35] Issue 16: Return the regexp compile error --- mux.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mux.go b/mux.go index aa2bedf..2304c91 100644 --- a/mux.go +++ b/mux.go @@ -343,7 +343,10 @@ func mapFromPairsToRegex(pairs ...string) (map[string]*regexp.Regexp, error) { } m := make(map[string]*regexp.Regexp, length/2) for i := 0; i < length; i += 2 { - regex, _ := regexp.Compile(pairs[i+1]) + regex, err := regexp.Compile(pairs[i+1]) + if err != nil { + return nil, err + } m[pairs[i]] = regex } return m, nil From 39cff3481ca9e2726231c3067f58fcf3a8c9e333 Mon Sep 17 00:00:00 2001 From: Orne Brocaar Date: Wed, 5 Aug 2015 09:26:50 +0200 Subject: [PATCH 11/35] Add note about the availability of CurrentRoute. --- mux.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mux.go b/mux.go index e253230..4a94a23 100644 --- a/mux.go +++ b/mux.go @@ -312,6 +312,9 @@ func Vars(r *http.Request) map[string]string { } // CurrentRoute returns the matched route for the current request, if any. +// Note: this only works when called inside the handler of the matched route +// because it uses context.Get() which will be cleared after executing the +// handler. func CurrentRoute(r *http.Request) *Route { if rv := context.Get(r, routeKey); rv != nil { return rv.(*Route) From 13c8226081008f7ade9c619da744ed14e78eb8de Mon Sep 17 00:00:00 2001 From: Orne Brocaar Date: Wed, 5 Aug 2015 10:24:37 +0200 Subject: [PATCH 12/35] Update comment and specify KeepContext option. --- mux.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mux.go b/mux.go index 4a94a23..002051f 100644 --- a/mux.go +++ b/mux.go @@ -312,9 +312,10 @@ func Vars(r *http.Request) map[string]string { } // CurrentRoute returns the matched route for the current request, if any. -// Note: this only works when called inside the handler of the matched route -// because it uses context.Get() which will be cleared after executing the -// handler. +// This only works when called inside the handler of the matched route +// because the matched route is stored in the request context which is cleared +// after the handler returns, unless the KeepContext option is set on the +// Router. func CurrentRoute(r *http.Request) *Route { if rv := context.Get(r, routeKey); rv != nil { return rv.(*Route) From e73f183699f8ab7d54609771e1fa0ab7ffddc21b Mon Sep 17 00:00:00 2001 From: Kamil Kisiel Date: Thu, 6 Aug 2015 20:31:19 -0700 Subject: [PATCH 13/35] fix use of capturing subexpressions in pattern matches. The router now associates a regexp named group with each mux variable. It only fills variables when capturing group name match instead of relying on indices, which doesn't work if a variable regexp has interior capturing groups. Fixes #62 --- mux_test.go | 27 +++++++++++++++++++++++++++ regexp.go | 29 ++++++++++++++++++++++------- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/mux_test.go b/mux_test.go index ba47727..455d68e 100644 --- a/mux_test.go +++ b/mux_test.go @@ -108,6 +108,15 @@ func TestHost(t *testing.T) { path: "", shouldMatch: true, }, + { + title: "Host route with pattern, additional capturing group, match", + route: new(Route).Host("aaa.{v1:[a-z]{2}(b|c)}.ccc"), + request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), + vars: map[string]string{"v1": "bbb"}, + host: "aaa.bbb.ccc", + path: "", + shouldMatch: true, + }, { title: "Host route with pattern, wrong host in request URL", route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"), @@ -260,6 +269,15 @@ func TestPath(t *testing.T) { path: "/111/222/333", shouldMatch: false, }, + { + title: "Path route with multiple patterns with pipe, match", + route: new(Route).Path("/{category:a|(b/c)}/{product}/{id:[0-9]+}"), + request: newRequest("GET", "http://localhost/a/product_name/1"), + vars: map[string]string{"category": "a", "product": "product_name", "id": "1"}, + host: "", + path: "/a/product_name/1", + shouldMatch: true, + }, } for _, test := range tests { @@ -597,6 +615,15 @@ func TestQueries(t *testing.T) { path: "", shouldMatch: false, }, + { + title: "Queries route with regexp pattern with quantifier, additional capturing group", + route: new(Route).Queries("foo", "{v1:[0-9]{1}(a|b)}"), + request: newRequest("GET", "http://localhost?foo=1a"), + vars: map[string]string{"v1": "1a"}, + host: "", + path: "", + shouldMatch: true, + }, { title: "Queries route with regexp pattern with quantifier, additional variable in query string, regexp does not match", route: new(Route).Queries("foo", "{v1:[0-9]{1}}"), diff --git a/regexp.go b/regexp.go index 7c636d0..6b34fec 100644 --- a/regexp.go +++ b/regexp.go @@ -72,7 +72,7 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash tpl[idxs[i]:end]) } // Build the regexp pattern. - fmt.Fprintf(pattern, "%s(%s)", regexp.QuoteMeta(raw), patt) + fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), name, patt) // Build the reverse template. fmt.Fprintf(reverse, "%s%%s", raw) @@ -241,8 +241,13 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) if v.host != nil { hostVars := v.host.regexp.FindStringSubmatch(getHost(req)) if hostVars != nil { - for k, v := range v.host.varsN { - m.Vars[v] = hostVars[k+1] + subexpNames := v.host.regexp.SubexpNames() + varName := 0 + for i, name := range subexpNames[1:] { + if name != "" && v.host.varsN[varName] == name { + m.Vars[name] = hostVars[i+1] + varName++ + } } } } @@ -250,8 +255,13 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) if v.path != nil { pathVars := v.path.regexp.FindStringSubmatch(req.URL.Path) if pathVars != nil { - for k, v := range v.path.varsN { - m.Vars[v] = pathVars[k+1] + subexpNames := v.path.regexp.SubexpNames() + varName := 0 + for i, name := range subexpNames[1:] { + if name != "" && v.path.varsN[varName] == name { + m.Vars[name] = pathVars[i+1] + varName++ + } } // Check if we should redirect. if v.path.strictSlash { @@ -273,8 +283,13 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) for _, q := range v.queries { queryVars := q.regexp.FindStringSubmatch(q.getUrlQuery(req)) if queryVars != nil { - for k, v := range q.varsN { - m.Vars[v] = queryVars[k+1] + subexpNames := q.regexp.SubexpNames() + varName := 0 + for i, name := range subexpNames[1:] { + if name != "" && q.varsN[varName] == name { + m.Vars[name] = queryVars[i+1] + varName++ + } } } } From 780d0505d751ba5c99ecf71ea287253f089a496b Mon Sep 17 00:00:00 2001 From: Kamil Kisiel Date: Thu, 6 Aug 2015 20:45:53 -0700 Subject: [PATCH 14/35] Update README --- README.md | 201 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 199 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e60301b..e7566ca 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,204 @@ mux === +[![GoDoc](https://godoc.org/github.com/gorilla/securecookie?status.svg)](https://godoc.org/github.com/gorilla/securecookie) [![Build Status](https://travis-ci.org/gorilla/mux.png?branch=master)](https://travis-ci.org/gorilla/mux) -gorilla/mux is a powerful URL router and dispatcher. +Package gorilla/mux implements a request router and dispatcher. -Read the full documentation here: http://www.gorillatoolkit.org/pkg/mux +The name mux stands for "HTTP request multiplexer". Like the standard +http.ServeMux, mux.Router matches incoming requests against a list of +registered routes and calls a handler for the route that matches the URL +or other conditions. The main features are: + + * Requests can be matched based on URL host, path, path prefix, schemes, + header and query values, HTTP methods or using custom matchers. + * URL hosts and paths can have variables with an optional regular + expression. + * Registered URLs can be built, or "reversed", which helps maintaining + references to resources. + * Routes can be used as subrouters: nested routes are only tested if the + parent route matches. This is useful to define groups of routes that + share common conditions like a host, a path prefix or other repeated + attributes. As a bonus, this optimizes request matching. + * It implements the http.Handler interface so it is compatible with the + standard http.ServeMux. + +Let's start registering a couple of URL paths and handlers: + + func main() { + r := mux.NewRouter() + r.HandleFunc("/", HomeHandler) + r.HandleFunc("/products", ProductsHandler) + r.HandleFunc("/articles", ArticlesHandler) + http.Handle("/", r) + } + +Here we register three routes mapping URL paths to handlers. This is +equivalent to how http.HandleFunc() works: if an incoming request URL matches +one of the paths, the corresponding handler is called passing +(http.ResponseWriter, *http.Request) as parameters. + +Paths can have variables. They are defined using the format {name} or +{name:pattern}. If a regular expression pattern is not defined, the matched +variable will be anything until the next slash. For example: + + r := mux.NewRouter() + r.HandleFunc("/products/{key}", ProductHandler) + r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler) + r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) + +The names are used to create a map of route variables which can be retrieved +calling mux.Vars(): + + vars := mux.Vars(request) + category := vars["category"] + +And this is all you need to know about the basic usage. More advanced options +are explained below. + +Routes can also be restricted to a domain or subdomain. Just define a host +pattern to be matched. They can also have variables: + + r := mux.NewRouter() + // Only matches if domain is "www.domain.com". + r.Host("www.domain.com") + // Matches a dynamic subdomain. + r.Host("{subdomain:[a-z]+}.domain.com") + +There are several other matchers that can be added. To match path prefixes: + + r.PathPrefix("/products/") + +...or HTTP methods: + + r.Methods("GET", "POST") + +...or URL schemes: + + r.Schemes("https") + +...or header values: + + r.Headers("X-Requested-With", "XMLHttpRequest") + +...or query values: + + r.Queries("key", "value") + +...or to use a custom matcher function: + + r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool { + return r.ProtoMajor == 0 + }) + +...and finally, it is possible to combine several matchers in a single route: + + r.HandleFunc("/products", ProductsHandler). + Host("www.domain.com"). + Methods("GET"). + Schemes("http") + +Setting the same matching conditions again and again can be boring, so we have +a way to group several routes that share the same requirements. +We call it "subrouting". + +For example, let's say we have several URLs that should only match when the +host is "www.domain.com". Create a route for that host and get a "subrouter" +from it: + + r := mux.NewRouter() + s := r.Host("www.domain.com").Subrouter() + +Then register routes in the subrouter: + + s.HandleFunc("/products/", ProductsHandler) + s.HandleFunc("/products/{key}", ProductHandler) + s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) + +The three URL paths we registered above will only be tested if the domain is +"www.domain.com", because the subrouter is tested first. This is not +only convenient, but also optimizes request matching. You can create +subrouters combining any attribute matchers accepted by a route. + +Subrouters can be used to create domain or path "namespaces": you define +subrouters in a central place and then parts of the app can register its +paths relatively to a given subrouter. + +There's one more thing about subroutes. When a subrouter has a path prefix, +the inner routes use it as base for their paths: + + r := mux.NewRouter() + s := r.PathPrefix("/products").Subrouter() + // "/products/" + s.HandleFunc("/", ProductsHandler) + // "/products/{key}/" + s.HandleFunc("/{key}/", ProductHandler) + // "/products/{key}/details" + s.HandleFunc("/{key}/details", ProductDetailsHandler) + +Now let's see how to build registered URLs. + +Routes can be named. All routes that define a name can have their URLs built, +or "reversed". We define a name calling Name() on a route. For example: + + r := mux.NewRouter() + r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). + Name("article") + +To build a URL, get the route and call the URL() method, passing a sequence of +key/value pairs for the route variables. For the previous route, we would do: + + url, err := r.Get("article").URL("category", "technology", "id", "42") + +...and the result will be a url.URL with the following path: + + "/articles/technology/42" + +This also works for host variables: + + r := mux.NewRouter() + r.Host("{subdomain}.domain.com"). + Path("/articles/{category}/{id:[0-9]+}"). + HandlerFunc(ArticleHandler). + Name("article") + + // url.String() will be "http://news.domain.com/articles/technology/42" + url, err := r.Get("article").URL("subdomain", "news", + "category", "technology", + "id", "42") + +All variables defined in the route are required, and their values must +conform to the corresponding patterns. These requirements guarantee that a +generated URL will always match a registered route -- the only exception is +for explicitly defined "build-only" routes which never match. + +Regex support also exists for matching Headers within a route. For example, we could do: + + r.HeadersRegexp("Content-Type", "application/(text|json)") + +...and the route will match both requests with a Content-Type of `application/json` as well as +`application/text` + +There's also a way to build only the URL host or path for a route: +use the methods URLHost() or URLPath() instead. For the previous route, +we would do: + + // "http://news.domain.com/" + host, err := r.Get("article").URLHost("subdomain", "news") + + // "/articles/technology/42" + path, err := r.Get("article").URLPath("category", "technology", "id", "42") + +And if you use subrouters, host and path defined separately can be built +as well: + + r := mux.NewRouter() + s := r.Host("{subdomain}.domain.com").Subrouter() + s.Path("/articles/{category}/{id:[0-9]+}"). + HandlerFunc(ArticleHandler). + Name("article") + + // "http://news.domain.com/articles/technology/42" + url, err := r.Get("article").URL("subdomain", "news", + "category", "technology", + "id", "42") From c3c5f0000f7b474738b08f34308c1e8a4060ce14 Mon Sep 17 00:00:00 2001 From: Kamil Kisiel Date: Thu, 6 Aug 2015 22:12:38 -0700 Subject: [PATCH 15/35] Add test which used to fail for queries. Fixes #66 --- mux_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mux_test.go b/mux_test.go index ba47727..caeaa46 100644 --- a/mux_test.go +++ b/mux_test.go @@ -660,6 +660,15 @@ func TestQueries(t *testing.T) { path: "", shouldMatch: true, }, + { + title: "Queries route, bad submatch", + route: new(Route).Queries("foo", "bar", "baz", "ding"), + request: newRequest("GET", "http://localhost?fffoo=bar&baz=dingggg"), + vars: map[string]string{}, + host: "", + path: "", + shouldMatch: false, + }, } for _, test := range tests { From ca524fd37fc91e043c82ba10aed96f77d523c514 Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Sat, 8 Aug 2015 12:41:49 +0800 Subject: [PATCH 16/35] Updated README w/ runnable example. Addresses #32. --- README.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/README.md b/README.md index e7566ca..bf49ebd 100644 --- a/README.md +++ b/README.md @@ -202,3 +202,34 @@ as well: url, err := r.Get("article").URL("subdomain", "news", "category", "technology", "id", "42") + +## Full Example + +Here's a complete, runnable example of a small mux based server: + +```go +package main + +import ( + "net/http" + + "github.com/gorilla/mux" +) + +func YourHandler(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Gorilla!\n")) +} + +func main() { + r := mux.NewRouter() + // Routes consist of a path and a handler function. + r.HandleFunc("/", YourHandler) + + // Bind to a port and pass our router in + http.ListenAndServe(":8000", r) +} +``` + +## License + +BSD licensed. See the LICENSE file for details. From 577b9e4a658e25897ddd3320255d96a9285a26c0 Mon Sep 17 00:00:00 2001 From: Shinya Kawaguchi Date: Tue, 11 Aug 2015 04:05:30 +0900 Subject: [PATCH 17/35] Add tests for hyphenated variable names --- mux_test.go | 90 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/mux_test.go b/mux_test.go index d49a0f2..7a0bc9e 100644 --- a/mux_test.go +++ b/mux_test.go @@ -144,6 +144,33 @@ func TestHost(t *testing.T) { path: "", shouldMatch: false, }, + { + title: "Host route with hyphenated name and pattern, match", + route: new(Route).Host("aaa.{v-1:[a-z]{3}}.ccc"), + request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), + vars: map[string]string{"v-1": "bbb"}, + host: "aaa.bbb.ccc", + path: "", + shouldMatch: true, + }, + { + title: "Host route with hyphenated name and pattern, additional capturing group, match", + route: new(Route).Host("aaa.{v-1:[a-z]{2}(b|c)}.ccc"), + request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), + vars: map[string]string{"v-1": "bbb"}, + host: "aaa.bbb.ccc", + path: "", + shouldMatch: true, + }, + { + title: "Host route with multiple hyphenated names and patterns, match", + route: new(Route).Host("{v-1:[a-z]{3}}.{v-2:[a-z]{3}}.{v-3:[a-z]{3}}"), + request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), + vars: map[string]string{"v-1": "aaa", "v-2": "bbb", "v-3": "ccc"}, + host: "aaa.bbb.ccc", + path: "", + shouldMatch: true, + }, { title: "Path route with single pattern with pipe, match", route: new(Route).Path("/{category:a|b/c}"), @@ -278,6 +305,33 @@ func TestPath(t *testing.T) { path: "/a/product_name/1", shouldMatch: true, }, + { + title: "Path route with hyphenated name and pattern, match", + route: new(Route).Path("/111/{v-1:[0-9]{3}}/333"), + request: newRequest("GET", "http://localhost/111/222/333"), + vars: map[string]string{"v-1": "222"}, + host: "", + path: "/111/222/333", + shouldMatch: true, + }, + { + title: "Path route with multiple hyphenated names and patterns, match", + route: new(Route).Path("/{v-1:[0-9]{3}}/{v-2:[0-9]{3}}/{v-3:[0-9]{3}}"), + request: newRequest("GET", "http://localhost/111/222/333"), + vars: map[string]string{"v-1": "111", "v-2": "222", "v-3": "333"}, + host: "", + path: "/111/222/333", + shouldMatch: true, + }, + { + title: "Path route with multiple hyphenated names and patterns with pipe, match", + route: new(Route).Path("/{product-category:a|(b/c)}/{product-name}/{product-id:[0-9]+}"), + request: newRequest("GET", "http://localhost/a/product_name/1"), + vars: map[string]string{"product-category": "a", "product-name": "product_name", "product-id": "1"}, + host: "", + path: "/a/product_name/1", + shouldMatch: true, + }, } for _, test := range tests { @@ -633,6 +687,42 @@ func TestQueries(t *testing.T) { path: "", shouldMatch: false, }, + { + title: "Queries route with hyphenated name, match", + route: new(Route).Queries("foo", "{v-1}"), + request: newRequest("GET", "http://localhost?foo=bar"), + vars: map[string]string{"v-1": "bar"}, + host: "", + path: "", + shouldMatch: true, + }, + { + title: "Queries route with multiple hyphenated names, match", + route: new(Route).Queries("foo", "{v-1}", "baz", "{v-2}"), + request: newRequest("GET", "http://localhost?foo=bar&baz=ding"), + vars: map[string]string{"v-1": "bar", "v-2": "ding"}, + host: "", + path: "", + shouldMatch: true, + }, + { + title: "Queries route with hyphenate name and pattern, match", + route: new(Route).Queries("foo", "{v-1:[0-9]+}"), + request: newRequest("GET", "http://localhost?foo=10"), + vars: map[string]string{"v-1": "10"}, + host: "", + path: "", + shouldMatch: true, + }, + { + title: "Queries route with hyphenated name and pattern with quantifier, additional capturing group", + route: new(Route).Queries("foo", "{v-1:[0-9]{1}(a|b)}"), + request: newRequest("GET", "http://localhost?foo=1a"), + vars: map[string]string{"v-1": "1a"}, + host: "", + path: "", + shouldMatch: true, + }, { title: "Queries route with empty value, should match", route: new(Route).Queries("foo", ""), From 273db68971215ed764f24e23f49469c54e9bcd4b Mon Sep 17 00:00:00 2001 From: Shinya Kawaguchi Date: Tue, 11 Aug 2015 04:09:52 +0900 Subject: [PATCH 18/35] Fix regexp syntax error caused by variable names containing any characters except letters, digits, and underscores --- regexp.go | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/regexp.go b/regexp.go index 6b34fec..d3f25de 100644 --- a/regexp.go +++ b/regexp.go @@ -72,13 +72,14 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash tpl[idxs[i]:end]) } // Build the regexp pattern. - fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), name, patt) + varIdx := i / 2 + fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), varGroupName(varIdx), patt) // Build the reverse template. fmt.Fprintf(reverse, "%s%%s", raw) // Append variable name and compiled pattern. - varsN[i/2] = name - varsR[i/2], err = regexp.Compile(fmt.Sprintf("^%s$", patt)) + varsN[varIdx] = name + varsR[varIdx], err = regexp.Compile(fmt.Sprintf("^%s$", patt)) if err != nil { return nil, err } @@ -224,6 +225,11 @@ func braceIndices(s string) ([]int, error) { return idxs, nil } +// varGroupName builds a capturing group name for the indexed variable. +func varGroupName(idx int) string { + return fmt.Sprintf("v%d", idx) +} + // ---------------------------------------------------------------------------- // routeRegexpGroup // ---------------------------------------------------------------------------- @@ -244,8 +250,8 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) subexpNames := v.host.regexp.SubexpNames() varName := 0 for i, name := range subexpNames[1:] { - if name != "" && v.host.varsN[varName] == name { - m.Vars[name] = hostVars[i+1] + if name == varGroupName(varName) { + m.Vars[v.host.varsN[varName]] = hostVars[i+1] varName++ } } @@ -258,8 +264,8 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) subexpNames := v.path.regexp.SubexpNames() varName := 0 for i, name := range subexpNames[1:] { - if name != "" && v.path.varsN[varName] == name { - m.Vars[name] = pathVars[i+1] + if name == varGroupName(varName) { + m.Vars[v.path.varsN[varName]] = pathVars[i+1] varName++ } } @@ -286,8 +292,8 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) subexpNames := q.regexp.SubexpNames() varName := 0 for i, name := range subexpNames[1:] { - if name != "" && q.varsN[varName] == name { - m.Vars[name] = queryVars[i+1] + if name == varGroupName(varName) { + m.Vars[q.varsN[varName]] = queryVars[i+1] varName++ } } From d17b93cab89c2fe13ef8292fdefb116d86666e75 Mon Sep 17 00:00:00 2001 From: Shinya Kawaguchi Date: Tue, 11 Aug 2015 14:26:09 +0900 Subject: [PATCH 19/35] Refactoring for better performance --- regexp.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/regexp.go b/regexp.go index d3f25de..06728dd 100644 --- a/regexp.go +++ b/regexp.go @@ -10,6 +10,7 @@ import ( "net/http" "net/url" "regexp" + "strconv" "strings" ) @@ -227,7 +228,7 @@ func braceIndices(s string) ([]int, error) { // varGroupName builds a capturing group name for the indexed variable. func varGroupName(idx int) string { - return fmt.Sprintf("v%d", idx) + return "v" + strconv.Itoa(idx) } // ---------------------------------------------------------------------------- @@ -250,7 +251,7 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) subexpNames := v.host.regexp.SubexpNames() varName := 0 for i, name := range subexpNames[1:] { - if name == varGroupName(varName) { + if name != "" && name == varGroupName(varName) { m.Vars[v.host.varsN[varName]] = hostVars[i+1] varName++ } @@ -264,7 +265,7 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) subexpNames := v.path.regexp.SubexpNames() varName := 0 for i, name := range subexpNames[1:] { - if name == varGroupName(varName) { + if name != "" && name == varGroupName(varName) { m.Vars[v.path.varsN[varName]] = pathVars[i+1] varName++ } @@ -292,7 +293,7 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) subexpNames := q.regexp.SubexpNames() varName := 0 for i, name := range subexpNames[1:] { - if name == varGroupName(varName) { + if name != "" && name == varGroupName(varName) { m.Vars[q.varsN[varName]] = queryVars[i+1] varName++ } From 5112c33f3a6ef694c1e5784b68981f08b3f0327c Mon Sep 17 00:00:00 2001 From: Kamil Kisiel Date: Tue, 11 Aug 2015 22:16:22 -0700 Subject: [PATCH 20/35] slightly improve printing of regexps in failed tests. --- mux_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/mux_test.go b/mux_test.go index 7a0bc9e..5732d2d 100644 --- a/mux_test.go +++ b/mux_test.go @@ -7,11 +7,24 @@ package mux import ( "fmt" "net/http" + "strings" "testing" "github.com/gorilla/context" ) +func (r *Route) GoString() string { + matchers := make([]string, len(r.matchers)) + for i, m := range r.matchers { + matchers[i] = fmt.Sprintf("%#v", m) + } + return fmt.Sprintf("&Route{matchers:[]matcher{%s}}", strings.Join(matchers, ", ")) +} + +func (r *routeRegexp) GoString() string { + return fmt.Sprintf("&routeRegexp{template: %q, matchHost: %t, matchQuery: %t, strictSlash: %t, regexp: regexp.MustCompile(%q), reverse: %q, varsN: %v, varsR: %v", r.template, r.matchHost, r.matchQuery, r.strictSlash, r.regexp.String(), r.reverse, r.varsN, r.varsR) +} + type routeTest struct { title string // title of the test route *Route // the route being tested From b0b2bc47bcd1442dfc76e58ab649dea056461ac9 Mon Sep 17 00:00:00 2001 From: Kamil Kisiel Date: Thu, 13 Aug 2015 12:01:04 -0700 Subject: [PATCH 21/35] Quote domain names in README.md. Use example.com instead of domain.com Fixes #119 --- README.md | 12 ++++++------ doc.go | 12 ++++++------ old_test.go | 6 +++--- route.go | 4 ++-- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index bf49ebd..9a046ff 100644 --- a/README.md +++ b/README.md @@ -60,8 +60,8 @@ Routes can also be restricted to a domain or subdomain. Just define a host pattern to be matched. They can also have variables: r := mux.NewRouter() - // Only matches if domain is "www.domain.com". - r.Host("www.domain.com") + // Only matches if domain is "www.example.com". + r.Host("www.example.com") // Matches a dynamic subdomain. r.Host("{subdomain:[a-z]+}.domain.com") @@ -94,7 +94,7 @@ There are several other matchers that can be added. To match path prefixes: ...and finally, it is possible to combine several matchers in a single route: r.HandleFunc("/products", ProductsHandler). - Host("www.domain.com"). + Host("www.example.com"). Methods("GET"). Schemes("http") @@ -103,11 +103,11 @@ a way to group several routes that share the same requirements. We call it "subrouting". For example, let's say we have several URLs that should only match when the -host is "www.domain.com". Create a route for that host and get a "subrouter" +host is `www.example.com`. Create a route for that host and get a "subrouter" from it: r := mux.NewRouter() - s := r.Host("www.domain.com").Subrouter() + s := r.Host("www.example.com").Subrouter() Then register routes in the subrouter: @@ -116,7 +116,7 @@ Then register routes in the subrouter: s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) The three URL paths we registered above will only be tested if the domain is -"www.domain.com", because the subrouter is tested first. This is not +`www.example.com`, because the subrouter is tested first. This is not only convenient, but also optimizes request matching. You can create subrouters combining any attribute matchers accepted by a route. diff --git a/doc.go b/doc.go index 442baba..49798cb 100644 --- a/doc.go +++ b/doc.go @@ -60,8 +60,8 @@ Routes can also be restricted to a domain or subdomain. Just define a host pattern to be matched. They can also have variables: r := mux.NewRouter() - // Only matches if domain is "www.domain.com". - r.Host("www.domain.com") + // Only matches if domain is "www.example.com". + r.Host("www.example.com") // Matches a dynamic subdomain. r.Host("{subdomain:[a-z]+}.domain.com") @@ -94,7 +94,7 @@ There are several other matchers that can be added. To match path prefixes: ...and finally, it is possible to combine several matchers in a single route: r.HandleFunc("/products", ProductsHandler). - Host("www.domain.com"). + Host("www.example.com"). Methods("GET"). Schemes("http") @@ -103,11 +103,11 @@ a way to group several routes that share the same requirements. We call it "subrouting". For example, let's say we have several URLs that should only match when the -host is "www.domain.com". Create a route for that host and get a "subrouter" +host is "www.example.com". Create a route for that host and get a "subrouter" from it: r := mux.NewRouter() - s := r.Host("www.domain.com").Subrouter() + s := r.Host("www.example.com").Subrouter() Then register routes in the subrouter: @@ -116,7 +116,7 @@ Then register routes in the subrouter: s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) The three URL paths we registered above will only be tested if the domain is -"www.domain.com", because the subrouter is tested first. This is not +"www.example.com", because the subrouter is tested first. This is not only convenient, but also optimizes request matching. You can create subrouters combining any attribute matchers accepted by a route. diff --git a/old_test.go b/old_test.go index 1f7c190..755db48 100644 --- a/old_test.go +++ b/old_test.go @@ -545,7 +545,7 @@ func TestMatchedRouteName(t *testing.T) { router := NewRouter() route := router.NewRoute().Path("/products/").Name(routeName) - url := "http://www.domain.com/products/" + url := "http://www.example.com/products/" request, _ := http.NewRequest("GET", url, nil) var rv RouteMatch ok := router.Match(request, &rv) @@ -563,10 +563,10 @@ func TestMatchedRouteName(t *testing.T) { func TestSubRouting(t *testing.T) { // Example from docs. router := NewRouter() - subrouter := router.NewRoute().Host("www.domain.com").Subrouter() + subrouter := router.NewRoute().Host("www.example.com").Subrouter() route := subrouter.NewRoute().Path("/products/").Name("products") - url := "http://www.domain.com/products/" + url := "http://www.example.com/products/" request, _ := http.NewRequest("GET", url, nil) var rv RouteMatch ok := router.Match(request, &rv) diff --git a/route.go b/route.go index 75481b5..8901304 100644 --- a/route.go +++ b/route.go @@ -255,7 +255,7 @@ func (r *Route) HeadersRegexp(pairs ...string) *Route { // For example: // // r := mux.NewRouter() -// r.Host("www.domain.com") +// r.Host("www.example.com") // r.Host("{subdomain}.domain.com") // r.Host("{subdomain:[a-z]+}.domain.com") // @@ -414,7 +414,7 @@ func (r *Route) BuildVarsFunc(f BuildVarsFunc) *Route { // It will test the inner routes only if the parent route matched. For example: // // r := mux.NewRouter() -// s := r.Host("www.domain.com").Subrouter() +// s := r.Host("www.example.com").Subrouter() // s.HandleFunc("/products/", ProductsHandler) // s.HandleFunc("/products/{key}", ProductHandler) // s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) From e45852e5374d4b40feb756a967c21a758fca25b1 Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Fri, 14 Aug 2015 22:25:16 +0800 Subject: [PATCH 22/35] Fixed GoDoc badge. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9a046ff..55dd4e5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ mux === -[![GoDoc](https://godoc.org/github.com/gorilla/securecookie?status.svg)](https://godoc.org/github.com/gorilla/securecookie) +[![GoDoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux) [![Build Status](https://travis-ci.org/gorilla/mux.png?branch=master)](https://travis-ci.org/gorilla/mux) Package gorilla/mux implements a request router and dispatcher. From f8220e087da2fc83d083bfc0289ed7f9db3536bd Mon Sep 17 00:00:00 2001 From: Kamil Kisiel Date: Sun, 16 Aug 2015 00:31:13 -0700 Subject: [PATCH 23/35] travis-ci: more recent go versions --- .travis.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index d87d465..245a2f5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: go go: - - 1.0 - - 1.1 - - 1.2 + - 1.3 + - 1.4 - tip From ee1815431e497d3850809578c93ab6705f1a19f7 Mon Sep 17 00:00:00 2001 From: Kamil Kisiel Date: Wed, 19 Aug 2015 22:15:06 -0700 Subject: [PATCH 24/35] Update .travis.yml --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 245a2f5..f983b60 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,8 @@ language: go +sudo: false go: - 1.3 - 1.4 + - 1.5 - tip From 8ae7a23e03967d170fbd699eaf8e55883b99e94b Mon Sep 17 00:00:00 2001 From: Clint Ryan Date: Tue, 8 Sep 2015 21:31:30 +1000 Subject: [PATCH 25/35] Fixed documentation from Issue 16 --- route.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/route.go b/route.go index e81723e..24fd975 100644 --- a/route.go +++ b/route.go @@ -200,15 +200,7 @@ func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool { // "X-Requested-With", "XMLHttpRequest") // // The above route will only match if both request header values match. -// Alternatively, you can provide a regular expression and match the header as follows: -// -// r.Headers("Content-Type", "application/(text|json)", -// "X-Requested-With", "XMLHttpRequest") -// -// The above route will the same as the previous example, with the addition of matching -// application/text as well. -// -// It the value is an empty string, it will match any value if the key is set. +// If the value is an empty string, it will match any value if the key is set. func (r *Route) Headers(pairs ...string) *Route { if r.err == nil { var headers map[string]string From ac3897eae3767628df9a43b55c53fca226870a27 Mon Sep 17 00:00:00 2001 From: Matt Casper Date: Sat, 3 Oct 2015 00:21:00 -0700 Subject: [PATCH 26/35] Fix a typo in the comments, add a few comment docs --- mux.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mux.go b/mux.go index 002051f..68c4ea5 100644 --- a/mux.go +++ b/mux.go @@ -70,7 +70,7 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { // Clean path to canonical form and redirect. if p := cleanPath(req.URL.Path); p != req.URL.Path { - // Added 3 lines (Philip Schlump) - It was droping the query string and #whatever from query. + // Added 3 lines (Philip Schlump) - It was dropping the query string and #whatever from query. // This matches with fix in go 1.2 r.c. 4 for same problem. Go Issue: // http://code.google.com/p/go/issues/detail?id=5252 url := *req.URL @@ -365,6 +365,8 @@ func uniqueVars(s1, s2 []string) error { return nil } +// checkPairs returns the count of strings passed in, and an error if +// the count is not an even number. func checkPairs(pairs ...string) (int, error) { length := len(pairs) if length%2 != 0 { @@ -374,7 +376,8 @@ func checkPairs(pairs ...string) (int, error) { return length, nil } -// mapFromPairs converts variadic string parameters to a string map. +// mapFromPairsToString converts variadic string parameters to a +// string to string map. func mapFromPairsToString(pairs ...string) (map[string]string, error) { length, err := checkPairs(pairs...) if err != nil { @@ -387,6 +390,8 @@ func mapFromPairsToString(pairs ...string) (map[string]string, error) { return m, nil } +// mapFromPairsToRegex converts variadic string paramers to a +// string to regex map. func mapFromPairsToRegex(pairs ...string) (map[string]*regexp.Regexp, error) { length, err := checkPairs(pairs...) if err != nil { From a90bbbc6fa11b6a5c4ad95c2ab27eb51229890a5 Mon Sep 17 00:00:00 2001 From: mitsuteru sawa Date: Sat, 7 Nov 2015 21:34:30 +0900 Subject: [PATCH 27/35] Correct a printf verb type % go vet mux_test.go:1080: arg len(ancestors) for printf verb %s of wrong type: int --- mux_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mux_test.go b/mux_test.go index 5732d2d..d1eae92 100644 --- a/mux_test.go +++ b/mux_test.go @@ -1077,7 +1077,7 @@ func TestWalkSingleDepth(t *testing.T) { return SkipRouter } if len(ancestors) != depths[i] { - t.Errorf(`Expected depth of %d at i = %d; got "%s"`, depths[i], i, len(ancestors)) + t.Errorf(`Expected depth of %d at i = %d; got "%d"`, depths[i], i, len(ancestors)) } if matcher.template != "/"+paths[i] { t.Errorf(`Expected "/%s" at i = %d; got "%s"`, paths[i], i, matcher.template) From 9a9f155278d9b29c53acbb38c89b3024f658b55d Mon Sep 17 00:00:00 2001 From: Dmitri Shuralyov Date: Sun, 8 Nov 2015 17:29:33 -0800 Subject: [PATCH 28/35] Travis: Perform gofmt, go vet checks; use race detector during tests. This change augments the Travis CI build to perform: - Check that all files follow gofmt style, including -s (simplify) option. - Check that go vet does not report any problems. - Use race detector when running tests, to ensure there are no data races found. --- .travis.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f983b60..83ab8f5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,14 @@ language: go sudo: false - go: - 1.3 - 1.4 - 1.5 - tip +install: + - go get golang.org/x/tools/cmd/vet +script: + - go get -t -v ./... + - diff -u <(echo -n) <(gofmt -d -s .) + - go tool vet . + - go test -v -race ./... From c329c7d193285eb0aeac7892896766be20a84c4c Mon Sep 17 00:00:00 2001 From: bign8 Date: Fri, 25 Dec 2015 13:16:04 -0700 Subject: [PATCH 29/35] Potential fix for #20 --- mux.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/mux.go b/mux.go index 68c4ea5..aabe995 100644 --- a/mux.go +++ b/mux.go @@ -59,6 +59,12 @@ func (r *Router) Match(req *http.Request, match *RouteMatch) bool { return true } } + + // Closest match for a router (includes sub-routers) + if r.NotFoundHandler != nil { + match.Handler = r.NotFoundHandler + return true + } return false } @@ -89,10 +95,7 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { setCurrentRoute(req, match.Route) } if handler == nil { - handler = r.NotFoundHandler - if handler == nil { - handler = http.NotFoundHandler() - } + handler = http.NotFoundHandler() } if !r.KeepContext { defer context.Clear(req) @@ -324,11 +327,15 @@ func CurrentRoute(r *http.Request) *Route { } func setVars(r *http.Request, val interface{}) { - context.Set(r, varsKey, val) + if val != nil { + context.Set(r, varsKey, val) + } } func setCurrentRoute(r *http.Request, val interface{}) { - context.Set(r, routeKey, val) + if val != nil { + context.Set(r, routeKey, val) + } } // ---------------------------------------------------------------------------- From 82a9c170d40582ee65ff8af081485e5e325fb4a0 Mon Sep 17 00:00:00 2001 From: Nate Woods Date: Sat, 26 Dec 2015 00:09:21 -0700 Subject: [PATCH 30/35] Covering change with unit test This test focuses on the feature of allowing sub-routers error handlers to precede the parents, rather than the code change required to provide this functionality. --- mux_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/mux_test.go b/mux_test.go index d1eae92..1ea439a 100644 --- a/mux_test.go +++ b/mux_test.go @@ -1123,6 +1123,30 @@ func TestWalkNested(t *testing.T) { } } +func TestSubrouterErrorHandling(t *testing.T) { + superRouterCalled := false + subRouterCalled := false + + router := NewRouter() + router.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + superRouterCalled = true + }) + subRouter := router.PathPrefix("/bign8").Subrouter() + subRouter.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + subRouterCalled = true + }) + + req, _ := http.NewRequest("GET", "http://localhost/bign8/was/here", nil) + router.ServeHTTP(NewRecorder(), req) + + if superRouterCalled { + t.Error("Super router 404 handler called when sub-router 404 handler is available.") + } + if !subRouterCalled { + t.Error("Sub-router 404 handler was not called.") + } +} + // ---------------------------------------------------------------------------- // Helpers // ---------------------------------------------------------------------------- From 66181ed3371dcbff358575eff5e41efc013608d8 Mon Sep 17 00:00:00 2001 From: Timothy Cyrus Date: Thu, 31 Dec 2015 11:12:17 -0500 Subject: [PATCH 31/35] Update README.md --- README.md | 283 +++++++++++++++++++++++++++--------------------------- 1 file changed, 144 insertions(+), 139 deletions(-) diff --git a/README.md b/README.md index 55dd4e5..b987c9e 100644 --- a/README.md +++ b/README.md @@ -1,211 +1,216 @@ mux === [![GoDoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux) -[![Build Status](https://travis-ci.org/gorilla/mux.png?branch=master)](https://travis-ci.org/gorilla/mux) - -Package gorilla/mux implements a request router and dispatcher. - -The name mux stands for "HTTP request multiplexer". Like the standard -http.ServeMux, mux.Router matches incoming requests against a list of -registered routes and calls a handler for the route that matches the URL -or other conditions. The main features are: - - * Requests can be matched based on URL host, path, path prefix, schemes, - header and query values, HTTP methods or using custom matchers. - * URL hosts and paths can have variables with an optional regular - expression. - * Registered URLs can be built, or "reversed", which helps maintaining - references to resources. - * Routes can be used as subrouters: nested routes are only tested if the - parent route matches. This is useful to define groups of routes that - share common conditions like a host, a path prefix or other repeated - attributes. As a bonus, this optimizes request matching. - * It implements the http.Handler interface so it is compatible with the - standard http.ServeMux. +[![Build Status](https://travis-ci.org/gorilla/mux.svg?branch=master)](https://travis-ci.org/gorilla/mux) -Let's start registering a couple of URL paths and handlers: +Package `gorilla/mux` implements a request router and dispatcher. - func main() { - r := mux.NewRouter() - r.HandleFunc("/", HomeHandler) - r.HandleFunc("/products", ProductsHandler) - r.HandleFunc("/articles", ArticlesHandler) - http.Handle("/", r) - } +The name mux stands for "HTTP request multiplexer". Like the standard `http.ServeMux`, `mux.Router` matches incoming requests against a list of registered routes and calls a handler for the route that matches the URL or other conditions. The main features are: -Here we register three routes mapping URL paths to handlers. This is -equivalent to how http.HandleFunc() works: if an incoming request URL matches -one of the paths, the corresponding handler is called passing -(http.ResponseWriter, *http.Request) as parameters. +* Requests can be matched based on URL host, path, path prefix, schemes, header and query values, HTTP methods or using custom matchers. +* URL hosts and paths can have variables with an optional regular expression. +* Registered URLs can be built, or "reversed", which helps maintaining references to resources. +* Routes can be used as subrouters: nested routes are only tested if the parent route matches. This is useful to define groups of routes that share common conditions like a host, a path prefix or other repeated attributes. As a bonus, this optimizes request matching. +* It implements the `http.Handler` interface so it is compatible with the standard `http.ServeMux`. -Paths can have variables. They are defined using the format {name} or -{name:pattern}. If a regular expression pattern is not defined, the matched -variable will be anything until the next slash. For example: +Let's start registering a couple of URL paths and handlers: +```go +func main() { r := mux.NewRouter() - r.HandleFunc("/products/{key}", ProductHandler) - r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler) - r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) + r.HandleFunc("/", HomeHandler) + r.HandleFunc("/products", ProductsHandler) + r.HandleFunc("/articles", ArticlesHandler) + http.Handle("/", r) +} +``` -The names are used to create a map of route variables which can be retrieved -calling mux.Vars(): +Here we register three routes mapping URL paths to handlers. This is equivalent to how `http.HandleFunc()` works: if an incoming request URL matches one of the paths, the corresponding handler is called passing (`http.ResponseWriter`, `*http.Request`) as parameters. - vars := mux.Vars(request) - category := vars["category"] +Paths can have variables. They are defined using the format `{name}` or `{name:pattern}`. If a regular expression pattern is not defined, the matched variable will be anything until the next slash. For example: -And this is all you need to know about the basic usage. More advanced options -are explained below. +```go +r := mux.NewRouter() +r.HandleFunc("/products/{key}", ProductHandler) +r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler) +r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) +``` -Routes can also be restricted to a domain or subdomain. Just define a host -pattern to be matched. They can also have variables: +The names are used to create a map of route variables which can be retrieved calling `mux.Vars()`: - r := mux.NewRouter() - // Only matches if domain is "www.example.com". - r.Host("www.example.com") - // Matches a dynamic subdomain. - r.Host("{subdomain:[a-z]+}.domain.com") +```go +vars := mux.Vars(request) +category := vars["category"] +``` + +And this is all you need to know about the basic usage. More advanced options are explained below. + +Routes can also be restricted to a domain or subdomain. Just define a host pattern to be matched. They can also have variables: + +```go +r := mux.NewRouter() +// Only matches if domain is "www.example.com". +r.Host("www.example.com") +// Matches a dynamic subdomain. +r.Host("{subdomain:[a-z]+}.domain.com") +``` There are several other matchers that can be added. To match path prefixes: - r.PathPrefix("/products/") +```go +r.PathPrefix("/products/") +``` ...or HTTP methods: - r.Methods("GET", "POST") +```go +r.Methods("GET", "POST") +``` ...or URL schemes: - r.Schemes("https") +```go +r.Schemes("https") +``` ...or header values: - r.Headers("X-Requested-With", "XMLHttpRequest") +```go +r.Headers("X-Requested-With", "XMLHttpRequest") +``` ...or query values: - r.Queries("key", "value") +```go +r.Queries("key", "value") +``` ...or to use a custom matcher function: - r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool { - return r.ProtoMajor == 0 - }) +```go +r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool { + return r.ProtoMajor == 0 +}) +``` ...and finally, it is possible to combine several matchers in a single route: - r.HandleFunc("/products", ProductsHandler). - Host("www.example.com"). - Methods("GET"). - Schemes("http") +```go +r.HandleFunc("/products", ProductsHandler). + Host("www.example.com"). + Methods("GET"). + Schemes("http") +``` -Setting the same matching conditions again and again can be boring, so we have -a way to group several routes that share the same requirements. -We call it "subrouting". +Setting the same matching conditions again and again can be boring, so we have a way to group several routes that share the same requirements. We call it "subrouting". -For example, let's say we have several URLs that should only match when the -host is `www.example.com`. Create a route for that host and get a "subrouter" -from it: +For example, let's say we have several URLs that should only match when the host is `www.example.com`. Create a route for that host and get a "subrouter" from it: - r := mux.NewRouter() - s := r.Host("www.example.com").Subrouter() +```go +r := mux.NewRouter() +s := r.Host("www.example.com").Subrouter() +``` Then register routes in the subrouter: - s.HandleFunc("/products/", ProductsHandler) - s.HandleFunc("/products/{key}", ProductHandler) - s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) +```go +s.HandleFunc("/products/", ProductsHandler) +s.HandleFunc("/products/{key}", ProductHandler) +s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) +``` -The three URL paths we registered above will only be tested if the domain is -`www.example.com`, because the subrouter is tested first. This is not -only convenient, but also optimizes request matching. You can create -subrouters combining any attribute matchers accepted by a route. +The three URL paths we registered above will only be tested if the domain is `www.example.com`, because the subrouter is tested first. This is not only convenient, but also optimizes request matching. You can create subrouters combining any attribute matchers accepted by a route. -Subrouters can be used to create domain or path "namespaces": you define -subrouters in a central place and then parts of the app can register its -paths relatively to a given subrouter. +Subrouters can be used to create domain or path "namespaces": you define subrouters in a central place and then parts of the app can register its paths relatively to a given subrouter. -There's one more thing about subroutes. When a subrouter has a path prefix, -the inner routes use it as base for their paths: +There's one more thing about subroutes. When a subrouter has a path prefix, the inner routes use it as base for their paths: - r := mux.NewRouter() - s := r.PathPrefix("/products").Subrouter() - // "/products/" - s.HandleFunc("/", ProductsHandler) - // "/products/{key}/" - s.HandleFunc("/{key}/", ProductHandler) - // "/products/{key}/details" - s.HandleFunc("/{key}/details", ProductDetailsHandler) +```go +r := mux.NewRouter() +s := r.PathPrefix("/products").Subrouter() +// "/products/" +s.HandleFunc("/", ProductsHandler) +// "/products/{key}/" +s.HandleFunc("/{key}/", ProductHandler) +// "/products/{key}/details" +s.HandleFunc("/{key}/details", ProductDetailsHandler) +``` Now let's see how to build registered URLs. -Routes can be named. All routes that define a name can have their URLs built, -or "reversed". We define a name calling Name() on a route. For example: +Routes can be named. All routes that define a name can have their URLs built, or "reversed". We define a name calling `Name()` on a route. For example: - r := mux.NewRouter() - r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). - Name("article") +```go +r := mux.NewRouter() +r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). + Name("article") +``` -To build a URL, get the route and call the URL() method, passing a sequence of -key/value pairs for the route variables. For the previous route, we would do: +To build a URL, get the route and call the `URL()` method, passing a sequence of key/value pairs for the route variables. For the previous route, we would do: - url, err := r.Get("article").URL("category", "technology", "id", "42") +```go +url, err := r.Get("article").URL("category", "technology", "id", "42") +``` -...and the result will be a url.URL with the following path: +...and the result will be a `url.URL` with the following path: - "/articles/technology/42" +``` +"/articles/technology/42" +``` This also works for host variables: - r := mux.NewRouter() - r.Host("{subdomain}.domain.com"). - Path("/articles/{category}/{id:[0-9]+}"). - HandlerFunc(ArticleHandler). - Name("article") - - // url.String() will be "http://news.domain.com/articles/technology/42" - url, err := r.Get("article").URL("subdomain", "news", - "category", "technology", - "id", "42") +```go +r := mux.NewRouter() +r.Host("{subdomain}.domain.com"). + Path("/articles/{category}/{id:[0-9]+}"). + HandlerFunc(ArticleHandler). + Name("article") + +// url.String() will be "http://news.domain.com/articles/technology/42" +url, err := r.Get("article").URL("subdomain", "news", + "category", "technology", + "id", "42") +``` -All variables defined in the route are required, and their values must -conform to the corresponding patterns. These requirements guarantee that a -generated URL will always match a registered route -- the only exception is -for explicitly defined "build-only" routes which never match. +All variables defined in the route are required, and their values must conform to the corresponding patterns. These requirements guarantee that a generated URL will always match a registered route -- the only exception is for explicitly defined "build-only" routes which never match. Regex support also exists for matching Headers within a route. For example, we could do: - r.HeadersRegexp("Content-Type", "application/(text|json)") - -...and the route will match both requests with a Content-Type of `application/json` as well as -`application/text` +```go +r.HeadersRegexp("Content-Type", "application/(text|json)") +``` -There's also a way to build only the URL host or path for a route: -use the methods URLHost() or URLPath() instead. For the previous route, -we would do: +...and the route will match both requests with a Content-Type of `application/json` as well as `application/text` - // "http://news.domain.com/" - host, err := r.Get("article").URLHost("subdomain", "news") +There's also a way to build only the URL host or path for a route: use the methods `URLHost()` or `URLPath()` instead. For the previous route, we would do: - // "/articles/technology/42" - path, err := r.Get("article").URLPath("category", "technology", "id", "42") +```go +// "http://news.domain.com/" +host, err := r.Get("article").URLHost("subdomain", "news") -And if you use subrouters, host and path defined separately can be built -as well: +// "/articles/technology/42" +path, err := r.Get("article").URLPath("category", "technology", "id", "42") +``` - r := mux.NewRouter() - s := r.Host("{subdomain}.domain.com").Subrouter() - s.Path("/articles/{category}/{id:[0-9]+}"). - HandlerFunc(ArticleHandler). - Name("article") +And if you use subrouters, host and path defined separately can be built as well: - // "http://news.domain.com/articles/technology/42" - url, err := r.Get("article").URL("subdomain", "news", - "category", "technology", - "id", "42") +```go +r := mux.NewRouter() +s := r.Host("{subdomain}.domain.com").Subrouter() +s.Path("/articles/{category}/{id:[0-9]+}"). + HandlerFunc(ArticleHandler). + Name("article") + +// "http://news.domain.com/articles/technology/42" +url, err := r.Get("article").URL("subdomain", "news", + "category", "technology", + "id", "42") +``` ## Full Example -Here's a complete, runnable example of a small mux based server: +Here's a complete, runnable example of a small `mux` based server: ```go package main From f48927253fa183f81eda684da508ba9226b87e9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20K=C5=82ak?= Date: Sat, 23 Jan 2016 17:42:00 +0100 Subject: [PATCH 32/35] Named groups replaced with conditional wrapping of regexps --- bench_test.go | 13 +++++++++++ regexp.go | 65 ++++++++++++++++++++++++--------------------------- 2 files changed, 44 insertions(+), 34 deletions(-) diff --git a/bench_test.go b/bench_test.go index c5f97b2..04409ab 100644 --- a/bench_test.go +++ b/bench_test.go @@ -19,3 +19,16 @@ func BenchmarkMux(b *testing.B) { router.ServeHTTP(nil, request) } } + +func BenchmarkMuxAlternativeInRegexp(b *testing.B) { + router := new(Router) + handler := func(w http.ResponseWriter, r *http.Request) {} + router.HandleFunc("/v1/{v1:(a|b)}", handler) + + requestA, _ := http.NewRequest("GET", "/v1/a", nil) + requestB, _ := http.NewRequest("GET", "/v1/b", nil) + for i := 0; i < b.N; i++ { + router.ServeHTTP(nil, requestA) + router.ServeHTTP(nil, requestB) + } +} diff --git a/regexp.go b/regexp.go index 06728dd..3c3a31b 100644 --- a/regexp.go +++ b/regexp.go @@ -73,14 +73,17 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash tpl[idxs[i]:end]) } // Build the regexp pattern. - varIdx := i / 2 - fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), varGroupName(varIdx), patt) + if patt[0] == '(' && patt[len(patt)-1] == ')' { + fmt.Fprintf(pattern, "%s%s", regexp.QuoteMeta(raw), patt) + } else { + fmt.Fprintf(pattern, "%s(%s)", regexp.QuoteMeta(raw), patt) + } // Build the reverse template. fmt.Fprintf(reverse, "%s%%s", raw) // Append variable name and compiled pattern. - varsN[varIdx] = name - varsR[varIdx], err = regexp.Compile(fmt.Sprintf("^%s$", patt)) + varsN[i/2] = name + varsR[i/2], err = regexp.Compile(fmt.Sprintf("^%s$", patt)) if err != nil { return nil, err } @@ -246,30 +249,17 @@ type routeRegexpGroup struct { func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) { // Store host variables. if v.host != nil { - hostVars := v.host.regexp.FindStringSubmatch(getHost(req)) - if hostVars != nil { - subexpNames := v.host.regexp.SubexpNames() - varName := 0 - for i, name := range subexpNames[1:] { - if name != "" && name == varGroupName(varName) { - m.Vars[v.host.varsN[varName]] = hostVars[i+1] - varName++ - } - } + host := getHost(req) + matches := v.host.regexp.FindStringSubmatchIndex(host) + if len(matches) > 0 { + extractVars(host, matches, v.host.varsN, m.Vars) } } // Store path variables. if v.path != nil { - pathVars := v.path.regexp.FindStringSubmatch(req.URL.Path) - if pathVars != nil { - subexpNames := v.path.regexp.SubexpNames() - varName := 0 - for i, name := range subexpNames[1:] { - if name != "" && name == varGroupName(varName) { - m.Vars[v.path.varsN[varName]] = pathVars[i+1] - varName++ - } - } + matches := v.path.regexp.FindStringSubmatchIndex(req.URL.Path) + if len(matches) > 0 { + extractVars(req.URL.Path, matches, v.path.varsN, m.Vars) // Check if we should redirect. if v.path.strictSlash { p1 := strings.HasSuffix(req.URL.Path, "/") @@ -288,16 +278,10 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) } // Store query string variables. for _, q := range v.queries { - queryVars := q.regexp.FindStringSubmatch(q.getUrlQuery(req)) - if queryVars != nil { - subexpNames := q.regexp.SubexpNames() - varName := 0 - for i, name := range subexpNames[1:] { - if name != "" && name == varGroupName(varName) { - m.Vars[q.varsN[varName]] = queryVars[i+1] - varName++ - } - } + queryUrl := q.getUrlQuery(req) + matches := q.regexp.FindStringSubmatchIndex(queryUrl) + if len(matches) > 0 { + extractVars(queryUrl, matches, q.varsN, m.Vars) } } } @@ -315,3 +299,16 @@ func getHost(r *http.Request) string { return host } + +func extractVars(input string, matches []int, names []string, output map[string]string) { + matchesCount := 0 + prevEnd := -1 + for i := 2; i < len(matches) && matchesCount < len(names); i += 2 { + if prevEnd < matches[i+1] { + value := input[matches[i]:matches[i+1]] + output[names[matchesCount]] = value + prevEnd = matches[i+1] + matchesCount++ + } + } +} From 78fb8eb962166e2ed581b9e30619fc353d2758b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20K=C5=82ak?= Date: Sat, 23 Jan 2016 18:09:52 +0100 Subject: [PATCH 33/35] Added benchmark with deep path --- bench_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/bench_test.go b/bench_test.go index 04409ab..946289b 100644 --- a/bench_test.go +++ b/bench_test.go @@ -6,6 +6,7 @@ package mux import ( "net/http" + "net/http/httptest" "testing" ) @@ -32,3 +33,17 @@ func BenchmarkMuxAlternativeInRegexp(b *testing.B) { router.ServeHTTP(nil, requestB) } } + +func BenchmarkManyPathVariables(b *testing.B) { + router := new(Router) + handler := func(w http.ResponseWriter, r *http.Request) {} + router.HandleFunc("/v1/{v1}/{v2}/{v3}/{v4}/{v5}", handler) + + matchingRequest, _ := http.NewRequest("GET", "/v1/1/2/3/4/5", nil) + notMatchingRequest, _ := http.NewRequest("GET", "/v1/1/2/3/4", nil) + recorder := httptest.NewRecorder() + for i := 0; i < b.N; i++ { + router.ServeHTTP(nil, matchingRequest) + router.ServeHTTP(recorder, notMatchingRequest) + } +} From 7872f90afae404a743afc2e74ef5e21797879951 Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Fri, 26 Feb 2016 13:23:39 -0800 Subject: [PATCH 34/35] Update .travis.yml to build Go 1.6 --- .travis.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 83ab8f5..4dcdacb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,20 @@ language: go sudo: false -go: - - 1.3 - - 1.4 - - 1.5 - - tip + +matrix: + include: + - go: 1.2 + - go: 1.3 + - go: 1.4 + - go: 1.5 + - go: 1.6 + - go: tip + install: - go get golang.org/x/tools/cmd/vet + script: - go get -t -v ./... - - diff -u <(echo -n) <(gofmt -d -s .) + - diff -u <(echo -n) <(gofmt -d .) - go tool vet . - go test -v -race ./... From 5f42f0f524cc51ac5f74cdedf0298bed528981f2 Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Sun, 28 Feb 2016 12:25:42 -0800 Subject: [PATCH 35/35] [docs] Add http://www.gorillatoolkit.org/pkg/mux to README. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b987c9e..9516c51 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ mux [![GoDoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux) [![Build Status](https://travis-ci.org/gorilla/mux.svg?branch=master)](https://travis-ci.org/gorilla/mux) +http://www.gorillatoolkit.org/pkg/mux + Package `gorilla/mux` implements a request router and dispatcher. The name mux stands for "HTTP request multiplexer". Like the standard `http.ServeMux`, `mux.Router` matches incoming requests against a list of registered routes and calls a handler for the route that matches the URL or other conditions. The main features are: