From 4e8d10dc715feb79b43f1d607b45148537546a0a Mon Sep 17 00:00:00 2001 From: rodrigo moraes Date: Tue, 2 Oct 2012 14:32:24 -0700 Subject: [PATCH 01/91] Initial commit --- README.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..ba77972 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +mux +=== \ No newline at end of file From eac83ba2c004bb759a2875b1f1dbb032adf8bb4a Mon Sep 17 00:00:00 2001 From: moraes Date: Wed, 3 Oct 2012 01:48:17 -0300 Subject: [PATCH 02/91] Initial files. --- LICENSE | 27 ++ bench_test.go | 21 ++ doc.go | 197 +++++++++++++ mux.go | 335 ++++++++++++++++++++++ mux_test.go | 417 +++++++++++++++++++++++++++ old_test.go | 758 ++++++++++++++++++++++++++++++++++++++++++++++++++ regexp.go | 247 ++++++++++++++++ route.go | 499 +++++++++++++++++++++++++++++++++ 8 files changed, 2501 insertions(+) create mode 100644 LICENSE create mode 100644 bench_test.go create mode 100644 doc.go create mode 100644 mux.go create mode 100644 mux_test.go create mode 100644 old_test.go create mode 100644 regexp.go create mode 100644 route.go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0e5fb87 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012 Rodrigo Moraes. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/bench_test.go b/bench_test.go new file mode 100644 index 0000000..c5f97b2 --- /dev/null +++ b/bench_test.go @@ -0,0 +1,21 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mux + +import ( + "net/http" + "testing" +) + +func BenchmarkMux(b *testing.B) { + router := new(Router) + handler := func(w http.ResponseWriter, r *http.Request) {} + router.HandleFunc("/v1/{v1}", handler) + + request, _ := http.NewRequest("GET", "/v1/anything", nil) + for i := 0; i < b.N; i++ { + router.ServeHTTP(nil, request) + } +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..217e948 --- /dev/null +++ b/doc.go @@ -0,0 +1,197 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +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. + +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(myFunc) + +...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. + +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") +*/ +package mux diff --git a/mux.go b/mux.go new file mode 100644 index 0000000..62f00e8 --- /dev/null +++ b/mux.go @@ -0,0 +1,335 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mux + +import ( + "fmt" + "net/http" + "path" + + "github.com/gorilla/context" +) + +// NewRouter returns a new router instance. +func NewRouter() *Router { + return &Router{namedRoutes: make(map[string]*Route)} +} + +// Router registers routes to be matched and dispatches a handler. +// +// It implements the http.Handler interface, so it can be registered to serve +// requests: +// +// var router = mux.NewRouter() +// +// func main() { +// http.Handle("/", router) +// } +// +// Or, for Google App Engine, register it in a init() function: +// +// func init() { +// http.Handle("/", router) +// } +// +// This will send all incoming requests to the router. +type Router struct { + // Configurable Handler to be used when no route matches. + NotFoundHandler http.Handler + // Parent route, if this is a subrouter. + parent parentRoute + // Routes to be matched, in order. + routes []*Route + // Routes by name for URL building. + namedRoutes map[string]*Route + // See Router.StrictSlash(). This defines the flag for new routes. + strictSlash bool +} + +// Match matches registered routes against the request. +func (r *Router) Match(req *http.Request, match *RouteMatch) bool { + for _, route := range r.routes { + if matched := route.Match(req, match); matched { + return true + } + } + return false +} + +// ServeHTTP dispatches the handler registered in the matched route. +// +// When there is a match, the route variables can be retrieved calling +// mux.Vars(request). +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 { + w.Header().Set("Location", p) + w.WriteHeader(http.StatusMovedPermanently) + return + } + var match RouteMatch + var handler http.Handler + if matched := r.Match(req, &match); matched { + handler = match.Handler + setVars(req, match.Vars) + setCurrentRoute(req, match.Route) + } + if handler == nil { + if r.NotFoundHandler == nil { + r.NotFoundHandler = http.NotFoundHandler() + } + handler = r.NotFoundHandler + } + defer context.Clear(req) + handler.ServeHTTP(w, req) +} + +// Get returns a route registered with the given name. +func (r *Router) Get(name string) *Route { + return r.getNamedRoutes()[name] +} + +// GetRoute returns a route registered with the given name. This method +// was renamed to Get() and remains here for backwards compatibility. +func (r *Router) GetRoute(name string) *Route { + return r.getNamedRoutes()[name] +} + +// StrictSlash defines the slash behavior for new routes. +// +// When true, if the route path is "/path/", accessing "/path" will redirect +// to the former and vice versa. +// +// Special case: when a route sets a path prefix, strict slash is +// automatically set to false for that route because the redirect behavior +// can't be determined for prefixes. +func (r *Router) StrictSlash(value bool) *Router { + r.strictSlash = value + return r +} + +// ---------------------------------------------------------------------------- +// parentRoute +// ---------------------------------------------------------------------------- + +// getNamedRoutes returns the map where named routes are registered. +func (r *Router) getNamedRoutes() map[string]*Route { + if r.namedRoutes == nil { + if r.parent != nil { + r.namedRoutes = r.parent.getNamedRoutes() + } else { + r.namedRoutes = make(map[string]*Route) + } + } + return r.namedRoutes +} + +// getRegexpGroup returns regexp definitions from the parent route, if any. +func (r *Router) getRegexpGroup() *routeRegexpGroup { + if r.parent != nil { + return r.parent.getRegexpGroup() + } + return nil +} + +// ---------------------------------------------------------------------------- +// Route factories +// ---------------------------------------------------------------------------- + +// NewRoute registers an empty route. +func (r *Router) NewRoute() *Route { + route := &Route{parent: r, strictSlash: r.strictSlash} + r.routes = append(r.routes, route) + return route +} + +// Handle registers a new route with a matcher for the URL path. +// See Route.Path() and Route.Handler(). +func (r *Router) Handle(path string, handler http.Handler) *Route { + return r.NewRoute().Path(path).Handler(handler) +} + +// HandleFunc registers a new route with a matcher for the URL path. +// See Route.Path() and Route.HandlerFunc(). +func (r *Router) HandleFunc(path string, f func(http.ResponseWriter, + *http.Request),) *Route { + return r.NewRoute().Path(path).HandlerFunc(f) +} + +// Headers registers a new route with a matcher for request header values. +// See Route.Headers(). +func (r *Router) Headers(pairs ...string) *Route { + return r.NewRoute().Headers(pairs...) +} + +// Host registers a new route with a matcher for the URL host. +// See Route.Host(). +func (r *Router) Host(tpl string) *Route { + return r.NewRoute().Host(tpl) +} + +// MatcherFunc registers a new route with a custom matcher function. +// See Route.MatcherFunc(). +func (r *Router) MatcherFunc(f MatcherFunc) *Route { + return r.NewRoute().MatcherFunc(f) +} + +// Methods registers a new route with a matcher for HTTP methods. +// See Route.Methods(). +func (r *Router) Methods(methods ...string) *Route { + return r.NewRoute().Methods(methods...) +} + +// Path registers a new route with a matcher for the URL path. +// See Route.Path(). +func (r *Router) Path(tpl string) *Route { + return r.NewRoute().Path(tpl) +} + +// PathPrefix registers a new route with a matcher for the URL path prefix. +// See Route.PathPrefix(). +func (r *Router) PathPrefix(tpl string) *Route { + return r.NewRoute().PathPrefix(tpl) +} + +// Queries registers a new route with a matcher for URL query values. +// See Route.Queries(). +func (r *Router) Queries(pairs ...string) *Route { + return r.NewRoute().Queries(pairs...) +} + +// Schemes registers a new route with a matcher for URL schemes. +// See Route.Schemes(). +func (r *Router) Schemes(schemes ...string) *Route { + return r.NewRoute().Schemes(schemes...) +} + +// ---------------------------------------------------------------------------- +// Context +// ---------------------------------------------------------------------------- + +// RouteMatch stores information about a matched route. +type RouteMatch struct { + Route *Route + Handler http.Handler + Vars map[string]string +} + +type contextKey int + +const ( + varsKey contextKey = iota + routeKey +) + +// Vars returns the route variables for the current request, if any. +func Vars(r *http.Request) map[string]string { + if rv := context.Get(r, varsKey); rv != nil { + return rv.(map[string]string) + } + return nil +} + +// CurrentRoute returns the matched route for the current request, if any. +func CurrentRoute(r *http.Request) *Route { + if rv := context.Get(r, routeKey); rv != nil { + return rv.(*Route) + } + return nil +} + +func setVars(r *http.Request, val interface{}) { + context.Set(r, varsKey, val) +} + +func setCurrentRoute(r *http.Request, val interface{}) { + context.Set(r, routeKey, val) +} + +// ---------------------------------------------------------------------------- +// Helpers +// ---------------------------------------------------------------------------- + +// cleanPath returns the canonical path for p, eliminating . and .. elements. +// Borrowed from the net/http package. +func cleanPath(p string) string { + if p == "" { + return "/" + } + if p[0] != '/' { + p = "/" + p + } + np := path.Clean(p) + // path.Clean removes trailing slash except for root; + // put the trailing slash back if necessary. + if p[len(p)-1] == '/' && np != "/" { + np += "/" + } + return np +} + +// uniqueVars returns an error if two slices contain duplicated strings. +func uniqueVars(s1, s2 []string) error { + for _, v1 := range s1 { + for _, v2 := range s2 { + if v1 == v2 { + return fmt.Errorf("mux: duplicated route variable %q", v2) + } + } + } + return nil +} + +// mapFromPairs converts variadic string parameters to a string map. +func mapFromPairs(pairs ...string) (map[string]string, error) { + length := len(pairs) + if length%2 != 0 { + return nil, fmt.Errorf( + "mux: number of parameters must be multiple of 2, got %v", pairs) + } + m := make(map[string]string, length/2) + for i := 0; i < length; i += 2 { + m[pairs[i]] = pairs[i+1] + } + 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 { + if v == value { + return true + } + } + return false +} + +// 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 { + 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 != "" { + // 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 == value { + valueExists = true + break + } + } + if !valueExists { + return false + } + } + } + return true +} diff --git a/mux_test.go b/mux_test.go new file mode 100644 index 0000000..19e0daf --- /dev/null +++ b/mux_test.go @@ -0,0 +1,417 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mux + +import ( + "fmt" + "net/http" + "testing" +) + +func TestRoute(t *testing.T) { + var route *Route + var request *http.Request + var vars map[string]string + var host, path, url string + + // Setup an id so we can see which test failed. :) + var idValue int + id := func() int { + idValue++ + return idValue + } + + // Host ------------------------------------------------------------------- + + route = new(Route).Host("aaa.bbb.ccc") + request, _ = http.NewRequest("GET", "http://aaa.bbb.ccc/111/222/333", nil) + vars = map[string]string{} + host = "aaa.bbb.ccc" + path = "" + url = host + path + testRoute(t, id(), true, route, request, vars, host, path, url) + // Non-match for the same config. + request, _ = http.NewRequest("GET", "http://aaa.222.ccc/111/222/333", nil) + testRoute(t, id(), false, route, request, vars, host, path, url) + + route = new(Route).Host("aaa.{v1:[a-z]{3}}.ccc") + request, _ = http.NewRequest("GET", "http://aaa.bbb.ccc/111/222/333", nil) + vars = map[string]string{"v1": "bbb"} + host = "aaa.bbb.ccc" + path = "" + url = host + path + testRoute(t, id(), true, route, request, vars, host, path, url) + // Non-match for the same config. + request, _ = http.NewRequest("GET", "http://aaa.222.ccc/111/222/333", nil) + testRoute(t, id(), false, route, request, vars, host, path, url) + + route = new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}") + request, _ = http.NewRequest("GET", "http://aaa.bbb.ccc/111/222/333", nil) + vars = map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"} + host = "aaa.bbb.ccc" + path = "" + url = host + path + testRoute(t, id(), true, route, request, vars, host, path, url) + // Non-match for the same config. + request, _ = http.NewRequest("GET", "http://aaa.222.ccc/111/222/333", nil) + testRoute(t, id(), false, route, request, vars, host, path, url) + + // Path ------------------------------------------------------------------- + + route = new(Route).Path("/111/222/333") + request, _ = http.NewRequest("GET", "http://localhost/111/222/333", nil) + vars = map[string]string{} + host = "" + path = "/111/222/333" + url = host + path + testRoute(t, id(), true, route, request, vars, host, path, url) + // Non-match for the same config. + request, _ = http.NewRequest("GET", "http://localhost/1/2/3", nil) + testRoute(t, id(), false, route, request, vars, host, path, url) + + route = new(Route).Path("/111/{v1:[0-9]{3}}/333") + request, _ = http.NewRequest("GET", "http://localhost/111/222/333", nil) + vars = map[string]string{"v1": "222"} + host = "" + path = "/111/222/333" + url = host + path + testRoute(t, id(), true, route, request, vars, host, path, url) + // Non-match for the same config. + request, _ = http.NewRequest("GET", "http://localhost/111/aaa/333", nil) + testRoute(t, id(), false, route, request, vars, host, path, url) + + route = new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}") + request, _ = http.NewRequest("GET", "http://localhost/111/222/333", nil) + vars = map[string]string{"v1": "111", "v2": "222", "v3": "333"} + host = "" + path = "/111/222/333" + url = host + path + testRoute(t, id(), true, route, request, vars, host, path, url) + // Non-match for the same config. + request, _ = http.NewRequest("GET", "http://localhost/111/aaa/333", nil) + testRoute(t, id(), false, route, request, vars, host, path, url) + + // PathPrefix ------------------------------------------------------------- + + route = new(Route).PathPrefix("/111") + request, _ = http.NewRequest("GET", "http://localhost/111/222/333", nil) + vars = map[string]string{} + host = "" + path = "/111" + url = host + path + testRoute(t, id(), true, route, request, vars, host, path, url) + // Non-match for the same config. + request, _ = http.NewRequest("GET", "http://localhost/1/2/3", nil) + testRoute(t, id(), false, route, request, vars, host, path, url) + + route = new(Route).PathPrefix("/111/{v1:[0-9]{3}}") + request, _ = http.NewRequest("GET", "http://localhost/111/222/333", nil) + vars = map[string]string{"v1": "222"} + host = "" + path = "/111/222" + url = host + path + testRoute(t, id(), true, route, request, vars, host, path, url) + // Non-match for the same config. + request, _ = http.NewRequest("GET", "http://localhost/111/aaa/333", nil) + testRoute(t, id(), false, route, request, vars, host, path, url) + + route = new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}") + request, _ = http.NewRequest("GET", "http://localhost/111/222/333", nil) + vars = map[string]string{"v1": "111", "v2": "222"} + host = "" + path = "/111/222" + url = host + path + testRoute(t, id(), true, route, request, vars, host, path, url) + // Non-match for the same config. + request, _ = http.NewRequest("GET", "http://localhost/111/aaa/333", nil) + testRoute(t, id(), false, route, request, vars, host, path, url) + + // Host + Path ------------------------------------------------------------ + + route = new(Route).Host("aaa.bbb.ccc").Path("/111/222/333") + request, _ = http.NewRequest("GET", "http://aaa.bbb.ccc/111/222/333", nil) + vars = map[string]string{} + host = "" + path = "" + url = host + path + testRoute(t, id(), true, route, request, vars, host, path, url) + // Non-match for the same config. + request, _ = http.NewRequest("GET", "http://aaa.222.ccc/111/222/333", nil) + testRoute(t, id(), false, route, request, vars, host, path, url) + + route = new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333") + request, _ = http.NewRequest("GET", "http://aaa.bbb.ccc/111/222/333", nil) + vars = map[string]string{"v1": "bbb", "v2": "222"} + host = "aaa.bbb.ccc" + path = "/111/222/333" + url = host + path + testRoute(t, id(), true, route, request, vars, host, path, url) + // Non-match for the same config. + request, _ = http.NewRequest("GET", "http://aaa.222.ccc/111/222/333", nil) + testRoute(t, id(), false, route, request, vars, host, path, url) + + route = new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}") + request, _ = http.NewRequest("GET", "http://aaa.bbb.ccc/111/222/333", nil) + vars = map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"} + host = "aaa.bbb.ccc" + path = "/111/222/333" + url = host + path + testRoute(t, id(), true, route, request, vars, host, path, url) + // Non-match for the same config. + request, _ = http.NewRequest("GET", "http://aaa.222.ccc/111/222/333", nil) + testRoute(t, id(), false, route, request, vars, host, path, url) + + // Headers ---------------------------------------------------------------- + + route = new(Route).Headers("foo", "bar", "baz", "ding") + request, _ = http.NewRequest("GET", "http://localhost", nil) + request.Header.Add("foo", "bar") + request.Header.Add("baz", "ding") + vars = map[string]string{} + host = "" + path = "" + url = host + path + testRoute(t, id(), true, route, request, vars, host, path, url) + // Non-match for the same config. + request, _ = http.NewRequest("GET", "http://localhost", nil) + request.Header.Add("foo", "bar") + request.Header.Add("baz", "dong") + testRoute(t, id(), false, route, request, vars, host, path, url) + + // Methods ---------------------------------------------------------------- + + route = new(Route).Methods("GET", "POST") + request, _ = http.NewRequest("GET", "http://localhost", nil) + vars = map[string]string{} + host = "" + path = "" + url = host + path + testRoute(t, id(), true, route, request, vars, host, path, url) + request, _ = http.NewRequest("POST", "http://localhost", nil) + testRoute(t, id(), true, route, request, vars, host, path, url) + // Non-match for the same config. + request, _ = http.NewRequest("PUT", "http://localhost", nil) + testRoute(t, id(), false, route, request, vars, host, path, url) + + // Queries ---------------------------------------------------------------- + + route = new(Route).Queries("foo", "bar", "baz", "ding") + request, _ = http.NewRequest("GET", "http://localhost?foo=bar&baz=ding", nil) + vars = map[string]string{} + host = "" + path = "" + url = host + path + testRoute(t, id(), true, route, request, vars, host, path, url) + // Non-match for the same config. + request, _ = http.NewRequest("GET", "http://localhost?foo=bar&baz=dong", nil) + testRoute(t, id(), false, route, request, vars, host, path, url) + + // Schemes ---------------------------------------------------------------- + + route = new(Route).Schemes("https", "ftp") + request, _ = http.NewRequest("GET", "https://localhost", nil) + vars = map[string]string{} + host = "" + path = "" + url = host + path + testRoute(t, id(), true, route, request, vars, host, path, url) + request, _ = http.NewRequest("GET", "ftp://localhost", nil) + testRoute(t, id(), true, route, request, vars, host, path, url) + // Non-match for the same config. + request, _ = http.NewRequest("GET", "http://localhost", nil) + testRoute(t, id(), false, route, request, vars, host, path, url) + + // Custom ----------------------------------------------------------------- + + m := func(r *http.Request, m *RouteMatch) bool { + if r.URL.Host == "aaa.bbb.ccc" { + return true + } + return false + } + route = new(Route).MatcherFunc(m) + request, _ = http.NewRequest("GET", "http://aaa.bbb.ccc", nil) + vars = map[string]string{} + host = "" + path = "" + url = host + path + testRoute(t, id(), true, route, request, vars, host, path, url) + // Non-match for the same config. + request, _ = http.NewRequest("GET", "http://aaa.ccc.bbb", nil) + testRoute(t, id(), false, route, request, vars, host, path, url) +} + +func TestSubRouter(t *testing.T) { + var route *Route + var request *http.Request + var vars map[string]string + var host, path, url string + + subrouter := new(Route).Host("{v1:[a-z]+}.google.com").Subrouter() + + // Setup an id so we can see which test failed. :) + var idValue int + id := func() int { + idValue++ + return idValue + } + + // ------------------------------------------------------------------------ + + route = subrouter.Path("/{v2:[a-z]+}") + request, _ = http.NewRequest("GET", "http://aaa.google.com/bbb", nil) + vars = map[string]string{"v1": "aaa", "v2": "bbb"} + host = "aaa.google.com" + path = "/bbb" + url = host + path + testRoute(t, id(), true, route, request, vars, host, path, url) + // Non-match for the same config. + request, _ = http.NewRequest("GET", "http://111.google.com/111", nil) + testRoute(t, id(), false, route, request, vars, host, path, url) + + // ------------------------------------------------------------------------ + + subrouter = new(Route).PathPrefix("/foo/{v1}").Subrouter() + route = subrouter.Path("/baz/{v2}") + request, _ = http.NewRequest("GET", "http://localhost/foo/bar/baz/ding", nil) + vars = map[string]string{"v1": "bar", "v2": "ding"} + host = "" + path = "/foo/bar/baz/ding" + url = host + path + testRoute(t, id(), true, route, request, vars, host, path, url) + // Non-match for the same config. + request, _ = http.NewRequest("GET", "http://localhost/foo/bar", nil) + testRoute(t, id(), false, route, request, vars, host, path, url) +} + +func TestNamedRoutes(t *testing.T) { + r1 := NewRouter() + r1.NewRoute().Name("a") + r1.NewRoute().Name("b") + r1.NewRoute().Name("c") + + r2 := r1.NewRoute().Subrouter() + r2.NewRoute().Name("d") + r2.NewRoute().Name("e") + r2.NewRoute().Name("f") + + r3 := r2.NewRoute().Subrouter() + r3.NewRoute().Name("g") + r3.NewRoute().Name("h") + r3.NewRoute().Name("i") + + if r1.namedRoutes == nil || len(r1.namedRoutes) != 9 { + t.Errorf("Expected 9 named routes, got %v", r1.namedRoutes) + } else if r1.Get("i") == nil { + t.Errorf("Subroute name not registered") + } +} + +// ---------------------------------------------------------------------------- +// Helpers +// ---------------------------------------------------------------------------- + +func getRouteTemplate(route *Route) string { + host, path := "none", "none" + if route.regexp != nil { + if route.regexp.host != nil { + host = route.regexp.host.template + } + if route.regexp.path != nil { + path = route.regexp.path.template + } + } + return fmt.Sprintf("Host: %v, Path: %v", host, path) +} + +func testRoute(t *testing.T, id int, shouldMatch bool, route *Route, + request *http.Request, vars map[string]string, host, path, url string) { + var match RouteMatch + ok := route.Match(request, &match) + if ok != shouldMatch { + msg := "Should match" + if !shouldMatch { + msg = "Should not match" + } + t.Errorf("(%v) %v:\nRoute: %#v\nRequest: %#v\nVars: %v\n", id, msg, route, request, vars) + return + } + if shouldMatch { + if vars != nil && !stringMapEqual(vars, match.Vars) { + t.Errorf("(%v) Vars not equal: expected %v, got %v", id, vars, match.Vars) + return + } + if host != "" { + u, _ := route.URLHost(mapToPairs(match.Vars)...) + if host != u.Host { + t.Errorf("(%v) URLHost not equal: expected %v, got %v -- %v", id, host, u.Host, getRouteTemplate(route)) + return + } + } + if path != "" { + u, _ := route.URLPath(mapToPairs(match.Vars)...) + if path != u.Path { + t.Errorf("(%v) URLPath not equal: expected %v, got %v -- %v", id, path, u.Path, getRouteTemplate(route)) + return + } + } + if url != "" { + u, _ := route.URL(mapToPairs(match.Vars)...) + if url != u.Host+u.Path { + t.Errorf("(%v) URL not equal: expected %v, got %v -- %v", id, url, u.Host+u.Path, getRouteTemplate(route)) + return + } + } + } +} + +func TestStrictSlash(t *testing.T) { + var r *Router + var req *http.Request + var route *Route + var match *RouteMatch + var matched bool + + // StrictSlash should be ignored for path prefix. + // So we register a route ending in slash but it doesn't attempt to add + // the slash for a path not ending in slash. + r = NewRouter() + r.StrictSlash(true) + route = r.NewRoute().PathPrefix("/static/") + req, _ = http.NewRequest("GET", "http://localhost/static/logo.png", nil) + match = new(RouteMatch) + matched = r.Match(req, match) + if !matched { + t.Errorf("Should match request %q -- %v", req.URL.Path, getRouteTemplate(route)) + } + if match.Handler != nil { + t.Errorf("Should not redirect") + } +} + +func mapToPairs(m map[string]string) []string { + var i int + p := make([]string, len(m)*2) + for k, v := range m { + p[i] = k + p[i+1] = v + i += 2 + } + return p +} + +func stringMapEqual(m1, m2 map[string]string) bool { + nil1 := m1 == nil + nil2 := m2 == nil + if nil1 != nil2 || len(m1) != len(m2) { + return false + } + for k, v := range m1 { + if v != m2[k] { + return false + } + } + return true +} diff --git a/old_test.go b/old_test.go new file mode 100644 index 0000000..7e266bb --- /dev/null +++ b/old_test.go @@ -0,0 +1,758 @@ +// Old tests ported to Go1. This is a mess. Want to drop it one day. + +// Copyright 2011 Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mux + +import ( + "bytes" + "net/http" + "testing" +) + +// ---------------------------------------------------------------------------- +// ResponseRecorder +// ---------------------------------------------------------------------------- +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// ResponseRecorder is an implementation of http.ResponseWriter that +// records its mutations for later inspection in tests. +type ResponseRecorder struct { + Code int // the HTTP response code from WriteHeader + HeaderMap http.Header // the HTTP response headers + Body *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to + Flushed bool +} + +// NewRecorder returns an initialized ResponseRecorder. +func NewRecorder() *ResponseRecorder { + return &ResponseRecorder{ + HeaderMap: make(http.Header), + Body: new(bytes.Buffer), + } +} + +// DefaultRemoteAddr is the default remote address to return in RemoteAddr if +// an explicit DefaultRemoteAddr isn't set on ResponseRecorder. +const DefaultRemoteAddr = "1.2.3.4" + +// Header returns the response headers. +func (rw *ResponseRecorder) Header() http.Header { + return rw.HeaderMap +} + +// Write always succeeds and writes to rw.Body, if not nil. +func (rw *ResponseRecorder) Write(buf []byte) (int, error) { + if rw.Body != nil { + rw.Body.Write(buf) + } + if rw.Code == 0 { + rw.Code = http.StatusOK + } + return len(buf), nil +} + +// WriteHeader sets rw.Code. +func (rw *ResponseRecorder) WriteHeader(code int) { + rw.Code = code +} + +// Flush sets rw.Flushed to true. +func (rw *ResponseRecorder) Flush() { + rw.Flushed = true +} + +// ---------------------------------------------------------------------------- + +func TestRouteMatchers(t *testing.T) { + var scheme, host, path, query, method string + var headers map[string]string + var resultVars map[bool]map[string]string + + router := NewRouter() + router.NewRoute().Host("{var1}.google.com"). + Path("/{var2:[a-z]+}/{var3:[0-9]+}"). + Queries("foo", "bar"). + Methods("GET"). + Schemes("https"). + Headers("x-requested-with", "XMLHttpRequest") + router.NewRoute().Host("www.{var4}.com"). + PathPrefix("/foo/{var5:[a-z]+}/{var6:[0-9]+}"). + Queries("baz", "ding"). + Methods("POST"). + Schemes("http"). + Headers("Content-Type", "application/json") + + reset := func() { + // Everything match. + scheme = "https" + host = "www.google.com" + path = "/product/42" + query = "?foo=bar" + method = "GET" + headers = map[string]string{"X-Requested-With": "XMLHttpRequest"} + resultVars = map[bool]map[string]string{ + true: map[string]string{"var1": "www", "var2": "product", "var3": "42"}, + false: map[string]string{}, + } + } + + reset2 := func() { + // Everything match. + scheme = "http" + host = "www.google.com" + path = "/foo/product/42/path/that/is/ignored" + query = "?baz=ding" + method = "POST" + headers = map[string]string{"Content-Type": "application/json"} + resultVars = map[bool]map[string]string{ + true: map[string]string{"var4": "google", "var5": "product", "var6": "42"}, + false: map[string]string{}, + } + } + + match := func(shouldMatch bool) { + url := scheme + "://" + host + path + query + request, _ := http.NewRequest(method, url, nil) + for key, value := range headers { + request.Header.Add(key, value) + } + + var routeMatch RouteMatch + matched := router.Match(request, &routeMatch) + if matched != shouldMatch { + // Need better messages. :) + if matched { + t.Errorf("Should match.") + } else { + t.Errorf("Should not match.") + } + } + + if matched { + currentRoute := routeMatch.Route + if currentRoute == nil { + t.Errorf("Expected a current route.") + } + vars := routeMatch.Vars + expectedVars := resultVars[shouldMatch] + if len(vars) != len(expectedVars) { + t.Errorf("Expected vars: %v Got: %v.", expectedVars, vars) + } + for name, value := range vars { + if expectedVars[name] != value { + t.Errorf("Expected vars: %v Got: %v.", expectedVars, vars) + } + } + } + } + + // 1st route -------------------------------------------------------------- + + // Everything match. + reset() + match(true) + + // Scheme doesn't match. + reset() + scheme = "http" + match(false) + + // Host doesn't match. + reset() + host = "www.mygoogle.com" + match(false) + + // Path doesn't match. + reset() + path = "/product/notdigits" + match(false) + + // Query doesn't match. + reset() + query = "?foo=baz" + match(false) + + // Method doesn't match. + reset() + method = "POST" + match(false) + + // Header doesn't match. + reset() + headers = map[string]string{} + match(false) + + // Everything match, again. + reset() + match(true) + + // 2nd route -------------------------------------------------------------- + + // Everything match. + reset2() + match(true) + + // Scheme doesn't match. + reset2() + scheme = "https" + match(false) + + // Host doesn't match. + reset2() + host = "sub.google.com" + match(false) + + // Path doesn't match. + reset2() + path = "/bar/product/42" + match(false) + + // Query doesn't match. + reset2() + query = "?foo=baz" + match(false) + + // Method doesn't match. + reset2() + method = "GET" + match(false) + + // Header doesn't match. + reset2() + headers = map[string]string{} + match(false) + + // Everything match, again. + reset2() + match(true) +} + +type headerMatcherTest struct { + matcher headerMatcher + headers map[string]string + result bool +} + +var headerMatcherTests = []headerMatcherTest{ + { + matcher: headerMatcher(map[string]string{"x-requested-with": "XMLHttpRequest"}), + headers: map[string]string{"X-Requested-With": "XMLHttpRequest"}, + result: true, + }, + { + matcher: headerMatcher(map[string]string{"x-requested-with": ""}), + headers: map[string]string{"X-Requested-With": "anything"}, + result: true, + }, + { + matcher: headerMatcher(map[string]string{"x-requested-with": "XMLHttpRequest"}), + headers: map[string]string{}, + result: false, + }, +} + +type hostMatcherTest struct { + matcher *Route + url string + vars map[string]string + result bool +} + +var hostMatcherTests = []hostMatcherTest{ + { + matcher: NewRouter().NewRoute().Host("{foo:[a-z][a-z][a-z]}.{bar:[a-z][a-z][a-z]}.{baz:[a-z][a-z][a-z]}"), + url: "http://abc.def.ghi/", + vars: map[string]string{"foo": "abc", "bar": "def", "baz": "ghi"}, + result: true, + }, + { + matcher: NewRouter().NewRoute().Host("{foo:[a-z][a-z][a-z]}.{bar:[a-z][a-z][a-z]}.{baz:[a-z][a-z][a-z]}"), + url: "http://a.b.c/", + vars: map[string]string{"foo": "abc", "bar": "def", "baz": "ghi"}, + result: false, + }, +} + +type methodMatcherTest struct { + matcher methodMatcher + method string + result bool +} + +var methodMatcherTests = []methodMatcherTest{ + { + matcher: methodMatcher([]string{"GET", "POST", "PUT"}), + method: "GET", + result: true, + }, + { + matcher: methodMatcher([]string{"GET", "POST", "PUT"}), + method: "POST", + result: true, + }, + { + matcher: methodMatcher([]string{"GET", "POST", "PUT"}), + method: "PUT", + result: true, + }, + { + matcher: methodMatcher([]string{"GET", "POST", "PUT"}), + method: "DELETE", + result: false, + }, +} + +type pathMatcherTest struct { + matcher *Route + url string + vars map[string]string + result bool +} + +var pathMatcherTests = []pathMatcherTest{ + { + matcher: NewRouter().NewRoute().Path("/{foo:[0-9][0-9][0-9]}/{bar:[0-9][0-9][0-9]}/{baz:[0-9][0-9][0-9]}"), + url: "http://localhost:8080/123/456/789", + vars: map[string]string{"foo": "123", "bar": "456", "baz": "789"}, + result: true, + }, + { + matcher: NewRouter().NewRoute().Path("/{foo:[0-9][0-9][0-9]}/{bar:[0-9][0-9][0-9]}/{baz:[0-9][0-9][0-9]}"), + url: "http://localhost:8080/1/2/3", + vars: map[string]string{"foo": "123", "bar": "456", "baz": "789"}, + result: false, + }, +} + +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 + result bool +} + +var schemeMatcherTests = []schemeMatcherTest{ + { + matcher: schemeMatcher([]string{"http", "https"}), + url: "http://localhost:8080/", + result: true, + }, + { + matcher: schemeMatcher([]string{"http", "https"}), + url: "https://localhost:8080/", + result: true, + }, + { + matcher: schemeMatcher([]string{"https"}), + url: "http://localhost:8080/", + result: false, + }, + { + matcher: schemeMatcher([]string{"http"}), + url: "https://localhost:8080/", + result: false, + }, +} + +type urlBuildingTest struct { + route *Route + vars []string + url string +} + +var urlBuildingTests = []urlBuildingTest{ + { + route: new(Route).Host("foo.domain.com"), + vars: []string{}, + url: "http://foo.domain.com", + }, + { + route: new(Route).Host("{subdomain}.domain.com"), + vars: []string{"subdomain", "bar"}, + url: "http://bar.domain.com", + }, + { + route: new(Route).Host("foo.domain.com").Path("/articles"), + vars: []string{}, + url: "http://foo.domain.com/articles", + }, + { + route: new(Route).Path("/articles"), + vars: []string{}, + url: "/articles", + }, + { + route: new(Route).Path("/articles/{category}/{id:[0-9]+}"), + vars: []string{"category", "technology", "id", "42"}, + url: "/articles/technology/42", + }, + { + route: new(Route).Host("{subdomain}.domain.com").Path("/articles/{category}/{id:[0-9]+}"), + vars: []string{"subdomain", "foo", "category", "technology", "id", "42"}, + url: "http://foo.domain.com/articles/technology/42", + }, +} + +func TestHeaderMatcher(t *testing.T) { + for _, v := range headerMatcherTests { + request, _ := http.NewRequest("GET", "http://localhost:8080/", nil) + for key, value := range v.headers { + request.Header.Add(key, value) + } + var routeMatch RouteMatch + result := v.matcher.Match(request, &routeMatch) + if result != v.result { + if v.result { + t.Errorf("%#v: should match %v.", v.matcher, request.Header) + } else { + t.Errorf("%#v: should not match %v.", v.matcher, request.Header) + } + } + } +} + +func TestHostMatcher(t *testing.T) { + for _, v := range hostMatcherTests { + request, _ := http.NewRequest("GET", v.url, nil) + var routeMatch RouteMatch + result := v.matcher.Match(request, &routeMatch) + vars := routeMatch.Vars + 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) + } + } + if result { + if len(vars) != len(v.vars) { + t.Errorf("%#v: vars length should be %v, got %v.", v.matcher, len(v.vars), len(vars)) + } + for name, value := range vars { + if v.vars[name] != value { + t.Errorf("%#v: expected value %v for key %v, got %v.", v.matcher, v.vars[name], name, value) + } + } + } else { + if len(vars) != 0 { + t.Errorf("%#v: vars length should be 0, got %v.", v.matcher, len(vars)) + } + } + } +} + +func TestMethodMatcher(t *testing.T) { + for _, v := range methodMatcherTests { + request, _ := http.NewRequest(v.method, "http://localhost:8080/", 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.method) + } else { + t.Errorf("%#v: should not match %v.", v.matcher, v.method) + } + } + } +} + +func TestPathMatcher(t *testing.T) { + for _, v := range pathMatcherTests { + request, _ := http.NewRequest("GET", v.url, nil) + var routeMatch RouteMatch + result := v.matcher.Match(request, &routeMatch) + vars := routeMatch.Vars + 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) + } + } + if result { + if len(vars) != len(v.vars) { + t.Errorf("%#v: vars length should be %v, got %v.", v.matcher, len(v.vars), len(vars)) + } + for name, value := range vars { + if v.vars[name] != value { + t.Errorf("%#v: expected value %v for key %v, got %v.", v.matcher, v.vars[name], name, value) + } + } + } else { + if len(vars) != 0 { + t.Errorf("%#v: vars length should be 0, got %v.", v.matcher, len(vars)) + } + } + } +} + +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 { + 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 TestUrlBuilding(t *testing.T) { + + for _, v := range urlBuildingTests { + u, _ := v.route.URL(v.vars...) + url := u.String() + if url != v.url { + t.Errorf("expected %v, got %v", v.url, url) + /* + reversePath := "" + reverseHost := "" + if v.route.pathTemplate != nil { + reversePath = v.route.pathTemplate.Reverse + } + if v.route.hostTemplate != nil { + reverseHost = v.route.hostTemplate.Reverse + } + + t.Errorf("%#v:\nexpected: %q\ngot: %q\nreverse path: %q\nreverse host: %q", v.route, v.url, url, reversePath, reverseHost) + */ + } + } + + ArticleHandler := func(w http.ResponseWriter, r *http.Request) { + } + + router := NewRouter() + router.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).Name("article") + + url, _ := router.Get("article").URL("category", "technology", "id", "42") + expected := "/articles/technology/42" + if url.String() != expected { + t.Errorf("Expected %v, got %v", expected, url.String()) + } +} + +func TestMatchedRouteName(t *testing.T) { + routeName := "stock" + router := NewRouter() + route := router.NewRoute().Path("/products/").Name(routeName) + + url := "http://www.domain.com/products/" + request, _ := http.NewRequest("GET", url, nil) + var rv RouteMatch + ok := router.Match(request, &rv) + + if !ok || rv.Route != route { + t.Errorf("Expected same route, got %+v.", rv.Route) + } + + retName := rv.Route.GetName() + if retName != routeName { + t.Errorf("Expected %q, got %q.", routeName, retName) + } +} + +func TestSubRouting(t *testing.T) { + // Example from docs. + router := NewRouter() + subrouter := router.NewRoute().Host("www.domain.com").Subrouter() + route := subrouter.NewRoute().Path("/products/").Name("products") + + url := "http://www.domain.com/products/" + request, _ := http.NewRequest("GET", url, nil) + var rv RouteMatch + ok := router.Match(request, &rv) + + if !ok || rv.Route != route { + t.Errorf("Expected same route, got %+v.", rv.Route) + } + + u, _ := router.Get("products").URL() + builtUrl := u.String() + // Yay, subroute aware of the domain when building! + if builtUrl != url { + t.Errorf("Expected %q, got %q.", url, builtUrl) + } +} + +func TestVariableNames(t *testing.T) { + route := new(Route).Host("{arg1}.domain.com").Path("/{arg1}/{arg2:[0-9]+}") + if route.err == nil { + t.Errorf("Expected error for duplicated variable names") + } +} + +func TestRedirectSlash(t *testing.T) { + var route *Route + var routeMatch RouteMatch + r := NewRouter() + + r.StrictSlash(false) + route = r.NewRoute() + if route.strictSlash != false { + t.Errorf("Expected false redirectSlash.") + } + + r.StrictSlash(true) + route = r.NewRoute() + if route.strictSlash != true { + t.Errorf("Expected true redirectSlash.") + } + + route = new(Route) + route.strictSlash = true + route.Path("/{arg1}/{arg2:[0-9]+}/") + request, _ := http.NewRequest("GET", "http://localhost/foo/123", nil) + routeMatch = RouteMatch{} + _ = route.Match(request, &routeMatch) + vars := routeMatch.Vars + if vars["arg1"] != "foo" { + t.Errorf("Expected foo.") + } + if vars["arg2"] != "123" { + t.Errorf("Expected 123.") + } + rsp := NewRecorder() + routeMatch.Handler.ServeHTTP(rsp, request) + if rsp.HeaderMap.Get("Location") != "http://localhost/foo/123/" { + t.Errorf("Expected redirect header.") + } + + route = new(Route) + route.strictSlash = true + route.Path("/{arg1}/{arg2:[0-9]+}") + request, _ = http.NewRequest("GET", "http://localhost/foo/123/", nil) + routeMatch = RouteMatch{} + _ = route.Match(request, &routeMatch) + vars = routeMatch.Vars + if vars["arg1"] != "foo" { + t.Errorf("Expected foo.") + } + if vars["arg2"] != "123" { + t.Errorf("Expected 123.") + } + rsp = NewRecorder() + routeMatch.Handler.ServeHTTP(rsp, request) + if rsp.HeaderMap.Get("Location") != "http://localhost/foo/123" { + t.Errorf("Expected redirect header.") + } +} + +// Test for the new regexp library, still not available in stable Go. +func TestNewRegexp(t *testing.T) { + var p *routeRegexp + var matches []string + + tests := map[string]map[string][]string{ + "/{foo:a{2}}": { + "/a": nil, + "/aa": {"aa"}, + "/aaa": nil, + "/aaaa": nil, + }, + "/{foo:a{2,}}": { + "/a": nil, + "/aa": {"aa"}, + "/aaa": {"aaa"}, + "/aaaa": {"aaaa"}, + }, + "/{foo:a{2,3}}": { + "/a": nil, + "/aa": {"aa"}, + "/aaa": {"aaa"}, + "/aaaa": nil, + }, + "/{foo:[a-z]{3}}/{bar:[a-z]{2}}": { + "/a": nil, + "/ab": nil, + "/abc": nil, + "/abcd": nil, + "/abc/ab": {"abc", "ab"}, + "/abc/abc": nil, + "/abcd/ab": nil, + }, + `/{foo:\w{3,}}/{bar:\d{2,}}`: { + "/a": nil, + "/ab": nil, + "/abc": nil, + "/abc/1": nil, + "/abc/12": {"abc", "12"}, + "/abcd/12": {"abcd", "12"}, + "/abcd/123": {"abcd", "123"}, + }, + } + + for pattern, paths := range tests { + p, _ = newRouteRegexp(pattern, false, false, false) + for path, result := range paths { + matches = p.regexp.FindStringSubmatch(path) + if result == nil { + if matches != nil { + t.Errorf("%v should not match %v.", pattern, path) + } + } else { + if len(matches) != len(result)+1 { + t.Errorf("Expected %v matches, got %v.", len(result)+1, len(matches)) + } else { + for k, v := range result { + if matches[k+1] != v { + t.Errorf("Expected %v, got %v.", v, matches[k+1]) + } + } + } + } + } + } +} diff --git a/regexp.go b/regexp.go new file mode 100644 index 0000000..4c3482b --- /dev/null +++ b/regexp.go @@ -0,0 +1,247 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mux + +import ( + "bytes" + "fmt" + "net/http" + "net/url" + "regexp" + "strings" +) + +// newRouteRegexp parses a route template and returns a routeRegexp, +// used to match a host or path. +// +// It will extract named variables, assemble a regexp to be matched, create +// a "reverse" template to build URLs and compile regexps to validate variable +// values used in URL building. +// +// 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) { + // Check if it is well-formed. + idxs, errBraces := braceIndices(tpl) + if errBraces != nil { + return nil, errBraces + } + // Backup the original. + template := tpl + // Now let's parse it. + defaultPattern := "[^/]+" + if matchHost { + defaultPattern = "[^.]+" + matchPrefix, strictSlash = false, false + } + if matchPrefix { + strictSlash = false + } + // Set a flag for strictSlash. + endSlash := false + if strictSlash && strings.HasSuffix(tpl, "/") { + tpl = tpl[:len(tpl)-1] + endSlash = true + } + varsN := make([]string, len(idxs)/2) + varsR := make([]*regexp.Regexp, len(idxs)/2) + pattern := bytes.NewBufferString("^") + reverse := bytes.NewBufferString("") + var end int + var err error + for i := 0; i < len(idxs); i += 2 { + // Set all values we are interested in. + raw := tpl[end:idxs[i]] + end = idxs[i+1] + parts := strings.SplitN(tpl[idxs[i]+1:end-1], ":", 2) + name := parts[0] + patt := defaultPattern + if len(parts) == 2 { + patt = parts[1] + } + // Name or pattern can't be empty. + if name == "" || patt == "" { + return nil, fmt.Errorf("mux: missing name or pattern in %q", + tpl[idxs[i]:end]) + } + // Build the regexp pattern. + 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[i/2] = name + varsR[i/2], err = regexp.Compile(fmt.Sprintf("^%s$", patt)) + if err != nil { + return nil, err + } + } + // Add the remaining. + raw := tpl[end:] + pattern.WriteString(regexp.QuoteMeta(raw)) + if strictSlash { + pattern.WriteString("[/]?") + } + if !matchPrefix { + pattern.WriteByte('$') + } + reverse.WriteString(raw) + if endSlash { + reverse.WriteByte('/') + } + // Compile full regexp. + reg, errCompile := regexp.Compile(pattern.String()) + if errCompile != nil { + return nil, errCompile + } + // Done! + return &routeRegexp{ + template: template, + matchHost: matchHost, + regexp: reg, + reverse: reverse.String(), + varsN: varsN, + varsR: varsR, + }, nil +} + +// routeRegexp stores a regexp to match a host or path and information to +// collect and validate route variables. +type routeRegexp struct { + // The unmodified template. + template string + // True for host match, false for path match. + matchHost bool + // Expanded regexp. + regexp *regexp.Regexp + // Reverse template. + reverse string + // Variable names. + varsN []string + // Variable regexps (validators). + varsR []*regexp.Regexp +} + +// 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) + } + return r.regexp.MatchString(getHost(req)) +} + +// url builds a URL part using the given values. +func (r *routeRegexp) url(pairs ...string) (string, error) { + values, err := mapFromPairs(pairs...) + if err != nil { + return "", err + } + urlValues := make([]interface{}, len(r.varsN)) + for k, v := range r.varsN { + value, ok := values[v] + if !ok { + return "", fmt.Errorf("mux: missing route variable %q", v) + } + urlValues[k] = value + } + rv := fmt.Sprintf(r.reverse, urlValues...) + if !r.regexp.MatchString(rv) { + // The URL is checked against the full regexp, instead of checking + // individual variables. This is faster but to provide a good error + // message, we check individual regexps if the URL doesn't match. + for k, v := range r.varsN { + if !r.varsR[k].MatchString(values[v]) { + return "", fmt.Errorf( + "mux: variable %q doesn't match, expected %q", values[v], + r.varsR[k].String()) + } + } + } + return rv, nil +} + +// braceIndices returns the first level curly brace indices from a string. +// It returns an error in case of unbalanced braces. +func braceIndices(s string) ([]int, error) { + var level, idx int + idxs := make([]int, 0) + for i := 0; i < len(s); i++ { + switch s[i] { + case '{': + if level++; level == 1 { + idx = i + } + case '}': + if level--; level == 0 { + idxs = append(idxs, idx, i+1) + } else if level < 0 { + return nil, fmt.Errorf("mux: unbalanced braces in %q", s) + } + } + } + if level != 0 { + return nil, fmt.Errorf("mux: unbalanced braces in %q", s) + } + return idxs, nil +} + +// ---------------------------------------------------------------------------- +// routeRegexpGroup +// ---------------------------------------------------------------------------- + +// routeRegexpGroup groups the route matchers that carry variables. +type routeRegexpGroup struct { + host *routeRegexp + path *routeRegexp +} + +// setMatch extracts the variables from the URL once a route matches. +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 { + for k, v := range v.host.varsN { + m.Vars[v] = hostVars[k+1] + } + } + } + // Store path variables. + 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] + } + // Check if we should redirect. + if r.strictSlash { + p1 := strings.HasSuffix(req.URL.Path, "/") + p2 := strings.HasSuffix(v.path.template, "/") + if p1 != p2 { + u, _ := url.Parse(req.URL.String()) + if p1 { + u.Path = u.Path[:len(u.Path)-1] + } else { + u.Path += "/" + } + m.Handler = http.RedirectHandler(u.String(), 301) + } + } + } + } +} + +// getHost tries its best to return the request host. +func getHost(r *http.Request) string { + if !r.URL.IsAbs() { + host := r.Host + // Slice off any port information. + if i := strings.Index(host, ":"); i != -1 { + host = host[:i] + } + return host + } + return r.URL.Host +} diff --git a/route.go b/route.go new file mode 100644 index 0000000..cb538ea --- /dev/null +++ b/route.go @@ -0,0 +1,499 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mux + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "strings" +) + +// Route stores information to match a request and build URLs. +type Route struct { + // Parent where the route was registered (a Router). + parent parentRoute + // Request handler for the route. + handler http.Handler + // List of matchers. + matchers []matcher + // Manager for the variables from host and path. + regexp *routeRegexpGroup + // If true, when the path pattern is "/path/", accessing "/path" will + // redirect to the former and vice versa. + strictSlash bool + // If true, this route never matches: it is only used to build URLs. + buildOnly bool + // The name used to build URLs. + name string + // Error resulted from building a route. + err error +} + +// Match matches the route against the request. +func (r *Route) Match(req *http.Request, match *RouteMatch) bool { + if r.buildOnly || r.err != nil { + return false + } + // Match everything. + for _, m := range r.matchers { + if matched := m.Match(req, match); !matched { + return false + } + } + // Yay, we have a match. Let's collect some info about it. + if match.Route == nil { + match.Route = r + } + if match.Handler == nil { + match.Handler = r.handler + } + if match.Vars == nil { + match.Vars = make(map[string]string) + } + // Set variables. + if r.regexp != nil { + r.regexp.setMatch(req, match, r) + } + return true +} + +// ---------------------------------------------------------------------------- +// Route attributes +// ---------------------------------------------------------------------------- + +// GetError returns an error resulted from building the route, if any. +func (r *Route) GetError() error { + return r.err +} + +// BuildOnly sets the route to never match: it is only used to build URLs. +func (r *Route) BuildOnly() *Route { + r.buildOnly = true + return r +} + +// Handler -------------------------------------------------------------------- + +// Handler sets a handler for the route. +func (r *Route) Handler(handler http.Handler) *Route { + if r.err == nil { + r.handler = handler + } + return r +} + +// HandlerFunc sets a handler function for the route. +func (r *Route) HandlerFunc(f func(http.ResponseWriter, *http.Request)) *Route { + return r.Handler(http.HandlerFunc(f)) +} + +// GetHandler returns the handler for the route, if any. +func (r *Route) GetHandler() http.Handler { + return r.handler +} + +// Name ----------------------------------------------------------------------- + +// Name sets the name for the route, used to build URLs. +// If the name was registered already it will be overwritten. +func (r *Route) Name(name string) *Route { + if r.name != "" { + r.err = fmt.Errorf("mux: route already has name %q, can't set %q", + r.name, name) + } + if r.err == nil { + r.name = name + r.getNamedRoutes()[name] = r + } + return r +} + +// GetName returns the name for the route, if any. +func (r *Route) GetName() string { + return r.name +} + +// ---------------------------------------------------------------------------- +// Matchers +// ---------------------------------------------------------------------------- + +// matcher types try to match a request. +type matcher interface { + Match(*http.Request, *RouteMatch) bool +} + +// addMatcher adds a matcher to the route. +func (r *Route) addMatcher(m matcher) *Route { + if r.err == nil { + r.matchers = append(r.matchers, m) + } + return r +} + +// addRegexpMatcher adds a host or path matcher and builder to a route. +func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix bool) error { + if r.err != nil { + return r.err + } + r.regexp = r.getRegexpGroup() + if !matchHost { + if len(tpl) == 0 || tpl[0] != '/' { + return fmt.Errorf("mux: path must start with a slash, got %q", tpl) + } + if r.regexp.path != nil { + tpl = strings.TrimRight(r.regexp.path.template, "/") + tpl + } + } + rr, err := newRouteRegexp(tpl, matchHost, matchPrefix, r.strictSlash) + if err != nil { + return err + } + if matchHost { + if r.regexp.path != nil { + if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil { + return err + } + } + r.regexp.host = rr + } else { + if r.regexp.host != nil { + if err = uniqueVars(rr.varsN, r.regexp.host.varsN); err != nil { + return err + } + } + r.regexp.path = rr + } + r.addMatcher(rr) + return nil +} + +// Headers -------------------------------------------------------------------- + +// headerMatcher matches the request against header values. +type headerMatcher map[string]string + +func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool { + return matchMap(m, r.Header, true) +} + +// Headers adds a matcher for request header values. +// It accepts a sequence of key/value pairs to be matched. For example: +// +// r := mux.NewRouter() +// r.Headers("Content-Type", "application/json", +// "X-Requested-With", "XMLHttpRequest") +// +// The above route will only match if both request header values match. +// +// 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...) + return r.addMatcher(headerMatcher(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: +// +// - {name} matches anything until the next dot. +// +// - {name:pattern} matches the given regexp pattern. +// +// For example: +// +// r := mux.NewRouter() +// r.Host("www.domain.com") +// r.Host("{subdomain}.domain.com") +// r.Host("{subdomain:[a-z]+}.domain.com") +// +// 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) + return r +} + +// MatcherFunc ---------------------------------------------------------------- + +// MatcherFunc is the function signature used by custom matchers. +type MatcherFunc func(*http.Request, *RouteMatch) bool + +func (m MatcherFunc) Match(r *http.Request, match *RouteMatch) bool { + return m(r, match) +} + +// MatcherFunc adds a custom function to be used as request matcher. +func (r *Route) MatcherFunc(f MatcherFunc) *Route { + return r.addMatcher(f) +} + +// Methods -------------------------------------------------------------------- + +// methodMatcher matches the request against HTTP methods. +type methodMatcher []string + +func (m methodMatcher) Match(r *http.Request, match *RouteMatch) bool { + return matchInArray(m, r.Method) +} + +// Methods adds a matcher for HTTP methods. +// It accepts a sequence of one or more methods to be matched, e.g.: +// "GET", "POST", "PUT". +func (r *Route) Methods(methods ...string) *Route { + for k, v := range methods { + methods[k] = strings.ToUpper(v) + } + return r.addMatcher(methodMatcher(methods)) +} + +// Path ----------------------------------------------------------------------- + +// Path adds a matcher for the URL path. +// It accepts a template with zero or more URL variables enclosed by {}. +// Variables can define an optional regexp pattern to me matched: +// +// - {name} matches anything until the next slash. +// +// - {name:pattern} matches the given regexp pattern. +// +// For example: +// +// r := mux.NewRouter() +// r.Path("/products/").Handler(ProductsHandler) +// r.Path("/products/{key}").Handler(ProductsHandler) +// r.Path("/articles/{category}/{id:[0-9]+}"). +// Handler(ArticleHandler) +// +// 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) + return r +} + +// PathPrefix ----------------------------------------------------------------- + +// PathPrefix adds a matcher for the URL path prefix. +func (r *Route) PathPrefix(tpl string) *Route { + r.strictSlash = false + r.err = r.addRegexpMatcher(tpl, false, true) + 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: +// +// r := mux.NewRouter() +// r.Queries("foo", "bar", "baz", "ding") +// +// The above route will only match if the URL contains the defined queries +// values, e.g.: ?foo=bar&baz=ding. +// +// It the value is an empty string, it will match any value if the key is set. +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)) + } + return r +} + +// Schemes -------------------------------------------------------------------- + +// schemeMatcher matches the request against URL schemes. +type schemeMatcher []string + +func (m schemeMatcher) Match(r *http.Request, match *RouteMatch) bool { + return matchInArray(m, r.URL.Scheme) +} + +// Schemes adds a matcher for URL schemes. +// It accepts a sequence schemes to be matched, e.g.: "http", "https". +func (r *Route) Schemes(schemes ...string) *Route { + for k, v := range schemes { + schemes[k] = strings.ToLower(v) + } + return r.addMatcher(schemeMatcher(schemes)) +} + +// Subrouter ------------------------------------------------------------------ + +// Subrouter creates a subrouter for the 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.HandleFunc("/products/", ProductsHandler) +// s.HandleFunc("/products/{key}", ProductHandler) +// s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) +// +// Here, the routes registered in the subrouter won't be tested if the host +// doesn't match. +func (r *Route) Subrouter() *Router { + router := &Router{parent: r, strictSlash: r.strictSlash} + r.addMatcher(router) + return router +} + +// ---------------------------------------------------------------------------- +// URL building +// ---------------------------------------------------------------------------- + +// URL builds a URL for the route. +// +// It accepts a sequence of key/value pairs for the route variables. For +// example, given this route: +// +// r := mux.NewRouter() +// r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). +// Name("article") +// +// ...a URL for it can be built using: +// +// url, err := r.Get("article").URL("category", "technology", "id", "42") +// +// ...which will return an url.URL with the following path: +// +// "/articles/technology/42" +// +// This also works for host variables: +// +// r := mux.NewRouter() +// r.Host("{subdomain}.domain.com"). +// HandleFunc("/articles/{category}/{id:[0-9]+}", 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. +func (r *Route) URL(pairs ...string) (*url.URL, error) { + if r.err != nil { + return nil, r.err + } + if r.regexp == nil { + return nil, errors.New("mux: route doesn't have a host or path") + } + var scheme, host, path string + var err error + if r.regexp.host != nil { + // Set a default scheme. + scheme = "http" + if host, err = r.regexp.host.url(pairs...); err != nil { + return nil, err + } + } + if r.regexp.path != nil { + if path, err = r.regexp.path.url(pairs...); err != nil { + return nil, err + } + } + return &url.URL{ + Scheme: scheme, + Host: host, + Path: path, + }, nil +} + +// URLHost builds the host part of the URL for a route. See Route.URL(). +// +// The route must have a host defined. +func (r *Route) URLHost(pairs ...string) (*url.URL, error) { + if r.err != nil { + return nil, r.err + } + if r.regexp == nil || r.regexp.host == nil { + return nil, errors.New("mux: route doesn't have a host") + } + host, err := r.regexp.host.url(pairs...) + if err != nil { + return nil, err + } + return &url.URL{ + Scheme: "http", + Host: host, + }, nil +} + +// URLPath builds the path part of the URL for a route. See Route.URL(). +// +// The route must have a path defined. +func (r *Route) URLPath(pairs ...string) (*url.URL, error) { + if r.err != nil { + return nil, r.err + } + if r.regexp == nil || r.regexp.path == nil { + return nil, errors.New("mux: route doesn't have a path") + } + path, err := r.regexp.path.url(pairs...) + if err != nil { + return nil, err + } + return &url.URL{ + Path: path, + }, nil +} + +// ---------------------------------------------------------------------------- +// parentRoute +// ---------------------------------------------------------------------------- + +// parentRoute allows routes to know about parent host and path definitions. +type parentRoute interface { + getNamedRoutes() map[string]*Route + getRegexpGroup() *routeRegexpGroup +} + +// getNamedRoutes returns the map where named routes are registered. +func (r *Route) getNamedRoutes() map[string]*Route { + if r.parent == nil { + // During tests router is not always set. + r.parent = NewRouter() + } + return r.parent.getNamedRoutes() +} + +// getRegexpGroup returns regexp definitions from this route. +func (r *Route) getRegexpGroup() *routeRegexpGroup { + if r.regexp == nil { + if r.parent == nil { + // During tests router is not always set. + r.parent = NewRouter() + } + regexp := r.parent.getRegexpGroup() + if regexp == nil { + r.regexp = new(routeRegexpGroup) + } else { + // Copy. + r.regexp = &routeRegexpGroup{ + host: regexp.host, + path: regexp.path, + } + } + } + return r.regexp +} From bcb4f4bcdcd397e47609704f9b88c327db9eaa90 Mon Sep 17 00:00:00 2001 From: Kamil Kisiel Date: Thu, 4 Oct 2012 23:08:44 -0700 Subject: [PATCH 03/91] Converted TestRoute to a table-driven test --- mux_test.go | 664 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 438 insertions(+), 226 deletions(-) diff --git a/mux_test.go b/mux_test.go index 19e0daf..1277022 100644 --- a/mux_test.go +++ b/mux_test.go @@ -10,237 +10,449 @@ import ( "testing" ) -func TestRoute(t *testing.T) { - var route *Route - var request *http.Request - var vars map[string]string - var host, path, url string - - // Setup an id so we can see which test failed. :) - var idValue int - id := func() int { - idValue++ - return idValue +// helper function to create a new request with a method and url +func newRequest(method, url string) *http.Request { + req, err := http.NewRequest(method, url, nil) + if err != nil { + panic(err) } + return req +} - // Host ------------------------------------------------------------------- - - route = new(Route).Host("aaa.bbb.ccc") - request, _ = http.NewRequest("GET", "http://aaa.bbb.ccc/111/222/333", nil) - vars = map[string]string{} - host = "aaa.bbb.ccc" - path = "" - url = host + path - testRoute(t, id(), true, route, request, vars, host, path, url) - // Non-match for the same config. - request, _ = http.NewRequest("GET", "http://aaa.222.ccc/111/222/333", nil) - testRoute(t, id(), false, route, request, vars, host, path, url) - - route = new(Route).Host("aaa.{v1:[a-z]{3}}.ccc") - request, _ = http.NewRequest("GET", "http://aaa.bbb.ccc/111/222/333", nil) - vars = map[string]string{"v1": "bbb"} - host = "aaa.bbb.ccc" - path = "" - url = host + path - testRoute(t, id(), true, route, request, vars, host, path, url) - // Non-match for the same config. - request, _ = http.NewRequest("GET", "http://aaa.222.ccc/111/222/333", nil) - testRoute(t, id(), false, route, request, vars, host, path, url) - - route = new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}") - request, _ = http.NewRequest("GET", "http://aaa.bbb.ccc/111/222/333", nil) - vars = map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"} - host = "aaa.bbb.ccc" - path = "" - url = host + path - testRoute(t, id(), true, route, request, vars, host, path, url) - // Non-match for the same config. - request, _ = http.NewRequest("GET", "http://aaa.222.ccc/111/222/333", nil) - testRoute(t, id(), false, route, request, vars, host, path, url) - - // Path ------------------------------------------------------------------- - - route = new(Route).Path("/111/222/333") - request, _ = http.NewRequest("GET", "http://localhost/111/222/333", nil) - vars = map[string]string{} - host = "" - path = "/111/222/333" - url = host + path - testRoute(t, id(), true, route, request, vars, host, path, url) - // Non-match for the same config. - request, _ = http.NewRequest("GET", "http://localhost/1/2/3", nil) - testRoute(t, id(), false, route, request, vars, host, path, url) - - route = new(Route).Path("/111/{v1:[0-9]{3}}/333") - request, _ = http.NewRequest("GET", "http://localhost/111/222/333", nil) - vars = map[string]string{"v1": "222"} - host = "" - path = "/111/222/333" - url = host + path - testRoute(t, id(), true, route, request, vars, host, path, url) - // Non-match for the same config. - request, _ = http.NewRequest("GET", "http://localhost/111/aaa/333", nil) - testRoute(t, id(), false, route, request, vars, host, path, url) - - route = new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}") - request, _ = http.NewRequest("GET", "http://localhost/111/222/333", nil) - vars = map[string]string{"v1": "111", "v2": "222", "v3": "333"} - host = "" - path = "/111/222/333" - url = host + path - testRoute(t, id(), true, route, request, vars, host, path, url) - // Non-match for the same config. - request, _ = http.NewRequest("GET", "http://localhost/111/aaa/333", nil) - testRoute(t, id(), false, route, request, vars, host, path, url) - - // PathPrefix ------------------------------------------------------------- - - route = new(Route).PathPrefix("/111") - request, _ = http.NewRequest("GET", "http://localhost/111/222/333", nil) - vars = map[string]string{} - host = "" - path = "/111" - url = host + path - testRoute(t, id(), true, route, request, vars, host, path, url) - // Non-match for the same config. - request, _ = http.NewRequest("GET", "http://localhost/1/2/3", nil) - testRoute(t, id(), false, route, request, vars, host, path, url) - - route = new(Route).PathPrefix("/111/{v1:[0-9]{3}}") - request, _ = http.NewRequest("GET", "http://localhost/111/222/333", nil) - vars = map[string]string{"v1": "222"} - host = "" - path = "/111/222" - url = host + path - testRoute(t, id(), true, route, request, vars, host, path, url) - // Non-match for the same config. - request, _ = http.NewRequest("GET", "http://localhost/111/aaa/333", nil) - testRoute(t, id(), false, route, request, vars, host, path, url) - - route = new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}") - request, _ = http.NewRequest("GET", "http://localhost/111/222/333", nil) - vars = map[string]string{"v1": "111", "v2": "222"} - host = "" - path = "/111/222" - url = host + path - testRoute(t, id(), true, route, request, vars, host, path, url) - // Non-match for the same config. - request, _ = http.NewRequest("GET", "http://localhost/111/aaa/333", nil) - testRoute(t, id(), false, route, request, vars, host, path, url) - - // Host + Path ------------------------------------------------------------ - - route = new(Route).Host("aaa.bbb.ccc").Path("/111/222/333") - request, _ = http.NewRequest("GET", "http://aaa.bbb.ccc/111/222/333", nil) - vars = map[string]string{} - host = "" - path = "" - url = host + path - testRoute(t, id(), true, route, request, vars, host, path, url) - // Non-match for the same config. - request, _ = http.NewRequest("GET", "http://aaa.222.ccc/111/222/333", nil) - testRoute(t, id(), false, route, request, vars, host, path, url) - - route = new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333") - request, _ = http.NewRequest("GET", "http://aaa.bbb.ccc/111/222/333", nil) - vars = map[string]string{"v1": "bbb", "v2": "222"} - host = "aaa.bbb.ccc" - path = "/111/222/333" - url = host + path - testRoute(t, id(), true, route, request, vars, host, path, url) - // Non-match for the same config. - request, _ = http.NewRequest("GET", "http://aaa.222.ccc/111/222/333", nil) - testRoute(t, id(), false, route, request, vars, host, path, url) - - route = new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}") - request, _ = http.NewRequest("GET", "http://aaa.bbb.ccc/111/222/333", nil) - vars = map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"} - host = "aaa.bbb.ccc" - path = "/111/222/333" - url = host + path - testRoute(t, id(), true, route, request, vars, host, path, url) - // Non-match for the same config. - request, _ = http.NewRequest("GET", "http://aaa.222.ccc/111/222/333", nil) - testRoute(t, id(), false, route, request, vars, host, path, url) - - // Headers ---------------------------------------------------------------- - - route = new(Route).Headers("foo", "bar", "baz", "ding") - request, _ = http.NewRequest("GET", "http://localhost", nil) - request.Header.Add("foo", "bar") - request.Header.Add("baz", "ding") - vars = map[string]string{} - host = "" - path = "" - url = host + path - testRoute(t, id(), true, route, request, vars, host, path, url) - // Non-match for the same config. - request, _ = http.NewRequest("GET", "http://localhost", nil) - request.Header.Add("foo", "bar") - request.Header.Add("baz", "dong") - testRoute(t, id(), false, route, request, vars, host, path, url) - - // Methods ---------------------------------------------------------------- - - route = new(Route).Methods("GET", "POST") - request, _ = http.NewRequest("GET", "http://localhost", nil) - vars = map[string]string{} - host = "" - path = "" - url = host + path - testRoute(t, id(), true, route, request, vars, host, path, url) - request, _ = http.NewRequest("POST", "http://localhost", nil) - testRoute(t, id(), true, route, request, vars, host, path, url) - // Non-match for the same config. - request, _ = http.NewRequest("PUT", "http://localhost", nil) - testRoute(t, id(), false, route, request, vars, host, path, url) - - // Queries ---------------------------------------------------------------- - - route = new(Route).Queries("foo", "bar", "baz", "ding") - request, _ = http.NewRequest("GET", "http://localhost?foo=bar&baz=ding", nil) - vars = map[string]string{} - host = "" - path = "" - url = host + path - testRoute(t, id(), true, route, request, vars, host, path, url) - // Non-match for the same config. - request, _ = http.NewRequest("GET", "http://localhost?foo=bar&baz=dong", nil) - testRoute(t, id(), false, route, request, vars, host, path, url) - - // Schemes ---------------------------------------------------------------- - - route = new(Route).Schemes("https", "ftp") - request, _ = http.NewRequest("GET", "https://localhost", nil) - vars = map[string]string{} - host = "" - path = "" - url = host + path - testRoute(t, id(), true, route, request, vars, host, path, url) - request, _ = http.NewRequest("GET", "ftp://localhost", nil) - testRoute(t, id(), true, route, request, vars, host, path, url) - // Non-match for the same config. - request, _ = http.NewRequest("GET", "http://localhost", nil) - testRoute(t, id(), false, route, request, vars, host, path, url) +// helper function to create a new request with a method, url, and host header +func newRequestHost(method, url, host string) *http.Request { + req, err := http.NewRequest(method, url, nil) + if err != nil { + panic(err) + } + req.Host = host + return req +} - // Custom ----------------------------------------------------------------- +// helper function to create a new request with a method, url, and headers +func newRequestHeaders(method, url string, headers map[string]string) *http.Request { + req, err := http.NewRequest(method, url, nil) + if err != nil { + panic(err) + } + for k, v := range headers { + req.Header.Add(k, v) + } + return req +} +// Tests for Route +func TestRoute(t *testing.T) { + // match function for Custom tests m := func(r *http.Request, m *RouteMatch) bool { if r.URL.Host == "aaa.bbb.ccc" { return true } return false } - route = new(Route).MatcherFunc(m) - request, _ = http.NewRequest("GET", "http://aaa.bbb.ccc", nil) - vars = map[string]string{} - host = "" - path = "" - url = host + path - testRoute(t, id(), true, route, request, vars, host, path, url) - // Non-match for the same config. - request, _ = http.NewRequest("GET", "http://aaa.ccc.bbb", nil) - testRoute(t, id(), false, route, request, vars, host, path, url) + + // the tests + tests := []struct { + title string // title of the test + route *Route // the route being tested + request *http.Request // a request to test the route + vars map[string]string // the expected vars of the match + host string // the expected host of the match + path string // the expected path of the match + match bool // whether the request is expected to match the route at all + }{ + // Host + { + title: "Host route match", + route: new(Route).Host("aaa.bbb.ccc"), + request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), + vars: map[string]string{}, + host: "aaa.bbb.ccc", + path: "", + match: true, + }, + { + title: "Host route, wrong host in request URL", + route: new(Route).Host("aaa.bbb.ccc"), + request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), + vars: map[string]string{}, + host: "aaa.bbb.ccc", + path: "", + match: false, + }, + { + title: "Host route with port, match", + route: new(Route).Host("aaa.bbb.ccc:1234"), + request: newRequest("GET", "http://aaa.bbb.ccc:1234/111/222/333"), + vars: map[string]string{}, + host: "aaa.bbb.ccc:1234", + path: "", + match: true, + }, + { + title: "Host route with port, wrong port in request URL", + route: new(Route).Host("aaa.bbb.ccc:1234"), + request: newRequest("GET", "http://aaa.bbb.ccc:9999/111/222/333"), + vars: map[string]string{}, + host: "aaa.bbb.ccc:1234", + path: "", + match: false, + }, + { + title: "Host route, match with host in request header", + route: new(Route).Host("aaa.bbb.ccc"), + request: newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc"), + vars: map[string]string{}, + host: "aaa.bbb.ccc", + path: "", + match: true, + }, + { + title: "Host route, wrong host in request header", + route: new(Route).Host("aaa.bbb.ccc"), + request: newRequestHost("GET", "/111/222/333", "aaa.222.ccc"), + vars: map[string]string{}, + host: "aaa.bbb.ccc", + path: "", + match: false, + }, + // BUG {new(Route).Host("aaa.bbb.ccc:1234"), newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc:1234"), map[string]string{}, "aaa.bbb.ccc:1234", "", true}, + { + title: "Host route with port, wrong host in request header", + route: new(Route).Host("aaa.bbb.ccc:1234"), + request: newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc:9999"), + vars: map[string]string{}, + host: "aaa.bbb.ccc:1234", + path: "", + match: false, + }, + { + title: "Host route with pattern, match", + route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"), + request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), + vars: map[string]string{"v1": "bbb"}, + host: "aaa.bbb.ccc", + path: "", + match: true, + }, + { + title: "Host route with pattern, wrong host in request URL", + route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"), + request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), + vars: map[string]string{"v1": "bbb"}, + host: "aaa.bbb.ccc", + path: "", + match: false, + }, + { + title: "Host route with multiple patterns, match", + route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}"), + request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), + vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"}, + host: "aaa.bbb.ccc", + path: "", + match: true, + }, + { + title: "Host route with multiple patterns, wrong host in request URL", + route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}"), + request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), + vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"}, + host: "aaa.bbb.ccc", + path: "", + match: false, + }, + + // Path + { + title: "Path route, match", + route: new(Route).Path("/111/222/333"), + request: newRequest("GET", "http://localhost/111/222/333"), + vars: map[string]string{}, + host: "", + path: "/111/222/333", + match: true, + }, + { + title: "Path route, wrong path in request in request URL", + route: new(Route).Path("/111/222/333"), + request: newRequest("GET", "http://localhost/1/2/3"), + vars: map[string]string{}, + host: "", + path: "/111/222/333", + match: false, + }, + { + title: "Path route with pattern, match", + route: new(Route).Path("/111/{v1:[0-9]{3}}/333"), + request: newRequest("GET", "http://localhost/111/222/333"), + vars: map[string]string{"v1": "222"}, + host: "", + path: "/111/222/333", + match: true, + }, + { + title: "Path route with pattern, URL in request does not match", + route: new(Route).Path("/111/{v1:[0-9]{3}}/333"), + request: newRequest("GET", "http://localhost/111/aaa/333"), + vars: map[string]string{"v1": "222"}, + host: "", + path: "/111/222/333", + match: false, + }, + { + title: "Path route with multiple patterns, match", + route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"), + request: newRequest("GET", "http://localhost/111/222/333"), + vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"}, + host: "", + path: "/111/222/333", + match: true, + }, + { + title: "Path route with multiple patterns, URL in request does not match", + route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"), + request: newRequest("GET", "http://localhost/111/aaa/333"), + vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"}, + host: "", + path: "/111/222/333", + match: false, + }, + + // PathPrefix + { + title: "PathPrefix route, match", + route: new(Route).PathPrefix("/111"), + request: newRequest("GET", "http://localhost/111/222/333"), + vars: map[string]string{}, + host: "", + path: "/111", + match: true, + }, + { + title: "PathPrefix route, URL prefix in request does not match", + route: new(Route).PathPrefix("/111"), + request: newRequest("GET", "http://localhost/1/2/3"), + vars: map[string]string{}, + host: "", + path: "/111", + match: false, + }, + { + title: "PathPrefix route with pattern, match", + route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"), + request: newRequest("GET", "http://localhost/111/222/333"), + vars: map[string]string{"v1": "222"}, + host: "", + path: "/111/222", + match: true, + }, + { + title: "PathPrefix route with pattern, URL prefix in request does not match", + route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"), + request: newRequest("GET", "http://localhost/111/aaa/333"), + vars: map[string]string{"v1": "222"}, + host: "", + path: "/111/222", + match: false, + }, + { + title: "PathPrefix route with multiple patterns, match", + route: new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}"), + request: newRequest("GET", "http://localhost/111/222/333"), + vars: map[string]string{"v1": "111", "v2": "222"}, + host: "", + path: "/111/222", + match: true, + }, + { + title: "PathPrefix route with multiple patterns, URL prefix in request does not match", + route: new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}"), + request: newRequest("GET", "http://localhost/111/aaa/333"), + vars: map[string]string{"v1": "111", "v2": "222"}, + host: "", + path: "/111/222", + match: false, + }, + + // Host + Path + { + title: "Host and Path route, match", + route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"), + request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), + vars: map[string]string{}, + host: "", + path: "", + match: true, + }, + { + title: "Host and Path route, wrong host in request URL", + route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"), + request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), + vars: map[string]string{}, + host: "", + path: "", + match: false, + }, + { + title: "Host and Path route with pattern, match", + route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), + request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), + vars: map[string]string{"v1": "bbb", "v2": "222"}, + host: "aaa.bbb.ccc", + path: "/111/222/333", + match: true, + }, + { + title: "Host and Path route with pattern, URL in request does not match", + route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), + request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), + vars: map[string]string{"v1": "bbb", "v2": "222"}, + host: "aaa.bbb.ccc", + path: "/111/222/333", + match: false, + }, + { + title: "Host and Path route with multiple patterns, match", + route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}"), + request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), + vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"}, + host: "aaa.bbb.ccc", + path: "/111/222/333", + match: true, + }, + { + title: "Host and Path route with multiple patterns, URL in request does not match", + route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}"), + request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), + vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"}, + host: "aaa.bbb.ccc", + path: "/111/222/333", + match: false, + }, + + // Headers + { + title: "Headers route, match", + route: new(Route).Headers("foo", "bar", "baz", "ding"), + request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "bar", "baz": "ding"}), + vars: map[string]string{}, + host: "", + path: "", + match: true, + }, + { + title: "Headers route, bad header values", + route: new(Route).Headers("foo", "bar", "baz", "ding"), + request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "bar", "baz": "dong"}), + vars: map[string]string{}, + host: "", + path: "", + match: false, + }, + + // Methods + { + title: "Methods route, match GET", + route: new(Route).Methods("GET", "POST"), + request: newRequest("GET", "http://localhost"), + vars: map[string]string{}, + host: "", + path: "", + match: true, + }, + { + title: "Methods route, match POST", + route: new(Route).Methods("GET", "POST"), + request: newRequest("POST", "http://localhost"), + vars: map[string]string{}, + host: "", + path: "", + match: true, + }, + { + title: "Methods route, bad method", + route: new(Route).Methods("GET", "POST"), + request: newRequest("PUT", "http://localhost"), + vars: map[string]string{}, + host: "", + path: "", + match: false, + }, + + // Queries + { + title: "Queries route, match", + route: new(Route).Queries("foo", "bar", "baz", "ding"), + request: newRequest("GET", "http://localhost?foo=bar&baz=ding"), + vars: map[string]string{}, + host: "", + path: "", + match: true, + }, + { + title: "Queries route, bad query", + route: new(Route).Queries("foo", "bar", "baz", "ding"), + request: newRequest("GET", "http://localhost?foo=bar&baz=dong"), + vars: map[string]string{}, + host: "", + path: "", + match: false, + }, + + // Schemes + { + title: "Schemes route, match https", + route: new(Route).Schemes("https", "ftp"), + request: newRequest("GET", "https://localhost"), + vars: map[string]string{}, + host: "", + path: "", + match: true, + }, + { + title: "Schemes route, match ftp", + route: new(Route).Schemes("https", "ftp"), + request: newRequest("GET", "ftp://localhost"), + vars: map[string]string{}, + host: "", + path: "", + match: true, + }, + { + title: "Schemes route, bad scheme", + route: new(Route).Schemes("https", "ftp"), + request: newRequest("GET", "http://localhost"), + vars: map[string]string{}, + host: "", + path: "", + match: false, + }, + + // Custom + { + title: "MatchFunc route, match", + route: new(Route).MatcherFunc(m), + request: newRequest("GET", "http://aaa.bbb.ccc"), + vars: map[string]string{}, + host: "", + path: "", + match: true, + }, + { + title: "MatchFunc route, non-match", + route: new(Route).MatcherFunc(m), + request: newRequest("GET", "http://aaa.222.ccc"), + vars: map[string]string{}, + host: "", + path: "", + match: false, + }, + } + + for i, test := range tests { + testRoute(t, fmt.Sprintf("%v: %s", i, test.title), test.match, test.route, test.request, test.vars, test.host, test.path, test.host+test.path) + } } func TestSubRouter(t *testing.T) { @@ -266,10 +478,10 @@ func TestSubRouter(t *testing.T) { host = "aaa.google.com" path = "/bbb" url = host + path - testRoute(t, id(), true, route, request, vars, host, path, url) + testRoute(t, string(id()), true, route, request, vars, host, path, url) // Non-match for the same config. request, _ = http.NewRequest("GET", "http://111.google.com/111", nil) - testRoute(t, id(), false, route, request, vars, host, path, url) + testRoute(t, string(id()), false, route, request, vars, host, path, url) // ------------------------------------------------------------------------ @@ -280,10 +492,10 @@ func TestSubRouter(t *testing.T) { host = "" path = "/foo/bar/baz/ding" url = host + path - testRoute(t, id(), true, route, request, vars, host, path, url) + testRoute(t, string(id()), true, route, request, vars, host, path, url) // Non-match for the same config. request, _ = http.NewRequest("GET", "http://localhost/foo/bar", nil) - testRoute(t, id(), false, route, request, vars, host, path, url) + testRoute(t, string(id()), false, route, request, vars, host, path, url) } func TestNamedRoutes(t *testing.T) { @@ -326,7 +538,7 @@ func getRouteTemplate(route *Route) string { return fmt.Sprintf("Host: %v, Path: %v", host, path) } -func testRoute(t *testing.T, id int, shouldMatch bool, route *Route, +func testRoute(t *testing.T, id string, shouldMatch bool, route *Route, request *http.Request, vars map[string]string, host, path, url string) { var match RouteMatch ok := route.Match(request, &match) From f89494aed121356fbc0ab4c1143cfdf7256477ae Mon Sep 17 00:00:00 2001 From: Kamil Kisiel Date: Sun, 14 Oct 2012 14:37:14 -0700 Subject: [PATCH 04/91] Finished converting rest of mux tests to table tests Separated the tests in to test functions depending on what they are testing. Moved some test-specific functiosn in to closures within the test functions themselves. --- mux_test.go | 1020 +++++++++++++++++++++++++++------------------------ 1 file changed, 542 insertions(+), 478 deletions(-) diff --git a/mux_test.go b/mux_test.go index 1277022..0ae175f 100644 --- a/mux_test.go +++ b/mux_test.go @@ -10,492 +10,538 @@ import ( "testing" ) -// helper function to create a new request with a method and url -func newRequest(method, url string) *http.Request { - req, err := http.NewRequest(method, url, nil) - if err != nil { - panic(err) +type routeTest struct { + title string // title of the test + route *Route // the route being tested + request *http.Request // a request to test the route + vars map[string]string // the expected vars of the match + host string // the expected host of the match + path string // the expected path of the match + shouldMatch bool // whether the request is expected to match the route at all +} + +func TestHost(t *testing.T) { + // newRequestHost a new request with a method, url, and host header + newRequestHost := func(method, url, host string) *http.Request { + req, err := http.NewRequest(method, url, nil) + if err != nil { + panic(err) + } + req.Host = host + return req + } + + tests := []routeTest{ + { + title: "Host route match", + route: new(Route).Host("aaa.bbb.ccc"), + request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), + vars: map[string]string{}, + host: "aaa.bbb.ccc", + path: "", + shouldMatch: true, + }, + { + title: "Host route, wrong host in request URL", + route: new(Route).Host("aaa.bbb.ccc"), + request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), + vars: map[string]string{}, + host: "aaa.bbb.ccc", + path: "", + shouldMatch: false, + }, + { + title: "Host route with port, match", + route: new(Route).Host("aaa.bbb.ccc:1234"), + request: newRequest("GET", "http://aaa.bbb.ccc:1234/111/222/333"), + vars: map[string]string{}, + host: "aaa.bbb.ccc:1234", + path: "", + shouldMatch: true, + }, + { + title: "Host route with port, wrong port in request URL", + route: new(Route).Host("aaa.bbb.ccc:1234"), + request: newRequest("GET", "http://aaa.bbb.ccc:9999/111/222/333"), + vars: map[string]string{}, + host: "aaa.bbb.ccc:1234", + path: "", + shouldMatch: false, + }, + { + title: "Host route, match with host in request header", + route: new(Route).Host("aaa.bbb.ccc"), + request: newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc"), + vars: map[string]string{}, + host: "aaa.bbb.ccc", + path: "", + shouldMatch: true, + }, + { + title: "Host route, wrong host in request header", + route: new(Route).Host("aaa.bbb.ccc"), + request: newRequestHost("GET", "/111/222/333", "aaa.222.ccc"), + vars: map[string]string{}, + host: "aaa.bbb.ccc", + path: "", + shouldMatch: false, + }, + // BUG {new(Route).Host("aaa.bbb.ccc:1234"), newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc:1234"), map[string]string{}, "aaa.bbb.ccc:1234", "", true}, + { + title: "Host route with port, wrong host in request header", + route: new(Route).Host("aaa.bbb.ccc:1234"), + request: newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc:9999"), + vars: map[string]string{}, + host: "aaa.bbb.ccc:1234", + path: "", + shouldMatch: false, + }, + { + title: "Host route with pattern, match", + route: new(Route).Host("aaa.{v1:[a-z]{3}}.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"), + request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), + vars: map[string]string{"v1": "bbb"}, + host: "aaa.bbb.ccc", + path: "", + shouldMatch: false, + }, + { + title: "Host route with multiple patterns, match", + route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}"), + request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), + vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"}, + host: "aaa.bbb.ccc", + path: "", + shouldMatch: true, + }, + { + title: "Host route with multiple patterns, wrong host in request URL", + route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}"), + request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), + vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"}, + host: "aaa.bbb.ccc", + path: "", + shouldMatch: false, + }, + } + for _, test := range tests { + testRoute(t, test) } - return req } -// helper function to create a new request with a method, url, and host header -func newRequestHost(method, url, host string) *http.Request { - req, err := http.NewRequest(method, url, nil) - if err != nil { - panic(err) +func TestPath(t *testing.T) { + tests := []routeTest{ + { + title: "Path route, match", + route: new(Route).Path("/111/222/333"), + request: newRequest("GET", "http://localhost/111/222/333"), + vars: map[string]string{}, + host: "", + path: "/111/222/333", + shouldMatch: true, + }, + { + title: "Path route, wrong path in request in request URL", + route: new(Route).Path("/111/222/333"), + request: newRequest("GET", "http://localhost/1/2/3"), + vars: map[string]string{}, + host: "", + path: "/111/222/333", + shouldMatch: false, + }, + { + title: "Path route with pattern, match", + route: new(Route).Path("/111/{v1:[0-9]{3}}/333"), + request: newRequest("GET", "http://localhost/111/222/333"), + vars: map[string]string{"v1": "222"}, + host: "", + path: "/111/222/333", + shouldMatch: true, + }, + { + title: "Path route with pattern, URL in request does not match", + route: new(Route).Path("/111/{v1:[0-9]{3}}/333"), + request: newRequest("GET", "http://localhost/111/aaa/333"), + vars: map[string]string{"v1": "222"}, + host: "", + path: "/111/222/333", + shouldMatch: false, + }, + { + title: "Path route with multiple patterns, match", + route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"), + request: newRequest("GET", "http://localhost/111/222/333"), + vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"}, + host: "", + path: "/111/222/333", + shouldMatch: true, + }, + { + title: "Path route with multiple patterns, URL in request does not match", + route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"), + request: newRequest("GET", "http://localhost/111/aaa/333"), + vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"}, + host: "", + path: "/111/222/333", + shouldMatch: false, + }, + } + + for _, test := range tests { + testRoute(t, test) } - req.Host = host - return req } -// helper function to create a new request with a method, url, and headers -func newRequestHeaders(method, url string, headers map[string]string) *http.Request { - req, err := http.NewRequest(method, url, nil) - if err != nil { - panic(err) +func TestPathPrefix(t *testing.T) { + tests := []routeTest{ + { + title: "PathPrefix route, match", + route: new(Route).PathPrefix("/111"), + request: newRequest("GET", "http://localhost/111/222/333"), + vars: map[string]string{}, + host: "", + path: "/111", + shouldMatch: true, + }, + { + title: "PathPrefix route, URL prefix in request does not match", + route: new(Route).PathPrefix("/111"), + request: newRequest("GET", "http://localhost/1/2/3"), + vars: map[string]string{}, + host: "", + path: "/111", + shouldMatch: false, + }, + { + title: "PathPrefix route with pattern, match", + route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"), + request: newRequest("GET", "http://localhost/111/222/333"), + vars: map[string]string{"v1": "222"}, + host: "", + path: "/111/222", + shouldMatch: true, + }, + { + title: "PathPrefix route with pattern, URL prefix in request does not match", + route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"), + request: newRequest("GET", "http://localhost/111/aaa/333"), + vars: map[string]string{"v1": "222"}, + host: "", + path: "/111/222", + shouldMatch: false, + }, + { + title: "PathPrefix route with multiple patterns, match", + route: new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}"), + request: newRequest("GET", "http://localhost/111/222/333"), + vars: map[string]string{"v1": "111", "v2": "222"}, + host: "", + path: "/111/222", + shouldMatch: true, + }, + { + title: "PathPrefix route with multiple patterns, URL prefix in request does not match", + route: new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}"), + request: newRequest("GET", "http://localhost/111/aaa/333"), + vars: map[string]string{"v1": "111", "v2": "222"}, + host: "", + path: "/111/222", + shouldMatch: false, + }, } - for k, v := range headers { - req.Header.Add(k, v) + + for _, test := range tests { + testRoute(t, test) } - return req } -// Tests for Route -func TestRoute(t *testing.T) { - // match function for Custom tests - m := func(r *http.Request, m *RouteMatch) bool { - if r.URL.Host == "aaa.bbb.ccc" { - return true +func TestHostPath(t *testing.T) { + tests := []routeTest{ + { + title: "Host and Path route, match", + route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"), + request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), + vars: map[string]string{}, + host: "", + path: "", + shouldMatch: true, + }, + { + title: "Host and Path route, wrong host in request URL", + route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"), + request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), + vars: map[string]string{}, + host: "", + path: "", + shouldMatch: false, + }, + { + title: "Host and Path route with pattern, match", + route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), + request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), + vars: map[string]string{"v1": "bbb", "v2": "222"}, + host: "aaa.bbb.ccc", + path: "/111/222/333", + shouldMatch: true, + }, + { + title: "Host and Path route with pattern, URL in request does not match", + route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), + request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), + vars: map[string]string{"v1": "bbb", "v2": "222"}, + host: "aaa.bbb.ccc", + path: "/111/222/333", + shouldMatch: false, + }, + { + title: "Host and Path route with multiple patterns, match", + route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}"), + request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), + vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"}, + host: "aaa.bbb.ccc", + path: "/111/222/333", + shouldMatch: true, + }, + { + title: "Host and Path route with multiple patterns, URL in request does not match", + route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}"), + request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), + vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"}, + host: "aaa.bbb.ccc", + path: "/111/222/333", + shouldMatch: false, + }, + } + + for _, test := range tests { + testRoute(t, test) + } +} + +func TestHeaders(t *testing.T) { + // newRequestHeaders creates a new request with a method, url, and headers + newRequestHeaders := func(method, url string, headers map[string]string) *http.Request { + req, err := http.NewRequest(method, url, nil) + if err != nil { + panic(err) } - return false + for k, v := range headers { + req.Header.Add(k, v) + } + return req } - // the tests - tests := []struct { - title string // title of the test - route *Route // the route being tested - request *http.Request // a request to test the route - vars map[string]string // the expected vars of the match - host string // the expected host of the match - path string // the expected path of the match - match bool // whether the request is expected to match the route at all - }{ - // Host - { - title: "Host route match", - route: new(Route).Host("aaa.bbb.ccc"), - request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), - vars: map[string]string{}, - host: "aaa.bbb.ccc", - path: "", - match: true, - }, - { - title: "Host route, wrong host in request URL", - route: new(Route).Host("aaa.bbb.ccc"), - request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), - vars: map[string]string{}, - host: "aaa.bbb.ccc", - path: "", - match: false, - }, - { - title: "Host route with port, match", - route: new(Route).Host("aaa.bbb.ccc:1234"), - request: newRequest("GET", "http://aaa.bbb.ccc:1234/111/222/333"), - vars: map[string]string{}, - host: "aaa.bbb.ccc:1234", - path: "", - match: true, - }, - { - title: "Host route with port, wrong port in request URL", - route: new(Route).Host("aaa.bbb.ccc:1234"), - request: newRequest("GET", "http://aaa.bbb.ccc:9999/111/222/333"), - vars: map[string]string{}, - host: "aaa.bbb.ccc:1234", - path: "", - match: false, - }, - { - title: "Host route, match with host in request header", - route: new(Route).Host("aaa.bbb.ccc"), - request: newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc"), - vars: map[string]string{}, - host: "aaa.bbb.ccc", - path: "", - match: true, - }, - { - title: "Host route, wrong host in request header", - route: new(Route).Host("aaa.bbb.ccc"), - request: newRequestHost("GET", "/111/222/333", "aaa.222.ccc"), - vars: map[string]string{}, - host: "aaa.bbb.ccc", - path: "", - match: false, + tests := []routeTest{ + { + title: "Headers route, match", + route: new(Route).Headers("foo", "bar", "baz", "ding"), + request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "bar", "baz": "ding"}), + vars: map[string]string{}, + host: "", + path: "", + shouldMatch: true, }, - // BUG {new(Route).Host("aaa.bbb.ccc:1234"), newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc:1234"), map[string]string{}, "aaa.bbb.ccc:1234", "", true}, { - title: "Host route with port, wrong host in request header", - route: new(Route).Host("aaa.bbb.ccc:1234"), - request: newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc:9999"), - vars: map[string]string{}, - host: "aaa.bbb.ccc:1234", - path: "", - match: false, - }, - { - title: "Host route with pattern, match", - route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"), - request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), - vars: map[string]string{"v1": "bbb"}, - host: "aaa.bbb.ccc", - path: "", - match: true, - }, - { - title: "Host route with pattern, wrong host in request URL", - route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"), - request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), - vars: map[string]string{"v1": "bbb"}, - host: "aaa.bbb.ccc", - path: "", - match: false, - }, - { - title: "Host route with multiple patterns, match", - route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}"), - request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), - vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"}, - host: "aaa.bbb.ccc", - path: "", - match: true, - }, - { - title: "Host route with multiple patterns, wrong host in request URL", - route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}"), - request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), - vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"}, - host: "aaa.bbb.ccc", - path: "", - match: false, - }, - - // Path - { - title: "Path route, match", - route: new(Route).Path("/111/222/333"), - request: newRequest("GET", "http://localhost/111/222/333"), - vars: map[string]string{}, - host: "", - path: "/111/222/333", - match: true, - }, - { - title: "Path route, wrong path in request in request URL", - route: new(Route).Path("/111/222/333"), - request: newRequest("GET", "http://localhost/1/2/3"), - vars: map[string]string{}, - host: "", - path: "/111/222/333", - match: false, - }, - { - title: "Path route with pattern, match", - route: new(Route).Path("/111/{v1:[0-9]{3}}/333"), - request: newRequest("GET", "http://localhost/111/222/333"), - vars: map[string]string{"v1": "222"}, - host: "", - path: "/111/222/333", - match: true, - }, - { - title: "Path route with pattern, URL in request does not match", - route: new(Route).Path("/111/{v1:[0-9]{3}}/333"), - request: newRequest("GET", "http://localhost/111/aaa/333"), - vars: map[string]string{"v1": "222"}, - host: "", - path: "/111/222/333", - match: false, - }, - { - title: "Path route with multiple patterns, match", - route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"), - request: newRequest("GET", "http://localhost/111/222/333"), - vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"}, - host: "", - path: "/111/222/333", - match: true, - }, - { - title: "Path route with multiple patterns, URL in request does not match", - route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"), - request: newRequest("GET", "http://localhost/111/aaa/333"), - vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"}, - host: "", - path: "/111/222/333", - match: false, - }, - - // PathPrefix - { - title: "PathPrefix route, match", - route: new(Route).PathPrefix("/111"), - request: newRequest("GET", "http://localhost/111/222/333"), - vars: map[string]string{}, - host: "", - path: "/111", - match: true, - }, - { - title: "PathPrefix route, URL prefix in request does not match", - route: new(Route).PathPrefix("/111"), - request: newRequest("GET", "http://localhost/1/2/3"), - vars: map[string]string{}, - host: "", - path: "/111", - match: false, - }, - { - title: "PathPrefix route with pattern, match", - route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"), - request: newRequest("GET", "http://localhost/111/222/333"), - vars: map[string]string{"v1": "222"}, - host: "", - path: "/111/222", - match: true, - }, - { - title: "PathPrefix route with pattern, URL prefix in request does not match", - route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"), - request: newRequest("GET", "http://localhost/111/aaa/333"), - vars: map[string]string{"v1": "222"}, - host: "", - path: "/111/222", - match: false, - }, - { - title: "PathPrefix route with multiple patterns, match", - route: new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}"), - request: newRequest("GET", "http://localhost/111/222/333"), - vars: map[string]string{"v1": "111", "v2": "222"}, - host: "", - path: "/111/222", - match: true, - }, - { - title: "PathPrefix route with multiple patterns, URL prefix in request does not match", - route: new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}"), - request: newRequest("GET", "http://localhost/111/aaa/333"), - vars: map[string]string{"v1": "111", "v2": "222"}, - host: "", - path: "/111/222", - match: false, - }, - - // Host + Path - { - title: "Host and Path route, match", - route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"), - request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), - vars: map[string]string{}, - host: "", - path: "", - match: true, - }, - { - title: "Host and Path route, wrong host in request URL", - route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"), - request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), - vars: map[string]string{}, - host: "", - path: "", - match: false, - }, - { - title: "Host and Path route with pattern, match", - route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), - request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), - vars: map[string]string{"v1": "bbb", "v2": "222"}, - host: "aaa.bbb.ccc", - path: "/111/222/333", - match: true, - }, - { - title: "Host and Path route with pattern, URL in request does not match", - route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), - request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), - vars: map[string]string{"v1": "bbb", "v2": "222"}, - host: "aaa.bbb.ccc", - path: "/111/222/333", - match: false, - }, - { - title: "Host and Path route with multiple patterns, match", - route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}"), - request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), - vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"}, - host: "aaa.bbb.ccc", - path: "/111/222/333", - match: true, - }, - { - title: "Host and Path route with multiple patterns, URL in request does not match", - route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}"), - request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), - vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"}, - host: "aaa.bbb.ccc", - path: "/111/222/333", - match: false, - }, - - // Headers - { - title: "Headers route, match", - route: new(Route).Headers("foo", "bar", "baz", "ding"), - request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "bar", "baz": "ding"}), - vars: map[string]string{}, - host: "", - path: "", - match: true, - }, - { - title: "Headers route, bad header values", - route: new(Route).Headers("foo", "bar", "baz", "ding"), - request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "bar", "baz": "dong"}), - vars: map[string]string{}, - host: "", - path: "", - match: false, - }, - - // Methods - { - title: "Methods route, match GET", - route: new(Route).Methods("GET", "POST"), - request: newRequest("GET", "http://localhost"), - vars: map[string]string{}, - host: "", - path: "", - match: true, - }, + title: "Headers route, bad header values", + route: new(Route).Headers("foo", "bar", "baz", "ding"), + request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "bar", "baz": "dong"}), + vars: map[string]string{}, + host: "", + path: "", + shouldMatch: false, + }, + } + + for _, test := range tests { + testRoute(t, test) + } + +} + +func TestMethods(t *testing.T) { + tests := []routeTest{ { - title: "Methods route, match POST", - route: new(Route).Methods("GET", "POST"), - request: newRequest("POST", "http://localhost"), - vars: map[string]string{}, - host: "", - path: "", - match: true, + title: "Methods route, match GET", + route: new(Route).Methods("GET", "POST"), + request: newRequest("GET", "http://localhost"), + vars: map[string]string{}, + host: "", + path: "", + shouldMatch: true, }, { - title: "Methods route, bad method", - route: new(Route).Methods("GET", "POST"), - request: newRequest("PUT", "http://localhost"), - vars: map[string]string{}, - host: "", - path: "", - match: false, - }, - - // Queries + title: "Methods route, match POST", + route: new(Route).Methods("GET", "POST"), + request: newRequest("POST", "http://localhost"), + vars: map[string]string{}, + host: "", + path: "", + shouldMatch: true, + }, { - title: "Queries route, match", - route: new(Route).Queries("foo", "bar", "baz", "ding"), - request: newRequest("GET", "http://localhost?foo=bar&baz=ding"), - vars: map[string]string{}, - host: "", - path: "", - match: true, + title: "Methods route, bad method", + route: new(Route).Methods("GET", "POST"), + request: newRequest("PUT", "http://localhost"), + vars: map[string]string{}, + host: "", + path: "", + shouldMatch: false, + }, + } + + for _, test := range tests { + testRoute(t, test) + } +} + +func TestQueries(t *testing.T) { + tests := []routeTest{ + { + title: "Queries route, match", + route: new(Route).Queries("foo", "bar", "baz", "ding"), + request: newRequest("GET", "http://localhost?foo=bar&baz=ding"), + vars: map[string]string{}, + host: "", + path: "", + shouldMatch: true, }, { - title: "Queries route, bad query", - route: new(Route).Queries("foo", "bar", "baz", "ding"), - request: newRequest("GET", "http://localhost?foo=bar&baz=dong"), - vars: map[string]string{}, - host: "", - path: "", - match: false, + title: "Queries route, bad query", + route: new(Route).Queries("foo", "bar", "baz", "ding"), + request: newRequest("GET", "http://localhost?foo=bar&baz=dong"), + vars: map[string]string{}, + host: "", + path: "", + shouldMatch: false, }, + } + for _, test := range tests { + testRoute(t, test) + } +} + +func TestSchemes(t *testing.T) { + tests := []routeTest{ // Schemes { - title: "Schemes route, match https", - route: new(Route).Schemes("https", "ftp"), - request: newRequest("GET", "https://localhost"), - vars: map[string]string{}, - host: "", - path: "", - match: true, + title: "Schemes route, match https", + route: new(Route).Schemes("https", "ftp"), + request: newRequest("GET", "https://localhost"), + vars: map[string]string{}, + host: "", + path: "", + shouldMatch: true, }, { - title: "Schemes route, match ftp", - route: new(Route).Schemes("https", "ftp"), - request: newRequest("GET", "ftp://localhost"), - vars: map[string]string{}, - host: "", - path: "", - match: true, + title: "Schemes route, match ftp", + route: new(Route).Schemes("https", "ftp"), + request: newRequest("GET", "ftp://localhost"), + vars: map[string]string{}, + host: "", + path: "", + shouldMatch: true, }, { - title: "Schemes route, bad scheme", - route: new(Route).Schemes("https", "ftp"), - request: newRequest("GET", "http://localhost"), - vars: map[string]string{}, - host: "", - path: "", - match: false, + title: "Schemes route, bad scheme", + route: new(Route).Schemes("https", "ftp"), + request: newRequest("GET", "http://localhost"), + vars: map[string]string{}, + host: "", + path: "", + shouldMatch: false, }, + } + for _, test := range tests { + testRoute(t, test) + } +} + +func TestMatcherFunc(t *testing.T) { + m := func(r *http.Request, m *RouteMatch) bool { + if r.URL.Host == "aaa.bbb.ccc" { + return true + } + return false + } - // Custom + tests := []routeTest{ { - title: "MatchFunc route, match", - route: new(Route).MatcherFunc(m), - request: newRequest("GET", "http://aaa.bbb.ccc"), - vars: map[string]string{}, - host: "", - path: "", - match: true, + title: "MatchFunc route, match", + route: new(Route).MatcherFunc(m), + request: newRequest("GET", "http://aaa.bbb.ccc"), + vars: map[string]string{}, + host: "", + path: "", + shouldMatch: true, }, { - title: "MatchFunc route, non-match", - route: new(Route).MatcherFunc(m), - request: newRequest("GET", "http://aaa.222.ccc"), - vars: map[string]string{}, - host: "", - path: "", - match: false, + title: "MatchFunc route, non-match", + route: new(Route).MatcherFunc(m), + request: newRequest("GET", "http://aaa.222.ccc"), + vars: map[string]string{}, + host: "", + path: "", + shouldMatch: false, }, } - for i, test := range tests { - testRoute(t, fmt.Sprintf("%v: %s", i, test.title), test.match, test.route, test.request, test.vars, test.host, test.path, test.host+test.path) + for _, test := range tests { + testRoute(t, test) } } func TestSubRouter(t *testing.T) { - var route *Route - var request *http.Request - var vars map[string]string - var host, path, url string - - subrouter := new(Route).Host("{v1:[a-z]+}.google.com").Subrouter() - - // Setup an id so we can see which test failed. :) - var idValue int - id := func() int { - idValue++ - return idValue - } - - // ------------------------------------------------------------------------ - - route = subrouter.Path("/{v2:[a-z]+}") - request, _ = http.NewRequest("GET", "http://aaa.google.com/bbb", nil) - vars = map[string]string{"v1": "aaa", "v2": "bbb"} - host = "aaa.google.com" - path = "/bbb" - url = host + path - testRoute(t, string(id()), true, route, request, vars, host, path, url) - // Non-match for the same config. - request, _ = http.NewRequest("GET", "http://111.google.com/111", nil) - testRoute(t, string(id()), false, route, request, vars, host, path, url) - - // ------------------------------------------------------------------------ - - subrouter = new(Route).PathPrefix("/foo/{v1}").Subrouter() - route = subrouter.Path("/baz/{v2}") - request, _ = http.NewRequest("GET", "http://localhost/foo/bar/baz/ding", nil) - vars = map[string]string{"v1": "bar", "v2": "ding"} - host = "" - path = "/foo/bar/baz/ding" - url = host + path - testRoute(t, string(id()), true, route, request, vars, host, path, url) - // Non-match for the same config. - request, _ = http.NewRequest("GET", "http://localhost/foo/bar", nil) - testRoute(t, string(id()), false, route, request, vars, host, path, url) + subrouter1 := new(Route).Host("{v1:[a-z]+}.google.com").Subrouter() + subrouter2 := new(Route).PathPrefix("/foo/{v1}").Subrouter() + + tests := []routeTest{ + { + route: subrouter1.Path("/{v2:[a-z]+}"), + request: newRequest("GET", "http://aaa.google.com/bbb"), + vars: map[string]string{"v1": "aaa", "v2": "bbb"}, + host: "aaa.google.com", + path: "/bbb", + shouldMatch: true, + }, + { + route: subrouter1.Path("/{v2:[a-z]+}"), + request: newRequest("GET", "http://111.google.com/111"), + vars: map[string]string{"v1": "aaa", "v2": "bbb"}, + host: "aaa.google.com", + path: "/bbb", + shouldMatch: false, + }, + { + route: subrouter2.Path("/baz/{v2}"), + request: newRequest("GET", "http://localhost/foo/bar/baz/ding"), + vars: map[string]string{"v1": "bar", "v2": "ding"}, + host: "", + path: "/foo/bar/baz/ding", + shouldMatch: true, + }, + { + route: subrouter2.Path("/baz/{v2}"), + request: newRequest("GET", "http://localhost/foo/bar"), + vars: map[string]string{"v1": "bar", "v2": "ding"}, + host: "", + path: "/foo/bar/baz/ding", + shouldMatch: false, + }, + } + + for _, test := range tests { + testRoute(t, test) + } } func TestNamedRoutes(t *testing.T) { @@ -521,6 +567,30 @@ func TestNamedRoutes(t *testing.T) { } } +func TestStrictSlash(t *testing.T) { + var r *Router + var req *http.Request + var route *Route + var match *RouteMatch + var matched bool + + // StrictSlash should be ignored for path prefix. + // So we register a route ending in slash but it doesn't attempt to add + // the slash for a path not ending in slash. + r = NewRouter() + r.StrictSlash(true) + route = r.NewRoute().PathPrefix("/static/") + req, _ = http.NewRequest("GET", "http://localhost/static/logo.png", nil) + match = new(RouteMatch) + matched = r.Match(req, match) + if !matched { + t.Errorf("Should match request %q -- %v", req.URL.Path, getRouteTemplate(route)) + } + if match.Handler != nil { + t.Errorf("Should not redirect") + } +} + // ---------------------------------------------------------------------------- // Helpers // ---------------------------------------------------------------------------- @@ -538,8 +608,15 @@ func getRouteTemplate(route *Route) string { return fmt.Sprintf("Host: %v, Path: %v", host, path) } -func testRoute(t *testing.T, id string, shouldMatch bool, route *Route, - request *http.Request, vars map[string]string, host, path, url string) { +func testRoute(t *testing.T, test routeTest) { + request := test.request + route := test.route + vars := test.vars + shouldMatch := test.shouldMatch + host := test.host + path := test.path + url := test.host + test.path + var match RouteMatch ok := route.Match(request, &match) if ok != shouldMatch { @@ -547,62 +624,39 @@ func testRoute(t *testing.T, id string, shouldMatch bool, route *Route, if !shouldMatch { msg = "Should not match" } - t.Errorf("(%v) %v:\nRoute: %#v\nRequest: %#v\nVars: %v\n", id, msg, route, request, vars) + t.Errorf("(%v) %v:\nRoute: %#v\nRequest: %#v\nVars: %v\n", test.title, msg, route, request, vars) return } if shouldMatch { - if vars != nil && !stringMapEqual(vars, match.Vars) { - t.Errorf("(%v) Vars not equal: expected %v, got %v", id, vars, match.Vars) + if test.vars != nil && !stringMapEqual(test.vars, match.Vars) { + t.Errorf("(%v) Vars not equal: expected %v, got %v", test.title, vars, match.Vars) return } if host != "" { - u, _ := route.URLHost(mapToPairs(match.Vars)...) + u, _ := test.route.URLHost(mapToPairs(match.Vars)...) if host != u.Host { - t.Errorf("(%v) URLHost not equal: expected %v, got %v -- %v", id, host, u.Host, getRouteTemplate(route)) + t.Errorf("(%v) URLHost not equal: expected %v, got %v -- %v", test.title, host, u.Host, getRouteTemplate(route)) return } } if path != "" { u, _ := route.URLPath(mapToPairs(match.Vars)...) if path != u.Path { - t.Errorf("(%v) URLPath not equal: expected %v, got %v -- %v", id, path, u.Path, getRouteTemplate(route)) + t.Errorf("(%v) URLPath not equal: expected %v, got %v -- %v", test.title, path, u.Path, getRouteTemplate(route)) return } } if url != "" { u, _ := route.URL(mapToPairs(match.Vars)...) if url != u.Host+u.Path { - t.Errorf("(%v) URL not equal: expected %v, got %v -- %v", id, url, u.Host+u.Path, getRouteTemplate(route)) + t.Errorf("(%v) URL not equal: expected %v, got %v -- %v", test.title, url, u.Host+u.Path, getRouteTemplate(route)) return } } } } -func TestStrictSlash(t *testing.T) { - var r *Router - var req *http.Request - var route *Route - var match *RouteMatch - var matched bool - - // StrictSlash should be ignored for path prefix. - // So we register a route ending in slash but it doesn't attempt to add - // the slash for a path not ending in slash. - r = NewRouter() - r.StrictSlash(true) - route = r.NewRoute().PathPrefix("/static/") - req, _ = http.NewRequest("GET", "http://localhost/static/logo.png", nil) - match = new(RouteMatch) - matched = r.Match(req, match) - if !matched { - t.Errorf("Should match request %q -- %v", req.URL.Path, getRouteTemplate(route)) - } - if match.Handler != nil { - t.Errorf("Should not redirect") - } -} - +// mapToPairs converts a string map to a slice of string pairs func mapToPairs(m map[string]string) []string { var i int p := make([]string, len(m)*2) @@ -614,6 +668,7 @@ func mapToPairs(m map[string]string) []string { return p } +// stringMapEqual checks the equality of two string maps func stringMapEqual(m1, m2 map[string]string) bool { nil1 := m1 == nil nil2 := m2 == nil @@ -627,3 +682,12 @@ func stringMapEqual(m1, m2 map[string]string) bool { } return true } + +// newRequest is a helper function to create a new request with a method and url +func newRequest(method, url string) *http.Request { + req, err := http.NewRequest(method, url, nil) + if err != nil { + panic(err) + } + return req +} From b20a198901ae3876ded3120faac0b5feec7fab76 Mon Sep 17 00:00:00 2001 From: moraes Date: Thu, 27 Dec 2012 06:12:28 -0200 Subject: [PATCH 05/91] Test for https://plus.google.com/101022900381697718949/posts/eWy6DjFJ6uW --- mux_test.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/mux_test.go b/mux_test.go index 0ae175f..5ff0fe1 100644 --- a/mux_test.go +++ b/mux_test.go @@ -656,6 +656,28 @@ func testRoute(t *testing.T, test routeTest) { } } +// https://plus.google.com/101022900381697718949/posts/eWy6DjFJ6uW +func testSubrouterHeader(t *testing.T, test routeTest) { + func1 := func(http.ResponseWriter, *http.Request) {} + func2 := func(http.ResponseWriter, *http.Request) {} + + r := NewRouter() + s := r.Headers("SomeSpecialHeader").Subrouter() + s.HandleFunc("/", func1).Name("func1") + r.HandleFunc("/", func2).Name("func2") + + req, _ := http.NewRequest("GET", "http://localhost/", nil) + req.Header.Add("SomeSpecialHeader", "") + match := new(RouteMatch) + matched := r.Match(req, match) + if !matched { + t.Errorf("Should match request") + } + if match.Route.GetName() != "func1" { + t.Errorf("Expecting func1 handler") + } +} + // mapToPairs converts a string map to a slice of string pairs func mapToPairs(m map[string]string) []string { var i int From a8ad2b0f1353d35106d769d0b98d55eeee970594 Mon Sep 17 00:00:00 2001 From: moraes Date: Thu, 27 Dec 2012 06:28:03 -0200 Subject: [PATCH 06/91] Now the real test for https://plus.google.com/101022900381697718949/posts/eWy6DjFJ6uW --- mux_test.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/mux_test.go b/mux_test.go index 5ff0fe1..55159bd 100644 --- a/mux_test.go +++ b/mux_test.go @@ -657,24 +657,32 @@ func testRoute(t *testing.T, test routeTest) { } // https://plus.google.com/101022900381697718949/posts/eWy6DjFJ6uW -func testSubrouterHeader(t *testing.T, test routeTest) { - func1 := func(http.ResponseWriter, *http.Request) {} +func TestSubrouterHeader(t *testing.T) { + expected := "func1 response" + func1 := func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, expected) + } func2 := func(http.ResponseWriter, *http.Request) {} r := NewRouter() - s := r.Headers("SomeSpecialHeader").Subrouter() + s := r.Headers("SomeSpecialHeader", "").Subrouter() s.HandleFunc("/", func1).Name("func1") r.HandleFunc("/", func2).Name("func2") req, _ := http.NewRequest("GET", "http://localhost/", nil) - req.Header.Add("SomeSpecialHeader", "") + req.Header.Add("SomeSpecialHeader", "foo") match := new(RouteMatch) matched := r.Match(req, match) if !matched { t.Errorf("Should match request") } if match.Route.GetName() != "func1" { - t.Errorf("Expecting func1 handler") + t.Errorf("Expecting func1 handler, got %s", match.Route.GetName()) + } + resp := NewRecorder() + match.Handler.ServeHTTP(resp, req) + if resp.Body.String() != expected { + t.Errorf("Expecting %q", expected) } } From 78f60492585e9f21942b155e870e019a401937ef Mon Sep 17 00:00:00 2001 From: Jongmin Kim Date: Thu, 17 Jan 2013 17:52:41 +0900 Subject: [PATCH 07/91] Update mux.go unnecessary comma --- mux.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mux.go b/mux.go index 62f00e8..27d7591 100644 --- a/mux.go +++ b/mux.go @@ -154,7 +154,7 @@ func (r *Router) Handle(path string, handler http.Handler) *Route { // HandleFunc registers a new route with a matcher for the URL path. // See Route.Path() and Route.HandlerFunc(). func (r *Router) HandleFunc(path string, f func(http.ResponseWriter, - *http.Request),) *Route { + *http.Request)) *Route { return r.NewRoute().Path(path).HandlerFunc(f) } From 38545dbae82fbb96b45986dcda1fcb87114904e2 Mon Sep 17 00:00:00 2001 From: Sergey Shepelev Date: Sun, 3 Feb 2013 06:48:19 +0400 Subject: [PATCH 08/91] Removed excess bool variable assignment in `if`s --- mux.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mux.go b/mux.go index 27d7591..3857173 100644 --- a/mux.go +++ b/mux.go @@ -51,7 +51,7 @@ type Router struct { // Match matches registered routes against the request. func (r *Router) Match(req *http.Request, match *RouteMatch) bool { for _, route := range r.routes { - if matched := route.Match(req, match); matched { + if route.Match(req, match) { return true } } @@ -71,7 +71,7 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { } var match RouteMatch var handler http.Handler - if matched := r.Match(req, &match); matched { + if r.Match(req, &match) { handler = match.Handler setVars(req, match.Vars) setCurrentRoute(req, match.Route) From bb851ab75f24e4a2ad8593bda4ce72a76164ec26 Mon Sep 17 00:00:00 2001 From: Aaron Yodaiken Date: Mon, 25 Mar 2013 16:52:38 -0500 Subject: [PATCH 09/91] Add example MatcherFunc --- doc.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc.go b/doc.go index 217e948..d6c5979 100644 --- a/doc.go +++ b/doc.go @@ -87,7 +87,9 @@ There are several other matchers that can be added. To match path prefixes: ...or to use a custom matcher function: - r.MatcherFunc(myFunc) + 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: From b9f42e6d55989960ba207c48c8796e1eb9300433 Mon Sep 17 00:00:00 2001 From: Kamil Kisiel Date: Mon, 25 Mar 2013 15:02:51 -0700 Subject: [PATCH 10/91] Tweak example indent --- doc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc.go b/doc.go index d6c5979..8ee5540 100644 --- a/doc.go +++ b/doc.go @@ -89,7 +89,7 @@ There are several other matchers that can be added. To match path prefixes: 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: From 9b36453141c35697401168b07f2c09fcff7721ce Mon Sep 17 00:00:00 2001 From: rodrigo moraes Date: Mon, 8 Apr 2013 09:01:04 -0300 Subject: [PATCH 11/91] Update README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ba77972..f6db41a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,6 @@ mux -=== \ No newline at end of file +=== + +gorilla/mux is a powerful URL router and dispatcher. + +Read the full documentation here: http://www.gorillatoolkit.org/pkg/mux From e718e932ee606838744df844eb75064959eb74bc Mon Sep 17 00:00:00 2001 From: John Nadratowski Date: Wed, 5 Jun 2013 19:07:27 -0400 Subject: [PATCH 12/91] Add KeepContext flag to Router to disable clearing of context data. --- mux.go | 8 ++++++-- mux_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/mux.go b/mux.go index 3857173..6136ab9 100644 --- a/mux.go +++ b/mux.go @@ -14,7 +14,7 @@ import ( // NewRouter returns a new router instance. func NewRouter() *Router { - return &Router{namedRoutes: make(map[string]*Route)} + return &Router{namedRoutes: make(map[string]*Route), KeepContext: false} } // Router registers routes to be matched and dispatches a handler. @@ -46,6 +46,8 @@ type Router struct { namedRoutes map[string]*Route // See Router.StrictSlash(). This defines the flag for new routes. strictSlash bool + // If true, do not clear the the request context after handling the request + KeepContext bool } // Match matches registered routes against the request. @@ -82,7 +84,9 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { } handler = r.NotFoundHandler } - defer context.Clear(req) + if !r.KeepContext { + defer context.Clear(req) + } handler.ServeHTTP(w, req) } diff --git a/mux_test.go b/mux_test.go index 55159bd..8789697 100644 --- a/mux_test.go +++ b/mux_test.go @@ -8,6 +8,8 @@ import ( "fmt" "net/http" "testing" + + "github.com/gorilla/context" ) type routeTest struct { @@ -656,6 +658,36 @@ func testRoute(t *testing.T, test routeTest) { } } +// Tests that the context is cleared or not cleared properly depending on +// the configuration of the router +func TestKeepContext(t *testing.T) { + func1 := func(w http.ResponseWriter, r *http.Request) {} + + r := NewRouter() + r.HandleFunc("/", func1).Name("func1") + + req, _ := http.NewRequest("GET", "http://localhost/", nil) + context.Set(req, "t", 1) + + res := new(http.ResponseWriter) + r.ServeHTTP(*res, req) + + if _, ok := context.GetOk(req, "t"); ok { + t.Error("Context should have been cleared at end of request") + } + + r.KeepContext = true + + req, _ = http.NewRequest("GET", "http://localhost/", nil) + context.Set(req, "t", 1) + + r.ServeHTTP(*res, req) + if _, ok := context.GetOk(req, "t"); !ok { + t.Error("Context should NOT have been cleared at end of request") + } + +} + // https://plus.google.com/101022900381697718949/posts/eWy6DjFJ6uW func TestSubrouterHeader(t *testing.T) { expected := "func1 response" From 04a79835ae36db13cbcc39e8420082a48549a42a Mon Sep 17 00:00:00 2001 From: Christopher Pfohl Date: Thu, 29 Aug 2013 12:05:40 -0400 Subject: [PATCH 13/91] Add "of" like the rest of the function docstrings --- route.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/route.go b/route.go index cb538ea..7766254 100644 --- a/route.go +++ b/route.go @@ -328,7 +328,7 @@ func (m schemeMatcher) Match(r *http.Request, match *RouteMatch) bool { } // Schemes adds a matcher for URL schemes. -// It accepts a sequence schemes to be matched, e.g.: "http", "https". +// It accepts a sequence of schemes to be matched, e.g.: "http", "https". func (r *Route) Schemes(schemes ...string) *Route { for k, v := range schemes { schemes[k] = strings.ToLower(v) From 0ae6d35b080d1dcbc5744904e4aec8504283115d Mon Sep 17 00:00:00 2001 From: Julien Schmidt Date: Wed, 11 Sep 2013 20:33:30 +0200 Subject: [PATCH 14/91] Fix typo remove doubly present "the" --- mux.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mux.go b/mux.go index 6136ab9..ddc1acc 100644 --- a/mux.go +++ b/mux.go @@ -46,7 +46,7 @@ type Router struct { namedRoutes map[string]*Route // See Router.StrictSlash(). This defines the flag for new routes. strictSlash bool - // If true, do not clear the the request context after handling the request + // If true, do not clear the request context after handling the request KeepContext bool } From 9d99a927754de25f584c08c423866c12207ce9e3 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Sat, 9 Nov 2013 22:20:00 -0600 Subject: [PATCH 15/91] Remove extra parentheses --- doc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc.go b/doc.go index 8ee5540..b2deed3 100644 --- a/doc.go +++ b/doc.go @@ -134,7 +134,7 @@ the inner routes use it as base for their paths: // "/products/{key}/" s.HandleFunc("/{key}/", ProductHandler) // "/products/{key}/details" - s.HandleFunc("/{key}/details"), ProductDetailsHandler) + s.HandleFunc("/{key}/details", ProductDetailsHandler) Now let's see how to build registered URLs. From d10d5466f2db2758bb616f627775692d2f69fe8a Mon Sep 17 00:00:00 2001 From: Philip Schlump Date: Thu, 14 Nov 2013 19:27:38 -0700 Subject: [PATCH 16/91] Fixed problem with droping query string. --- mux.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mux.go b/mux.go index ddc1acc..ca51a01 100644 --- a/mux.go +++ b/mux.go @@ -67,6 +67,14 @@ func (r *Router) Match(req *http.Request, match *RouteMatch) bool { 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. + // 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 + url.Path = p + p = url.String() + w.Header().Set("Location", p) w.WriteHeader(http.StatusMovedPermanently) return From 1a2411f44a858576b750ffb9d5c0975570ef1f2d Mon Sep 17 00:00:00 2001 From: Philip Schlump Date: Mon, 18 Nov 2013 10:14:28 -0700 Subject: [PATCH 17/91] Added tests to verify that the 301 redirect returns query string --- mux_test.go | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/mux_test.go b/mux_test.go index 8789697..4cbd55d 100644 --- a/mux_test.go +++ b/mux_test.go @@ -8,6 +8,7 @@ import ( "fmt" "net/http" "testing" +// "encoding/json" // Philip Schlump "github.com/gorilla/context" ) @@ -22,6 +23,18 @@ type routeTest struct { shouldMatch bool // whether the request is expected to match the route at all } +// Philip Schlump - added to understand the results from tests. +//func dumpVar ( v interface{} ) { +// // s, err := json.Marshal ( v ) +// s, err := json.MarshalIndent ( v, "", "\t" ) +// if ( err != nil ) { +// fmt.Printf ( "Error: %s\n", err ) +// } else { +// fmt.Printf ( "%s\n", s ) +// } +//} + + func TestHost(t *testing.T) { // newRequestHost a new request with a method, url, and host header newRequestHost := func(method, url, host string) *http.Request { @@ -416,6 +429,15 @@ func TestQueries(t *testing.T) { path: "", shouldMatch: true, }, + { + title: "Queries route, match (ToDo - with redirect 301) (Philip Schlump added)", + route: new(Route).Host("www.2cwhy.com").Path("/api").Queries("foo", "bar", "baz", "ding"), + request: newRequest("GET", "http://www.2cwhy.com/api?foo=bar&baz=ding"), + vars: map[string]string{}, + host: "", + path: "", + shouldMatch: true, + }, { title: "Queries route, bad query", route: new(Route).Queries("foo", "bar", "baz", "ding"), @@ -655,6 +677,8 @@ func testRoute(t *testing.T, test routeTest) { return } } + // Philip Schlump - Added to understand what match is returning. + // dumpVar ( match ) } } @@ -663,7 +687,7 @@ func testRoute(t *testing.T, test routeTest) { func TestKeepContext(t *testing.T) { func1 := func(w http.ResponseWriter, r *http.Request) {} - r := NewRouter() + r:= NewRouter() r.HandleFunc("/", func1).Name("func1") req, _ := http.NewRequest("GET", "http://localhost/", nil) @@ -688,6 +712,59 @@ func TestKeepContext(t *testing.T) { } + +type TestA301ResponseWriter struct { + hh http.Header + status int +} + +func (ho TestA301ResponseWriter) Header() http.Header { + // fmt.Printf ( "Header() called\n" ); + return http.Header(ho.hh) +} + +func (ho TestA301ResponseWriter) Write( b []byte) (int, error) { + // fmt.Printf ( "Write called %v\n", b ); + return 0, nil +} + +func (ho TestA301ResponseWriter) WriteHeader( code int ) { + // fmt.Printf ( "WriteHeader called code=%d\n", code ); + ho.status = code +} + +func Test301Redirect(t *testing.T) { + m := make(http.Header) + + func1 := func(w http.ResponseWriter, r *http.Request) {} + func2 := func(w http.ResponseWriter, r *http.Request) {} + + r:= NewRouter() + r.HandleFunc("/api/", func2).Name("func2") + r.HandleFunc("/", func1).Name("func1") + + req, _ := http.NewRequest("GET", "http://localhost//api/?abc=def", nil) + + res := TestA301ResponseWriter{ + hh: m, + status : 0, + } + r.ServeHTTP(&res, req) + + //fmt.Printf ( "This one %v\n", res ); + //fmt.Printf ( "res[\"Location\"] = ///%v///\n", res.hh["Location"] ); + //fmt.Printf ( "res[\"Location\"][0] = ///%v///\n", res.hh["Location"][0] ); + if "http://localhost/api/?abc=def" != res.hh["Location"][0] { + t.Errorf("Should have complete URL with query string") + } + // OK - I don't understand why this check on "status is not working. + // (p.s. the real answer is I am still learning go) + //if 301 != res.status { + // t.Errorf("Should have status of 301, got %d", res.status ) + //} + //fmt.Printf ( "Done\n" ); + +} // https://plus.google.com/101022900381697718949/posts/eWy6DjFJ6uW func TestSubrouterHeader(t *testing.T) { expected := "func1 response" From 6689ee8243e28381f38fff95d65c84a575fa86f8 Mon Sep 17 00:00:00 2001 From: Philip Schlump Date: Mon, 18 Nov 2013 10:54:45 -0700 Subject: [PATCH 18/91] Cleaned up testing. --- mux_test.go | 30 ++---------------------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/mux_test.go b/mux_test.go index 4cbd55d..35f0037 100644 --- a/mux_test.go +++ b/mux_test.go @@ -8,7 +8,6 @@ import ( "fmt" "net/http" "testing" -// "encoding/json" // Philip Schlump "github.com/gorilla/context" ) @@ -23,17 +22,6 @@ type routeTest struct { shouldMatch bool // whether the request is expected to match the route at all } -// Philip Schlump - added to understand the results from tests. -//func dumpVar ( v interface{} ) { -// // s, err := json.Marshal ( v ) -// s, err := json.MarshalIndent ( v, "", "\t" ) -// if ( err != nil ) { -// fmt.Printf ( "Error: %s\n", err ) -// } else { -// fmt.Printf ( "%s\n", s ) -// } -//} - func TestHost(t *testing.T) { // newRequestHost a new request with a method, url, and host header @@ -430,7 +418,7 @@ func TestQueries(t *testing.T) { shouldMatch: true, }, { - title: "Queries route, match (ToDo - with redirect 301) (Philip Schlump added)", + title: "Queries route, match with a query string", route: new(Route).Host("www.2cwhy.com").Path("/api").Queries("foo", "bar", "baz", "ding"), request: newRequest("GET", "http://www.2cwhy.com/api?foo=bar&baz=ding"), vars: map[string]string{}, @@ -677,8 +665,6 @@ func testRoute(t *testing.T, test routeTest) { return } } - // Philip Schlump - Added to understand what match is returning. - // dumpVar ( match ) } } @@ -719,17 +705,14 @@ type TestA301ResponseWriter struct { } func (ho TestA301ResponseWriter) Header() http.Header { - // fmt.Printf ( "Header() called\n" ); return http.Header(ho.hh) } func (ho TestA301ResponseWriter) Write( b []byte) (int, error) { - // fmt.Printf ( "Write called %v\n", b ); return 0, nil } func (ho TestA301ResponseWriter) WriteHeader( code int ) { - // fmt.Printf ( "WriteHeader called code=%d\n", code ); ho.status = code } @@ -751,20 +734,11 @@ func Test301Redirect(t *testing.T) { } r.ServeHTTP(&res, req) - //fmt.Printf ( "This one %v\n", res ); - //fmt.Printf ( "res[\"Location\"] = ///%v///\n", res.hh["Location"] ); - //fmt.Printf ( "res[\"Location\"][0] = ///%v///\n", res.hh["Location"][0] ); if "http://localhost/api/?abc=def" != res.hh["Location"][0] { t.Errorf("Should have complete URL with query string") } - // OK - I don't understand why this check on "status is not working. - // (p.s. the real answer is I am still learning go) - //if 301 != res.status { - // t.Errorf("Should have status of 301, got %d", res.status ) - //} - //fmt.Printf ( "Done\n" ); - } + // https://plus.google.com/101022900381697718949/posts/eWy6DjFJ6uW func TestSubrouterHeader(t *testing.T) { expected := "func1 response" From ab8ae247f17a9f1bbe9bda321aa6719600eb4775 Mon Sep 17 00:00:00 2001 From: Philip Schlump Date: Mon, 18 Nov 2013 11:53:02 -0700 Subject: [PATCH 19/91] Fixed the domain name copied from my other code to be example.com --- mux_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mux_test.go b/mux_test.go index 35f0037..1a2a092 100644 --- a/mux_test.go +++ b/mux_test.go @@ -419,8 +419,8 @@ func TestQueries(t *testing.T) { }, { title: "Queries route, match with a query string", - route: new(Route).Host("www.2cwhy.com").Path("/api").Queries("foo", "bar", "baz", "ding"), - request: newRequest("GET", "http://www.2cwhy.com/api?foo=bar&baz=ding"), + route: new(Route).Host("www.example.com").Path("/api").Queries("foo", "bar", "baz", "ding"), + request: newRequest("GET", "http://www.example.com/api?foo=bar&baz=ding"), vars: map[string]string{}, host: "", path: "", From cebebed6c46dc99617b23015fa96ef1f78d93ee4 Mon Sep 17 00:00:00 2001 From: Andrews Medina Date: Fri, 29 Nov 2013 22:23:25 -0200 Subject: [PATCH 20/91] go fmt. result of `gofmt -s -w . `. --- old_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/old_test.go b/old_test.go index 7e266bb..4253059 100644 --- a/old_test.go +++ b/old_test.go @@ -96,8 +96,8 @@ func TestRouteMatchers(t *testing.T) { method = "GET" headers = map[string]string{"X-Requested-With": "XMLHttpRequest"} resultVars = map[bool]map[string]string{ - true: map[string]string{"var1": "www", "var2": "product", "var3": "42"}, - false: map[string]string{}, + true: {"var1": "www", "var2": "product", "var3": "42"}, + false: {}, } } @@ -110,8 +110,8 @@ func TestRouteMatchers(t *testing.T) { method = "POST" headers = map[string]string{"Content-Type": "application/json"} resultVars = map[bool]map[string]string{ - true: map[string]string{"var4": "google", "var5": "product", "var6": "42"}, - false: map[string]string{}, + true: {"var4": "google", "var5": "product", "var6": "42"}, + false: {}, } } From 3829d74241c40bf4b132a413fddeb190b061dc95 Mon Sep 17 00:00:00 2001 From: Kamil Kisiel Date: Sat, 30 Nov 2013 13:24:27 -0800 Subject: [PATCH 21/91] Add travis.yml --- .travis.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..70e012b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +language: go + +go: + - 1.0 + - 1.1 + - tip From e807f7c33833c2264798b00c24fce1cfb6d6b298 Mon Sep 17 00:00:00 2001 From: Kamil Kisiel Date: Sat, 30 Nov 2013 13:26:46 -0800 Subject: [PATCH 22/91] Add Travis badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f6db41a..e60301b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ mux === +[![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. From 9ede152210fa25c1377d33e867cb828c19316445 Mon Sep 17 00:00:00 2001 From: Kamil Kisiel Date: Wed, 4 Dec 2013 23:16:17 -0800 Subject: [PATCH 23/91] Add go 1.2 to travis.yml --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 70e012b..d87d465 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,4 +3,5 @@ language: go go: - 1.0 - 1.1 + - 1.2 - tip From 9ee70e572c1827c6524470f8fd12839bf7b98a39 Mon Sep 17 00:00:00 2001 From: Quinn Slack Date: Thu, 17 Apr 2014 00:44:35 -0700 Subject: [PATCH 24/91] gofmt --- mux.go | 2 +- mux_test.go | 20 +++++++++----------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/mux.go b/mux.go index ca51a01..711630d 100644 --- a/mux.go +++ b/mux.go @@ -69,7 +69,7 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { 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. - // This matches with fix in go 1.2 r.c. 4 for same problem. Go Issue: + // 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 url.Path = p diff --git a/mux_test.go b/mux_test.go index 1a2a092..292776d 100644 --- a/mux_test.go +++ b/mux_test.go @@ -22,7 +22,6 @@ type routeTest struct { shouldMatch bool // whether the request is expected to match the route at all } - func TestHost(t *testing.T) { // newRequestHost a new request with a method, url, and host header newRequestHost := func(method, url, host string) *http.Request { @@ -673,7 +672,7 @@ func testRoute(t *testing.T, test routeTest) { func TestKeepContext(t *testing.T) { func1 := func(w http.ResponseWriter, r *http.Request) {} - r:= NewRouter() + r := NewRouter() r.HandleFunc("/", func1).Name("func1") req, _ := http.NewRequest("GET", "http://localhost/", nil) @@ -698,21 +697,20 @@ func TestKeepContext(t *testing.T) { } - type TestA301ResponseWriter struct { - hh http.Header - status int + hh http.Header + status int } func (ho TestA301ResponseWriter) Header() http.Header { return http.Header(ho.hh) } -func (ho TestA301ResponseWriter) Write( b []byte) (int, error) { +func (ho TestA301ResponseWriter) Write(b []byte) (int, error) { return 0, nil } -func (ho TestA301ResponseWriter) WriteHeader( code int ) { +func (ho TestA301ResponseWriter) WriteHeader(code int) { ho.status = code } @@ -722,16 +720,16 @@ func Test301Redirect(t *testing.T) { func1 := func(w http.ResponseWriter, r *http.Request) {} func2 := func(w http.ResponseWriter, r *http.Request) {} - r:= NewRouter() + r := NewRouter() r.HandleFunc("/api/", func2).Name("func2") r.HandleFunc("/", func1).Name("func1") req, _ := http.NewRequest("GET", "http://localhost//api/?abc=def", nil) res := TestA301ResponseWriter{ - hh: m, - status : 0, - } + hh: m, + status: 0, + } r.ServeHTTP(&res, req) if "http://localhost/api/?abc=def" != res.hh["Location"][0] { From 525eff436e45137f9c4ffa422f18bab457a96500 Mon Sep 17 00:00:00 2001 From: Thomas ten Cate Date: Wed, 23 Apr 2014 17:40:15 +0200 Subject: [PATCH 25/91] Improve docs about leading and trailing slashes --- mux_test.go | 9 +++++++++ route.go | 8 ++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/mux_test.go b/mux_test.go index 292776d..95b894c 100644 --- a/mux_test.go +++ b/mux_test.go @@ -214,6 +214,15 @@ func TestPathPrefix(t *testing.T) { path: "/111", shouldMatch: true, }, + { + title: "PathPrefix route, match substring", + route: new(Route).PathPrefix("/1"), + request: newRequest("GET", "http://localhost/111/222/333"), + vars: map[string]string{}, + host: "", + path: "/1", + shouldMatch: true, + }, { title: "PathPrefix route, URL prefix in request does not match", route: new(Route).PathPrefix("/111"), diff --git a/route.go b/route.go index 7766254..6bf84ec 100644 --- a/route.go +++ b/route.go @@ -259,7 +259,8 @@ func (r *Route) Methods(methods ...string) *Route { // Path ----------------------------------------------------------------------- // Path adds a matcher for the URL path. -// It accepts a template with zero or more URL variables enclosed by {}. +// 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: // // - {name} matches anything until the next slash. @@ -283,7 +284,10 @@ func (r *Route) Path(tpl string) *Route { // PathPrefix ----------------------------------------------------------------- -// PathPrefix adds a matcher for the URL path prefix. +// PathPrefix adds a matcher for the URL path prefix. Note that it does not +// treat slashes specially ("/foobar/" will be matched by the prefix "/foo") so +// in most cases you'll want to use a trailing slash here. See Route.Path() for +// details on the tpl argument. func (r *Route) PathPrefix(tpl string) *Route { r.strictSlash = false r.err = r.addRegexpMatcher(tpl, false, true) From 033224c12ed48938d1815f4851103f86abcf6201 Mon Sep 17 00:00:00 2001 From: Thomas ten Cate Date: Wed, 23 Apr 2014 18:19:14 +0200 Subject: [PATCH 26/91] Document behaviour of StrictSlash and PathPrefix better, and add tests to nail this down --- mux.go | 9 +++++++-- mux_test.go | 27 +++++++++++++++++++++++++++ route.go | 13 +++++++++---- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/mux.go b/mux.go index 711630d..fa12ffa 100644 --- a/mux.go +++ b/mux.go @@ -109,10 +109,15 @@ func (r *Router) GetRoute(name string) *Route { return r.getNamedRoutes()[name] } -// StrictSlash defines the slash behavior for new routes. +// StrictSlash defines the trailing slash behavior for new routes. The initial +// value is false. // // When true, if the route path is "/path/", accessing "/path" will redirect -// to the former and vice versa. +// to the former and vice versa. In other words, your application will always +// see the path as specified in the route. +// +// When false, if the route path is "/path", accessing "/path/" will not match +// this route and vice versa. // // Special case: when a route sets a path prefix, strict slash is // automatically set to false for that route because the redirect behavior diff --git a/mux_test.go b/mux_test.go index 95b894c..29a4ef2 100644 --- a/mux_test.go +++ b/mux_test.go @@ -151,6 +151,33 @@ func TestPath(t *testing.T) { path: "/111/222/333", shouldMatch: true, }, + { + title: "Path route, match with trailing slash in request and path", + route: new(Route).Path("/111/"), + request: newRequest("GET", "http://localhost/111/"), + vars: map[string]string{}, + host: "", + path: "/111/", + shouldMatch: true, + }, + { + title: "Path route, do not match with trailing slash in path", + route: new(Route).Path("/111/"), + request: newRequest("GET", "http://localhost/111"), + vars: map[string]string{}, + host: "", + path: "/111", + shouldMatch: false, + }, + { + title: "Path route, do not match with trailing slash in request", + route: new(Route).Path("/111"), + request: newRequest("GET", "http://localhost/111/"), + vars: map[string]string{}, + host: "", + path: "/111/", + shouldMatch: false, + }, { title: "Path route, wrong path in request in request URL", route: new(Route).Path("/111/222/333"), diff --git a/route.go b/route.go index 6bf84ec..4ce8c82 100644 --- a/route.go +++ b/route.go @@ -284,10 +284,15 @@ func (r *Route) Path(tpl string) *Route { // PathPrefix ----------------------------------------------------------------- -// PathPrefix adds a matcher for the URL path prefix. Note that it does not -// treat slashes specially ("/foobar/" will be matched by the prefix "/foo") so -// in most cases you'll want to use a trailing slash here. See Route.Path() for -// details on the tpl argument. +// PathPrefix adds a matcher for the URL path prefix. This matches if the given +// template is a prefix of the full URL path. See Route.Path() for details on +// the tpl argument. +// +// Note that it does not treat slashes specially ("/foobar/" will be matched by +// the prefix "/foo") so you may want to use a trailing slash here. +// +// 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.strictSlash = false r.err = r.addRegexpMatcher(tpl, false, true) From bac13721298f7a2dbf554446a58826c2035e70e8 Mon Sep 17 00:00:00 2001 From: Thomas ten Cate Date: Wed, 23 Apr 2014 19:24:12 +0200 Subject: [PATCH 27/91] Convert TestStrictSlash to a table driven test --- mux_test.go | 57 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/mux_test.go b/mux_test.go index 29a4ef2..a67efee 100644 --- a/mux_test.go +++ b/mux_test.go @@ -13,13 +13,14 @@ import ( ) type routeTest struct { - title string // title of the test - route *Route // the route being tested - request *http.Request // a request to test the route - vars map[string]string // the expected vars of the match - host string // the expected host of the match - path string // the expected path of the match - shouldMatch bool // whether the request is expected to match the route at all + title string // title of the test + route *Route // the route being tested + request *http.Request // a request to test the route + vars map[string]string // the expected vars of the match + host string // the expected host of the match + path string // the expected path of the match + shouldMatch bool // whether the request is expected to match the route at all + shouldRedirect bool // whether the request should result in a redirect } func TestHost(t *testing.T) { @@ -615,26 +616,23 @@ func TestNamedRoutes(t *testing.T) { } func TestStrictSlash(t *testing.T) { - var r *Router - var req *http.Request - var route *Route - var match *RouteMatch - var matched bool - - // StrictSlash should be ignored for path prefix. - // So we register a route ending in slash but it doesn't attempt to add - // the slash for a path not ending in slash. - r = NewRouter() + r := NewRouter() r.StrictSlash(true) - route = r.NewRoute().PathPrefix("/static/") - req, _ = http.NewRequest("GET", "http://localhost/static/logo.png", nil) - match = new(RouteMatch) - matched = r.Match(req, match) - if !matched { - t.Errorf("Should match request %q -- %v", req.URL.Path, getRouteTemplate(route)) + + tests := []routeTest{ + { + title: "Ignore StrictSlash for path prefix", + route: r.NewRoute().PathPrefix("/static/"), + request: newRequest("GET", "http://localhost/static/logo.png"), + vars: map[string]string{}, + path: "/static/", + shouldMatch: true, + shouldRedirect: false, + }, } - if match.Handler != nil { - t.Errorf("Should not redirect") + + for _, test := range tests { + testRoute(t, test) } } @@ -663,6 +661,7 @@ func testRoute(t *testing.T, test routeTest) { host := test.host path := test.path url := test.host + test.path + shouldRedirect := test.shouldRedirect var match RouteMatch ok := route.Match(request, &match) @@ -700,6 +699,14 @@ func testRoute(t *testing.T, test routeTest) { return } } + if shouldRedirect && match.Handler == nil { + t.Errorf("(%v) Did not redirect", test.title) + return + } + if !shouldRedirect && match.Handler != nil { + t.Errorf("(%v) Unexpected redirect", test.title) + return + } } } From 3509745ae8a2db852c51794a6f6e9f1e533e1632 Mon Sep 17 00:00:00 2001 From: Thomas ten Cate Date: Wed, 23 Apr 2014 19:27:53 +0200 Subject: [PATCH 28/91] Add tests for all four StrictSlash cases --- mux_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/mux_test.go b/mux_test.go index a67efee..63bc491 100644 --- a/mux_test.go +++ b/mux_test.go @@ -620,11 +620,52 @@ func TestStrictSlash(t *testing.T) { r.StrictSlash(true) tests := []routeTest{ + { + title: "Redirect path without slash", + route: r.NewRoute().Path("/111/"), + request: newRequest("GET", "http://localhost/111"), + vars: map[string]string{}, + host: "", + path: "/111/", + shouldMatch: true, + shouldRedirect: true, + }, + { + title: "Do not redirect path with slash", + route: r.NewRoute().Path("/111/"), + request: newRequest("GET", "http://localhost/111/"), + vars: map[string]string{}, + host: "", + path: "/111/", + shouldMatch: true, + shouldRedirect: false, + }, + { + title: "Redirect path with slash", + route: r.NewRoute().Path("/111"), + request: newRequest("GET", "http://localhost/111/"), + vars: map[string]string{}, + host: "", + path: "/111", + shouldMatch: true, + shouldRedirect: true, + }, + { + title: "Do not redirect path without slash", + route: r.NewRoute().Path("/111"), + request: newRequest("GET", "http://localhost/111"), + vars: map[string]string{}, + host: "", + path: "/111", + shouldMatch: true, + shouldRedirect: false, + }, { title: "Ignore StrictSlash for path prefix", route: r.NewRoute().PathPrefix("/static/"), request: newRequest("GET", "http://localhost/static/logo.png"), vars: map[string]string{}, + host: "", path: "/static/", shouldMatch: true, shouldRedirect: false, From b864f07c539c7abc16652d65350d070d8e2810de Mon Sep 17 00:00:00 2001 From: Thomas ten Cate Date: Wed, 23 Apr 2014 19:53:35 +0200 Subject: [PATCH 29/91] Propagate StrictSlash to subrouters instead of rudely turning it off --- mux.go | 7 ++++--- mux_test.go | 10 ++++++++++ regexp.go | 17 ++++++++++------- route.go | 1 - 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/mux.go b/mux.go index fa12ffa..8b23c39 100644 --- a/mux.go +++ b/mux.go @@ -119,9 +119,10 @@ func (r *Router) GetRoute(name string) *Route { // When false, if the route path is "/path", accessing "/path/" will not match // this route and vice versa. // -// Special case: when a route sets a path prefix, strict slash is -// automatically set to false for that route because the redirect behavior -// can't be determined for prefixes. +// Special case: when a route sets a path prefix using the PathPrefix() method, +// strict slash is ignored for that route because the redirect behavior can't +// be determined from a prefix alone. However, any subrouters created from that +// route inherit the original StrictSlash setting. func (r *Router) StrictSlash(value bool) *Router { r.strictSlash = value return r diff --git a/mux_test.go b/mux_test.go index 63bc491..0e2e480 100644 --- a/mux_test.go +++ b/mux_test.go @@ -660,6 +660,16 @@ func TestStrictSlash(t *testing.T) { shouldMatch: true, shouldRedirect: false, }, + { + title: "Propagate StrictSlash to subrouters", + route: r.NewRoute().PathPrefix("/static/").Subrouter().Path("/images/"), + request: newRequest("GET", "http://localhost/static/images"), + vars: map[string]string{}, + host: "", + path: "/static/images/", + shouldMatch: true, + shouldRedirect: true, + }, { title: "Ignore StrictSlash for path prefix", route: r.NewRoute().PathPrefix("/static/"), diff --git a/regexp.go b/regexp.go index 4c3482b..925f268 100644 --- a/regexp.go +++ b/regexp.go @@ -98,12 +98,13 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, strictSlash bool) (*rout } // Done! return &routeRegexp{ - template: template, - matchHost: matchHost, - regexp: reg, - reverse: reverse.String(), - varsN: varsN, - varsR: varsR, + template: template, + matchHost: matchHost, + strictSlash: strictSlash, + regexp: reg, + reverse: reverse.String(), + varsN: varsN, + varsR: varsR, }, nil } @@ -114,6 +115,8 @@ type routeRegexp struct { template string // True for host match, false for path match. matchHost bool + // The strictSlash value defined on the route, but disabled if PathPrefix was used. + strictSlash bool // Expanded regexp. regexp *regexp.Regexp // Reverse template. @@ -216,7 +219,7 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) m.Vars[v] = pathVars[k+1] } // Check if we should redirect. - if r.strictSlash { + if v.path.strictSlash { p1 := strings.HasSuffix(req.URL.Path, "/") p2 := strings.HasSuffix(v.path.template, "/") if p1 != p2 { diff --git a/route.go b/route.go index 4ce8c82..5cb2526 100644 --- a/route.go +++ b/route.go @@ -294,7 +294,6 @@ 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.strictSlash = false r.err = r.addRegexpMatcher(tpl, false, true) return r } From cef3b0cd6d468434914c21146d6e400436f2d0e3 Mon Sep 17 00:00:00 2001 From: Quinn Slack Date: Thu, 17 Apr 2014 01:17:15 -0700 Subject: [PATCH 30/91] Add BuildVarsFunc to allow modifying route variables before generating a route's URL --- mux.go | 6 ++++++ mux_test.go | 19 +++++++++++++++++++ regexp.go | 6 +----- route.go | 49 ++++++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 70 insertions(+), 10 deletions(-) diff --git a/mux.go b/mux.go index 711630d..e22dcac 100644 --- a/mux.go +++ b/mux.go @@ -218,6 +218,12 @@ func (r *Router) Schemes(schemes ...string) *Route { return r.NewRoute().Schemes(schemes...) } +// BuildVars registers a new route with a custom function for modifying +// route variables before building a URL. +func (r *Router) BuildVarsFunc(f BuildVarsFunc) *Route { + return r.NewRoute().BuildVarsFunc(f) +} + // ---------------------------------------------------------------------------- // Context // ---------------------------------------------------------------------------- diff --git a/mux_test.go b/mux_test.go index 292776d..bd76a01 100644 --- a/mux_test.go +++ b/mux_test.go @@ -511,6 +511,25 @@ func TestMatcherFunc(t *testing.T) { } } +func TestBuildVarsFunc(t *testing.T) { + tests := []routeTest{ + { + route: new(Route).Path(`/111/{v1:\d}{v2:.*}`).BuildVarsFunc(func(vars map[string]string) map[string]string { + vars["v1"] = "3" + vars["v2"] = "a" + return vars + }), + request: newRequest("GET", "http://localhost/111/2"), + path: "/111/3a", + shouldMatch: true, + }, + } + + for _, test := range tests { + testRoute(t, test) + } +} + func TestSubRouter(t *testing.T) { subrouter1 := new(Route).Host("{v1:[a-z]+}.google.com").Subrouter() subrouter2 := new(Route).PathPrefix("/foo/{v1}").Subrouter() diff --git a/regexp.go b/regexp.go index 4c3482b..e56f1bb 100644 --- a/regexp.go +++ b/regexp.go @@ -133,11 +133,7 @@ func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool { } // url builds a URL part using the given values. -func (r *routeRegexp) url(pairs ...string) (string, error) { - values, err := mapFromPairs(pairs...) - if err != nil { - return "", err - } +func (r *routeRegexp) url(values map[string]string) (string, error) { urlValues := make([]interface{}, len(r.varsN)) for k, v := range r.varsN { value, ok := values[v] diff --git a/route.go b/route.go index 7766254..e79afa7 100644 --- a/route.go +++ b/route.go @@ -31,6 +31,8 @@ type Route struct { name string // Error resulted from building a route. err error + + buildVarsFunc BuildVarsFunc } // Match matches the route against the request. @@ -336,6 +338,19 @@ func (r *Route) Schemes(schemes ...string) *Route { return r.addMatcher(schemeMatcher(schemes)) } +// BuildVarsFunc -------------------------------------------------------------- + +// BuildVarsFunc is the function signature used by custom build variable +// functions (which can modify route variables before a route's URL is built). +type BuildVarsFunc func(map[string]string) map[string]string + +// BuildVarsFunc adds a custom function to be used to modify build variables +// before a route's URL is built. +func (r *Route) BuildVarsFunc(f BuildVarsFunc) *Route { + r.buildVarsFunc = f + return r +} + // Subrouter ------------------------------------------------------------------ // Subrouter creates a subrouter for the route. @@ -398,17 +413,20 @@ func (r *Route) URL(pairs ...string) (*url.URL, error) { if r.regexp == nil { return nil, errors.New("mux: route doesn't have a host or path") } + values, err := r.buildVars(pairs...) + if err != nil { + return nil, err + } var scheme, host, path string - var err error if r.regexp.host != nil { // Set a default scheme. scheme = "http" - if host, err = r.regexp.host.url(pairs...); err != nil { + if host, err = r.regexp.host.url(values); err != nil { return nil, err } } if r.regexp.path != nil { - if path, err = r.regexp.path.url(pairs...); err != nil { + if path, err = r.regexp.path.url(values); err != nil { return nil, err } } @@ -429,7 +447,11 @@ func (r *Route) URLHost(pairs ...string) (*url.URL, error) { if r.regexp == nil || r.regexp.host == nil { return nil, errors.New("mux: route doesn't have a host") } - host, err := r.regexp.host.url(pairs...) + values, err := r.buildVars(pairs...) + if err != nil { + return nil, err + } + host, err := r.regexp.host.url(values) if err != nil { return nil, err } @@ -449,7 +471,11 @@ func (r *Route) URLPath(pairs ...string) (*url.URL, error) { if r.regexp == nil || r.regexp.path == nil { return nil, errors.New("mux: route doesn't have a path") } - path, err := r.regexp.path.url(pairs...) + values, err := r.buildVars(pairs...) + if err != nil { + return nil, err + } + path, err := r.regexp.path.url(values) if err != nil { return nil, err } @@ -458,6 +484,19 @@ func (r *Route) URLPath(pairs ...string) (*url.URL, error) { }, nil } +// buildVars converts the route variable pairs into a map. If the route has a +// BuildVarsFunc, it is invoked. +func (r *Route) buildVars(pairs ...string) (map[string]string, error) { + m, err := mapFromPairs(pairs...) + if err != nil { + return nil, err + } + if r.buildVarsFunc != nil { + m = r.buildVarsFunc(m) + } + return m, nil +} + // ---------------------------------------------------------------------------- // parentRoute // ---------------------------------------------------------------------------- From a883d5a9b9d5fb9f3c084dd9858ee6fa11d9c622 Mon Sep 17 00:00:00 2001 From: Quinn Slack Date: Thu, 17 Apr 2014 01:54:35 -0700 Subject: [PATCH 31/91] Call parent BuildVarsFuncs --- mux.go | 7 +++++++ mux_test.go | 14 ++++++++++++++ route.go | 20 ++++++++++++++------ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/mux.go b/mux.go index e22dcac..caa8ad5 100644 --- a/mux.go +++ b/mux.go @@ -146,6 +146,13 @@ func (r *Router) getRegexpGroup() *routeRegexpGroup { return nil } +func (r *Router) buildVars(m map[string]string) map[string]string { + if r.parent != nil { + m = r.parent.buildVars(m) + } + return m +} + // ---------------------------------------------------------------------------- // Route factories // ---------------------------------------------------------------------------- diff --git a/mux_test.go b/mux_test.go index bd76a01..b163639 100644 --- a/mux_test.go +++ b/mux_test.go @@ -514,6 +514,7 @@ func TestMatcherFunc(t *testing.T) { func TestBuildVarsFunc(t *testing.T) { tests := []routeTest{ { + title: "BuildVarsFunc set on route", route: new(Route).Path(`/111/{v1:\d}{v2:.*}`).BuildVarsFunc(func(vars map[string]string) map[string]string { vars["v1"] = "3" vars["v2"] = "a" @@ -523,6 +524,19 @@ func TestBuildVarsFunc(t *testing.T) { path: "/111/3a", shouldMatch: true, }, + { + title: "BuildVarsFunc set on route and parent route", + route: new(Route).PathPrefix(`/{v1:\d}`).BuildVarsFunc(func(vars map[string]string) map[string]string { + vars["v1"] = "2" + return vars + }).Subrouter().Path(`/{v2:\w}`).BuildVarsFunc(func(vars map[string]string) map[string]string { + vars["v2"] = "b" + return vars + }), + request: newRequest("GET", "http://localhost/1/a"), + path: "/2/b", + shouldMatch: true, + }, } for _, test := range tests { diff --git a/route.go b/route.go index e79afa7..b2900de 100644 --- a/route.go +++ b/route.go @@ -413,7 +413,7 @@ func (r *Route) URL(pairs ...string) (*url.URL, error) { if r.regexp == nil { return nil, errors.New("mux: route doesn't have a host or path") } - values, err := r.buildVars(pairs...) + values, err := r.prepareVars(pairs...) if err != nil { return nil, err } @@ -447,7 +447,7 @@ func (r *Route) URLHost(pairs ...string) (*url.URL, error) { if r.regexp == nil || r.regexp.host == nil { return nil, errors.New("mux: route doesn't have a host") } - values, err := r.buildVars(pairs...) + values, err := r.prepareVars(pairs...) if err != nil { return nil, err } @@ -471,7 +471,7 @@ func (r *Route) URLPath(pairs ...string) (*url.URL, error) { if r.regexp == nil || r.regexp.path == nil { return nil, errors.New("mux: route doesn't have a path") } - values, err := r.buildVars(pairs...) + values, err := r.prepareVars(pairs...) if err != nil { return nil, err } @@ -484,17 +484,24 @@ func (r *Route) URLPath(pairs ...string) (*url.URL, error) { }, nil } -// buildVars converts the route variable pairs into a map. If the route has a +// prepareVars converts the route variable pairs into a map. If the route has a // BuildVarsFunc, it is invoked. -func (r *Route) buildVars(pairs ...string) (map[string]string, error) { +func (r *Route) prepareVars(pairs ...string) (map[string]string, error) { m, err := mapFromPairs(pairs...) if err != nil { return nil, err } + return r.buildVars(m), nil +} + +func (r *Route) buildVars(m map[string]string) map[string]string { + if r.parent != nil { + m = r.parent.buildVars(m) + } if r.buildVarsFunc != nil { m = r.buildVarsFunc(m) } - return m, nil + return m } // ---------------------------------------------------------------------------- @@ -505,6 +512,7 @@ func (r *Route) buildVars(pairs ...string) (map[string]string, error) { type parentRoute interface { getNamedRoutes() map[string]*Route getRegexpGroup() *routeRegexpGroup + buildVars(map[string]string) map[string]string } // getNamedRoutes returns the map where named routes are registered. From 65cc9b5df83a841607cf9f88c58cb6cdd8ab0421 Mon Sep 17 00:00:00 2001 From: Raphael Simon Date: Mon, 26 May 2014 15:01:42 -0700 Subject: [PATCH 32/91] 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 33/91] 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 34/91] 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 } From 69237eaae593124b64a499c92be50a5ce17db2e6 Mon Sep 17 00:00:00 2001 From: Raphael Simon Date: Tue, 27 May 2014 09:34:08 -0700 Subject: [PATCH 35/91] Fix "Queries" matcher to support out-of-order query string parameters --- mux_test.go | 9 +++++++++ regexp.go | 27 +++++++++++++++------------ route.go | 41 ++++++++++++++++++----------------------- 3 files changed, 42 insertions(+), 35 deletions(-) diff --git a/mux_test.go b/mux_test.go index 48506bf..e455bce 100644 --- a/mux_test.go +++ b/mux_test.go @@ -462,6 +462,15 @@ func TestQueries(t *testing.T) { path: "", shouldMatch: true, }, + { + title: "Queries route, match with a query string out of order", + route: new(Route).Host("www.example.com").Path("/api").Queries("foo", "bar", "baz", "ding"), + request: newRequest("GET", "http://www.example.com/api?baz=ding&foo=bar"), + vars: map[string]string{}, + host: "", + path: "", + shouldMatch: true, + }, { title: "Queries route, bad query", route: new(Route).Queries("foo", "bar", "baz", "ding"), diff --git a/regexp.go b/regexp.go index f1d3147..19a358b 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 = "[^?&]+" matchPrefix, strictSlash = true, false } else if matchHost { defaultPattern = "[^.]+" @@ -51,9 +51,9 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash } varsN := make([]string, len(idxs)/2) varsR := make([]*regexp.Regexp, len(idxs)/2) - pattern := bytes.NewBufferString("^") - if matchQuery { - pattern = bytes.NewBufferString("") + pattern := bytes.NewBufferString("") + if !matchQuery { + pattern.WriteByte('^') } reverse := bytes.NewBufferString("") var end int @@ -209,9 +209,9 @@ func braceIndices(s string) ([]int, error) { // routeRegexpGroup groups the route matchers that carry variables. type routeRegexpGroup struct { - host *routeRegexp - path *routeRegexp - query *routeRegexp + host *routeRegexp + path *routeRegexp + queries []*routeRegexp } // setMatch extracts the variables from the URL once a route matches. @@ -249,11 +249,14 @@ 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] + if v.queries != nil && len(v.queries) > 0 { + rawQuery := req.URL.RawQuery + for _, q := range v.queries { + queryVars := q.regexp.FindStringSubmatch(rawQuery) + if queryVars != nil { + for k, v := range q.varsN { + m.Vars[v] = queryVars[k+1] + } } } } diff --git a/route.go b/route.go index 00989bf..1ac2065 100644 --- a/route.go +++ b/route.go @@ -5,7 +5,6 @@ package mux import ( - "bytes" "errors" "fmt" "net/http" @@ -153,14 +152,16 @@ func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix, matchQuery if err != nil { return err } - if matchHost { - if r.regexp.path != nil { - if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil { + if r.regexp.queries != nil { + for _, q := range r.regexp.queries { + if err = uniqueVars(rr.varsN, q.varsN); err != nil { return err } } - if r.regexp.query != nil { - if err = uniqueVars(rr.varsN, r.regexp.query.varsN); err != nil { + } + if matchHost { + if r.regexp.path != nil { + if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil { return err } } @@ -172,18 +173,13 @@ func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix, matchQuery } } if matchQuery { - if r.regexp.path != nil { - if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil { - return err - } + if r.regexp.queries == nil { + r.regexp.queries = make([]*routeRegexp, 1) + r.regexp.queries[0] = rr + } else { + r.regexp.queries = append(r.regexp.queries, rr) } - 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 } } @@ -345,12 +341,11 @@ func (r *Route) Queries(pairs ...string) *Route { "mux: number of parameters must be multiple of 2, got %v", pairs) return nil } - var buf bytes.Buffer for i := 0; i < length; i += 2 { - buf.WriteString(fmt.Sprintf("%s=%s&", pairs[i], pairs[i+1])) + if r.err = r.addRegexpMatcher(fmt.Sprintf("%s=%s", pairs[i], pairs[i+1]), false, true, true); r.err != nil { + return r + } } - tpl := strings.TrimRight(buf.String(), "&") - r.err = r.addRegexpMatcher(tpl, false, true, true) return r } @@ -527,9 +522,9 @@ func (r *Route) getRegexpGroup() *routeRegexpGroup { } else { // Copy. r.regexp = &routeRegexpGroup{ - host: regexp.host, - path: regexp.path, - query: regexp.query, + host: regexp.host, + path: regexp.path, + queries: regexp.queries, } } } From 3505396fb555c93ffac5e511eb8addb3bc4a9397 Mon Sep 17 00:00:00 2001 From: Raphael Simon Date: Tue, 27 May 2014 11:36:15 -0700 Subject: [PATCH 36/91] Code cleanup (see comments on issue #56) --- route.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/route.go b/route.go index 1ac2065..c70fe60 100644 --- a/route.go +++ b/route.go @@ -173,12 +173,7 @@ func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix, matchQuery } } if matchQuery { - if r.regexp.queries == nil { - r.regexp.queries = make([]*routeRegexp, 1) - r.regexp.queries[0] = rr - } else { - r.regexp.queries = append(r.regexp.queries, rr) - } + r.regexp.queries = append(r.regexp.queries, rr) } else { r.regexp.path = rr } @@ -342,7 +337,7 @@ func (r *Route) Queries(pairs ...string) *Route { return nil } for i := 0; i < length; i += 2 { - if r.err = r.addRegexpMatcher(fmt.Sprintf("%s=%s", pairs[i], pairs[i+1]), false, true, true); r.err != nil { + if r.err = r.addRegexpMatcher(pairs[i]+"="+pairs[i+1], false, true, true); r.err != nil { return r } } From 451fd8b7796015afbe01cfe9168e4c51e8b40013 Mon Sep 17 00:00:00 2001 From: Raphael Simon Date: Tue, 27 May 2014 16:37:30 -0700 Subject: [PATCH 37/91] Additional code cleanup (see comments on issue #56) --- regexp.go | 14 ++++++-------- route.go | 8 +++----- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/regexp.go b/regexp.go index 19a358b..ef1db8f 100644 --- a/regexp.go +++ b/regexp.go @@ -249,14 +249,12 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) } } // Store query string variables. - if v.queries != nil && len(v.queries) > 0 { - rawQuery := req.URL.RawQuery - for _, q := range v.queries { - queryVars := q.regexp.FindStringSubmatch(rawQuery) - if queryVars != nil { - for k, v := range q.varsN { - m.Vars[v] = queryVars[k+1] - } + rawQuery := req.URL.RawQuery + for _, q := range v.queries { + queryVars := q.regexp.FindStringSubmatch(rawQuery) + if queryVars != nil { + for k, v := range q.varsN { + m.Vars[v] = queryVars[k+1] } } } diff --git a/route.go b/route.go index c70fe60..4fb85f7 100644 --- a/route.go +++ b/route.go @@ -152,11 +152,9 @@ func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix, matchQuery if err != nil { return err } - if r.regexp.queries != nil { - for _, q := range r.regexp.queries { - if err = uniqueVars(rr.varsN, q.varsN); err != nil { - return err - } + for _, q := range r.regexp.queries { + if err = uniqueVars(rr.varsN, q.varsN); err != nil { + return err } } if matchHost { From 854d482e26505d59549690719cbc009f04042c2e Mon Sep 17 00:00:00 2001 From: Kamil Kisiel Date: Wed, 4 Jun 2014 14:12:38 -0700 Subject: [PATCH 38/91] Fix racy modification of NotFoundHandler. --- mux.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mux.go b/mux.go index 8b23c39..5b5f8e7 100644 --- a/mux.go +++ b/mux.go @@ -87,10 +87,10 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { setCurrentRoute(req, match.Route) } if handler == nil { - if r.NotFoundHandler == nil { - r.NotFoundHandler = http.NotFoundHandler() - } handler = r.NotFoundHandler + if handler == nil { + handler = http.NotFoundHandler() + } } if !r.KeepContext { defer context.Clear(req) From 3351f602a5ba82692d059fad533e58702e0cb5ce Mon Sep 17 00:00:00 2001 From: Tzu-Jung Lee Date: Tue, 24 Jun 2014 11:40:51 -0700 Subject: [PATCH 39/91] gofmt --- old_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/old_test.go b/old_test.go index 06ae0eb..1f7c190 100644 --- a/old_test.go +++ b/old_test.go @@ -490,7 +490,6 @@ func TestPathMatcher(t *testing.T) { } } - func TestSchemeMatcher(t *testing.T) { for _, v := range schemeMatcherTests { request, _ := http.NewRequest("GET", v.url, nil) From 8f1a419c38688087a65a29395ac483f7b113c745 Mon Sep 17 00:00:00 2001 From: Peter Waller Date: Mon, 11 Aug 2014 15:45:39 +0100 Subject: [PATCH 40/91] Remove blank line to fix docs for Route.Queries The presence of a blank line was preventing the documentation from appearing in godoc. --- route.go | 1 - 1 file changed, 1 deletion(-) diff --git a/route.go b/route.go index 4fb85f7..c310e66 100644 --- a/route.go +++ b/route.go @@ -326,7 +326,6 @@ func (r *Route) PathPrefix(tpl string) *Route { // - {name} matches anything until the next slash. // // - {name:pattern} matches the given regexp pattern. - func (r *Route) Queries(pairs ...string) *Route { length := len(pairs) if length%2 != 0 { From 8df3a80fb8cd2b280d3b0b7d809ad80c1d0be236 Mon Sep 17 00:00:00 2001 From: Felipe Madrigal Date: Fri, 26 Sep 2014 00:20:12 -0500 Subject: [PATCH 41/91] Rearrange getHost function --- regexp.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/regexp.go b/regexp.go index ef1db8f..35c8177 100644 --- a/regexp.go +++ b/regexp.go @@ -262,13 +262,14 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) // getHost tries its best to return the request host. func getHost(r *http.Request) string { - if !r.URL.IsAbs() { - host := r.Host - // Slice off any port information. - if i := strings.Index(host, ":"); i != -1 { - host = host[:i] - } - return host + if r.URL.IsAbs() { + return r.URL.Host + } + host := r.Host + // Slice off any port information. + if i := strings.Index(host, ":"); i != -1 { + host = host[:i] } - return r.URL.Host + return host + } From d7e46398189abd89d18d874e3c53b42d492002b8 Mon Sep 17 00:00:00 2001 From: Felipe Madrigal Date: Fri, 26 Sep 2014 00:20:46 -0500 Subject: [PATCH 42/91] Rearrange rules for matching strict slash --- regexp.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/regexp.go b/regexp.go index 35c8177..a630548 100644 --- a/regexp.go +++ b/regexp.go @@ -35,12 +35,13 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash defaultPattern := "[^/]+" if matchQuery { defaultPattern = "[^?&]+" - matchPrefix, strictSlash = true, false + matchPrefix = true } else if matchHost { defaultPattern = "[^.]+" - matchPrefix, strictSlash = false, false + matchPrefix = false } - if matchPrefix { + // Only match strict slash if not matching + if matchPrefix || matchHost || matchQuery { strictSlash = false } // Set a flag for strictSlash. From 00bf0e01a12e9ce13adef15e5ddb53700d90bd81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Lafoucrie=CC=80re?= Date: Tue, 10 Mar 2015 21:36:59 -0400 Subject: [PATCH 43/91] Add tests for patterns with pipe closes #62 --- mux_test.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/mux_test.go b/mux_test.go index c37be8a..075dedb 100644 --- a/mux_test.go +++ b/mux_test.go @@ -135,6 +135,33 @@ func TestHost(t *testing.T) { path: "", shouldMatch: false, }, + { + title: "Path route with single pattern with pipe, match", + route: new(Route).Path("/{category:a|b/c}"), + request: newRequest("GET", "http://localhost/a"), + vars: map[string]string{"category": "a"}, + host: "", + path: "/a", + shouldMatch: true, + }, + { + title: "Path route with single pattern with pipe, match", + route: new(Route).Path("/{category:a|b/c}"), + request: newRequest("GET", "http://localhost/b/c"), + vars: map[string]string{"category": "b/c"}, + host: "", + path: "/b/c", + shouldMatch: true, + }, + { + 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 { testRoute(t, test) From e98fd88cbd7216fd46d0b480a047d34748d603d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Lafoucrie=CC=80re?= Date: Tue, 10 Mar 2015 21:48:38 -0400 Subject: [PATCH 44/91] Add complementary test for patterns with pipe --- mux_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mux_test.go b/mux_test.go index 075dedb..6b2c1d2 100644 --- a/mux_test.go +++ b/mux_test.go @@ -162,6 +162,15 @@ func TestHost(t *testing.T) { path: "/a/product_name/1", shouldMatch: true, }, + { + 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/b/c/product_name/1"), + vars: map[string]string{"category": "b/c", "product": "product_name", "id": "1"}, + host: "", + path: "/b/c/product_name/1", + shouldMatch: true, + }, } for _, test := range tests { testRoute(t, test) From 9641367e95358701e3b0b36e09090c8c10131e0c Mon Sep 17 00:00:00 2001 From: Dmitri Shuralyov Date: Wed, 22 Apr 2015 12:28:32 -0700 Subject: [PATCH 45/91] Fix up doc formatting, use spaces for alignment (tabs are for indentation only). This fixes issue where if user has tab width other than 4, some lines will become misaligned. For example, see the package description at https://godoc.org/github.com/gorilla/mux. --- doc.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc.go b/doc.go index b2deed3..9a5e381 100644 --- a/doc.go +++ b/doc.go @@ -89,7 +89,7 @@ There are several other matchers that can be added. To match path prefixes: 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: @@ -164,8 +164,8 @@ This also works for host variables: // url.String() will be "http://news.domain.com/articles/technology/42" url, err := r.Get("article").URL("subdomain", "news", - "category", "technology", - "id", "42") + "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 @@ -193,7 +193,7 @@ as well: // "http://news.domain.com/articles/technology/42" url, err := r.Get("article").URL("subdomain", "news", - "category", "technology", - "id", "42") + "category", "technology", + "id", "42") */ package mux From 77f424d3ae04bf4130e489900890fa84992d9b40 Mon Sep 17 00:00:00 2001 From: Craig Jellick Date: Fri, 29 May 2015 14:16:30 -0700 Subject: [PATCH 46/91] Fix typo --- route.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/route.go b/route.go index d4f0146..00d2300 100644 --- a/route.go +++ b/route.go @@ -214,7 +214,7 @@ func (r *Route) Headers(pairs ...string) *Route { // 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 +272,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 +323,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. // From a063f14812c8ea1ae2a62a8d9b2307afe23348a7 Mon Sep 17 00:00:00 2001 From: Craig Jellick Date: Fri, 29 May 2015 14:16:30 -0700 Subject: [PATCH 47/91] 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 48/91] 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 49/91] 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 a710a8bfa96010c24d50767a4207e34a0b9f1b90 Mon Sep 17 00:00:00 2001 From: Bay Dodd Date: Sun, 5 Jul 2015 12:49:02 +0100 Subject: [PATCH 50/91] adding ^ and $ to query pattern --- mux_test.go | 18 ++++++++++++++++++ regexp.go | 23 ++++++++++++++++------- route.go | 2 +- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/mux_test.go b/mux_test.go index 6b2c1d2..44cc57e 100644 --- a/mux_test.go +++ b/mux_test.go @@ -552,6 +552,24 @@ func TestQueries(t *testing.T) { path: "", shouldMatch: false, }, + { + title: "Queries route with regexp pattern with quantifier, match", + route: new(Route).Queries("foo", "{v1:[0-9]{1}}"), + request: newRequest("GET", "http://localhost?foo=1"), + vars: map[string]string{"v1": "1"}, + host: "", + path: "", + shouldMatch: true, + }, + { + title: "Queries route with regexp pattern with quantifier, regexp does not match", + route: new(Route).Queries("foo", "{v1:[0-9]{1}}"), + request: newRequest("GET", "http://localhost?foo=12"), + vars: map[string]string{}, + host: "", + path: "", + shouldMatch: false, + }, } for _, test := range tests { diff --git a/regexp.go b/regexp.go index aa30679..8838727 100644 --- a/regexp.go +++ b/regexp.go @@ -35,7 +35,6 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash defaultPattern := "[^/]+" if matchQuery { defaultPattern = "[^?&]+" - matchPrefix = true } else if matchHost { defaultPattern = "[^.]+" matchPrefix = false @@ -53,9 +52,7 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash varsN := make([]string, len(idxs)/2) varsR := make([]*regexp.Regexp, len(idxs)/2) pattern := bytes.NewBufferString("") - if !matchQuery { - pattern.WriteByte('^') - } + pattern.WriteByte('^') reverse := bytes.NewBufferString("") var end int var err error @@ -78,6 +75,7 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash 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[i/2] = name varsR[i/2], err = regexp.Compile(fmt.Sprintf("^%s$", patt)) @@ -141,7 +139,7 @@ type routeRegexp struct { func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool { if !r.matchHost { if r.matchQuery { - return r.regexp.MatchString(req.URL.RawQuery) + return r.regexp.MatchString(r.getUrlQuery(req)) } else { return r.regexp.MatchString(req.URL.Path) } @@ -175,6 +173,18 @@ func (r *routeRegexp) url(values map[string]string) (string, error) { return rv, nil } +// getUrlQuery returns a single query parameter from a request URL. +// For a URL with foo=bar&baz=ding, we return only the relevant key +// value pair for the routeRegexp. +func (r *routeRegexp) getUrlQuery(req *http.Request) string { + keyVal := strings.Split(r.template, "=") + if len(keyVal) == 0 { + return "" + } + re := regexp.MustCompile(keyVal[0] + "[^&]*") + return re.FindString(req.URL.RawQuery) +} + // braceIndices returns the first level curly brace indices from a string. // It returns an error in case of unbalanced braces. func braceIndices(s string) ([]int, error) { @@ -246,9 +256,8 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) } } // Store query string variables. - rawQuery := req.URL.RawQuery for _, q := range v.queries { - queryVars := q.regexp.FindStringSubmatch(rawQuery) + queryVars := q.regexp.FindStringSubmatch(q.getUrlQuery(req)) if queryVars != nil { for k, v := range q.varsN { m.Vars[v] = queryVars[k+1] diff --git a/route.go b/route.go index 00d2300..6cb12d3 100644 --- a/route.go +++ b/route.go @@ -336,7 +336,7 @@ func (r *Route) Queries(pairs ...string) *Route { return nil } for i := 0; i < length; i += 2 { - if r.err = r.addRegexpMatcher(pairs[i]+"="+pairs[i+1], false, true, true); r.err != nil { + if r.err = r.addRegexpMatcher(pairs[i]+"="+pairs[i+1], false, false, true); r.err != nil { return r } } From 0c9d5c08c1d6103f2ced11750a195fc94902d3fc Mon Sep 17 00:00:00 2001 From: Bay Dodd Date: Sun, 5 Jul 2015 23:00:51 +0100 Subject: [PATCH 51/91] refactoring --- regexp.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/regexp.go b/regexp.go index 8838727..d98575e 100644 --- a/regexp.go +++ b/regexp.go @@ -139,7 +139,7 @@ type routeRegexp struct { func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool { if !r.matchHost { if r.matchQuery { - return r.regexp.MatchString(r.getUrlQuery(req)) + return r.matchQueryString(req) } else { return r.regexp.MatchString(req.URL.Path) } @@ -177,12 +177,16 @@ func (r *routeRegexp) url(values map[string]string) (string, error) { // For a URL with foo=bar&baz=ding, we return only the relevant key // value pair for the routeRegexp. func (r *routeRegexp) getUrlQuery(req *http.Request) string { - keyVal := strings.Split(r.template, "=") - if len(keyVal) == 0 { + if !r.matchQuery { return "" } - re := regexp.MustCompile(keyVal[0] + "[^&]*") - return re.FindString(req.URL.RawQuery) + key := strings.Split(r.template, "=")[0] + val := req.URL.Query().Get(key) + return key + "=" + val +} + +func (r *routeRegexp) matchQueryString(req *http.Request) bool { + return r.regexp.MatchString(r.getUrlQuery(req)) } // braceIndices returns the first level curly brace indices from a string. From 9cb89f07ca03c6a28d1e237ba033bc6a29422fd6 Mon Sep 17 00:00:00 2001 From: Kamil Kisiel Date: Sun, 5 Jul 2015 16:02:00 -0700 Subject: [PATCH 52/91] Add a couple of additional tests for query strings. --- mux_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/mux_test.go b/mux_test.go index 44cc57e..8118aab 100644 --- a/mux_test.go +++ b/mux_test.go @@ -561,6 +561,15 @@ func TestQueries(t *testing.T) { path: "", shouldMatch: true, }, + { + title: "Queries route with regexp pattern with quantifier, additional variable in query string, match", + route: new(Route).Queries("foo", "{v1:[0-9]{1}}"), + request: newRequest("GET", "http://localhost?bar=2&foo=1"), + vars: map[string]string{"v1": "1"}, + host: "", + path: "", + shouldMatch: true, + }, { title: "Queries route with regexp pattern with quantifier, regexp does not match", route: new(Route).Queries("foo", "{v1:[0-9]{1}}"), @@ -570,6 +579,15 @@ func TestQueries(t *testing.T) { path: "", shouldMatch: false, }, + { + 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}}"), + request: newRequest("GET", "http://localhost?foo=12"), + vars: map[string]string{}, + host: "", + path: "", + shouldMatch: false, + }, } for _, test := range tests { From 8eb02a9f12e527fe2ce9ab3b640579a6104f2d44 Mon Sep 17 00:00:00 2001 From: Dj Gilcrease Date: Wed, 8 Jul 2015 09:47:01 -0700 Subject: [PATCH 53/91] Add URLPathTemplate to Route to make it easier to generate a API page that lists all routes by template --- route.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/route.go b/route.go index 6cb12d3..77fca93 100644 --- a/route.go +++ b/route.go @@ -508,6 +508,19 @@ func (r *Route) URLPath(pairs ...string) (*url.URL, error) { }, nil } +// URLPathTemplate returns the template used to match against for the route +// +// The route must have a path defined. +func (r *Route) URLPathTemplate() (string, error) { + if r.err != nil { + return "", r.err + } + if r.regexp == nil || r.regexp.path == nil { + return "", errors.New("mux: route doesn't have a path") + } + return r.regexp.path.template, nil +} + // 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) { From 92ae1d67265b4cf52d659a340435398e8da2ad05 Mon Sep 17 00:00:00 2001 From: Dj Gilcrease Date: Thu, 9 Jul 2015 11:46:53 -0700 Subject: [PATCH 54/91] 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 55/91] 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 56/91] 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 57/91] 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 58/91] 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 59/91] 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 60/91] 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 61/91] 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 62/91] 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 63/91] 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 64/91] 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 65/91] 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 66/91] 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 67/91] 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 68/91] 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 69/91] 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 70/91] 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 71/91] 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 72/91] 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 73/91] 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 74/91] 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 75/91] 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 76/91] 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 77/91] 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 78/91] 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 79/91] 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 80/91] 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 81/91] 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 82/91] 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 83/91] 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 84/91] 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 85/91] [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: From 0d60c4bfebed9448afb78ab65727a497cc2d9f3d Mon Sep 17 00:00:00 2001 From: Dj Gilcrease Date: Sun, 28 Feb 2016 14:42:09 -0800 Subject: [PATCH 86/91] Add tests for GetPathTemplate. Added GetHostTemplate and associated tests as well --- mux_test.go | 102 +++++++++++++++++++++++++++++++++++++++++++++++----- route.go | 17 +++++++-- 2 files changed, 108 insertions(+), 11 deletions(-) diff --git a/mux_test.go b/mux_test.go index 1ea439a..6fe249a 100644 --- a/mux_test.go +++ b/mux_test.go @@ -32,6 +32,8 @@ type routeTest struct { vars map[string]string // the expected vars of the match host string // the expected host of the match path string // the expected path of the match + path_template string // the expected path template to match + host_template string // the expected host template to match shouldMatch bool // whether the request is expected to match the route at all shouldRedirect bool // whether the request should result in a redirect } @@ -119,6 +121,7 @@ func TestHost(t *testing.T) { vars: map[string]string{"v1": "bbb"}, host: "aaa.bbb.ccc", path: "", + host_template: `aaa.{v1:[a-z]{3}}.ccc`, shouldMatch: true, }, { @@ -128,6 +131,7 @@ func TestHost(t *testing.T) { vars: map[string]string{"v1": "bbb"}, host: "aaa.bbb.ccc", path: "", + host_template: `aaa.{v1:[a-z]{2}(b|c)}.ccc`, shouldMatch: true, }, { @@ -137,6 +141,7 @@ func TestHost(t *testing.T) { vars: map[string]string{"v1": "bbb"}, host: "aaa.bbb.ccc", path: "", + host_template: `aaa.{v1:[a-z]{3}}.ccc`, shouldMatch: false, }, { @@ -146,6 +151,7 @@ func TestHost(t *testing.T) { vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"}, host: "aaa.bbb.ccc", path: "", + host_template: `{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}`, shouldMatch: true, }, { @@ -155,6 +161,7 @@ func TestHost(t *testing.T) { vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"}, host: "aaa.bbb.ccc", path: "", + host_template: `{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}`, shouldMatch: false, }, { @@ -164,6 +171,7 @@ func TestHost(t *testing.T) { vars: map[string]string{"v-1": "bbb"}, host: "aaa.bbb.ccc", path: "", + host_template: `aaa.{v-1:[a-z]{3}}.ccc`, shouldMatch: true, }, { @@ -173,6 +181,7 @@ func TestHost(t *testing.T) { vars: map[string]string{"v-1": "bbb"}, host: "aaa.bbb.ccc", path: "", + host_template: `aaa.{v-1:[a-z]{2}(b|c)}.ccc`, shouldMatch: true, }, { @@ -182,6 +191,7 @@ func TestHost(t *testing.T) { vars: map[string]string{"v-1": "aaa", "v-2": "bbb", "v-3": "ccc"}, host: "aaa.bbb.ccc", path: "", + host_template: `{v-1:[a-z]{3}}.{v-2:[a-z]{3}}.{v-3:[a-z]{3}}`, shouldMatch: true, }, { @@ -191,6 +201,7 @@ func TestHost(t *testing.T) { vars: map[string]string{"category": "a"}, host: "", path: "/a", + path_template: `/{category:a|b/c}`, shouldMatch: true, }, { @@ -200,6 +211,7 @@ func TestHost(t *testing.T) { vars: map[string]string{"category": "b/c"}, host: "", path: "/b/c", + path_template: `/{category:a|b/c}`, shouldMatch: true, }, { @@ -209,6 +221,7 @@ func TestHost(t *testing.T) { vars: map[string]string{"category": "a", "product": "product_name", "id": "1"}, host: "", path: "/a/product_name/1", + path_template: `/{category:a|b/c}/{product}/{id:[0-9]+}`, shouldMatch: true, }, { @@ -218,11 +231,13 @@ func TestHost(t *testing.T) { vars: map[string]string{"category": "b/c", "product": "product_name", "id": "1"}, host: "", path: "/b/c/product_name/1", + path_template: `/{category:a|b/c}/{product}/{id:[0-9]+}`, shouldMatch: true, }, } for _, test := range tests { testRoute(t, test) + testTemplate(t, test) } } @@ -253,6 +268,7 @@ func TestPath(t *testing.T) { vars: map[string]string{}, host: "", path: "/111", + path_template: `/111/`, shouldMatch: false, }, { @@ -262,6 +278,7 @@ func TestPath(t *testing.T) { vars: map[string]string{}, host: "", path: "/111/", + path_template: `/111`, shouldMatch: false, }, { @@ -280,6 +297,7 @@ func TestPath(t *testing.T) { vars: map[string]string{"v1": "222"}, host: "", path: "/111/222/333", + path_template: `/111/{v1:[0-9]{3}}/333`, shouldMatch: true, }, { @@ -289,6 +307,7 @@ func TestPath(t *testing.T) { vars: map[string]string{"v1": "222"}, host: "", path: "/111/222/333", + path_template: `/111/{v1:[0-9]{3}}/333`, shouldMatch: false, }, { @@ -298,6 +317,7 @@ func TestPath(t *testing.T) { vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"}, host: "", path: "/111/222/333", + path_template: `/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}`, shouldMatch: true, }, { @@ -307,6 +327,7 @@ func TestPath(t *testing.T) { vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"}, host: "", path: "/111/222/333", + path_template: `/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}`, shouldMatch: false, }, { @@ -316,6 +337,7 @@ func TestPath(t *testing.T) { vars: map[string]string{"category": "a", "product": "product_name", "id": "1"}, host: "", path: "/a/product_name/1", + path_template: `/{category:a|(b/c)}/{product}/{id:[0-9]+}`, shouldMatch: true, }, { @@ -325,6 +347,7 @@ func TestPath(t *testing.T) { vars: map[string]string{"v-1": "222"}, host: "", path: "/111/222/333", + path_template: `/111/{v-1:[0-9]{3}}/333`, shouldMatch: true, }, { @@ -334,6 +357,7 @@ func TestPath(t *testing.T) { vars: map[string]string{"v-1": "111", "v-2": "222", "v-3": "333"}, host: "", path: "/111/222/333", + path_template: `/{v-1:[0-9]{3}}/{v-2:[0-9]{3}}/{v-3:[0-9]{3}}`, shouldMatch: true, }, { @@ -343,12 +367,14 @@ func TestPath(t *testing.T) { vars: map[string]string{"product-category": "a", "product-name": "product_name", "product-id": "1"}, host: "", path: "/a/product_name/1", + path_template: `/{product-category:a|(b/c)}/{product-name}/{product-id:[0-9]+}`, shouldMatch: true, }, } for _, test := range tests { testRoute(t, test) + testTemplate(t, test) } } @@ -388,6 +414,7 @@ func TestPathPrefix(t *testing.T) { vars: map[string]string{"v1": "222"}, host: "", path: "/111/222", + path_template: `/111/{v1:[0-9]{3}}`, shouldMatch: true, }, { @@ -397,6 +424,7 @@ func TestPathPrefix(t *testing.T) { vars: map[string]string{"v1": "222"}, host: "", path: "/111/222", + path_template: `/111/{v1:[0-9]{3}}`, shouldMatch: false, }, { @@ -406,6 +434,7 @@ func TestPathPrefix(t *testing.T) { vars: map[string]string{"v1": "111", "v2": "222"}, host: "", path: "/111/222", + path_template: `/{v1:[0-9]{3}}/{v2:[0-9]{3}}`, shouldMatch: true, }, { @@ -415,12 +444,14 @@ func TestPathPrefix(t *testing.T) { vars: map[string]string{"v1": "111", "v2": "222"}, host: "", path: "/111/222", + path_template: `/{v1:[0-9]{3}}/{v2:[0-9]{3}}`, shouldMatch: false, }, } for _, test := range tests { testRoute(t, test) + testTemplate(t, test) } } @@ -433,6 +464,8 @@ func TestHostPath(t *testing.T) { vars: map[string]string{}, host: "", path: "", + path_template: `/111/222/333`, + host_template: `aaa.bbb.ccc`, shouldMatch: true, }, { @@ -442,6 +475,8 @@ func TestHostPath(t *testing.T) { vars: map[string]string{}, host: "", path: "", + path_template: `/111/222/333`, + host_template: `aaa.bbb.ccc`, shouldMatch: false, }, { @@ -451,6 +486,8 @@ func TestHostPath(t *testing.T) { vars: map[string]string{"v1": "bbb", "v2": "222"}, host: "aaa.bbb.ccc", path: "/111/222/333", + path_template: `/111/{v2:[0-9]{3}}/333`, + host_template: `aaa.{v1:[a-z]{3}}.ccc`, shouldMatch: true, }, { @@ -460,6 +497,8 @@ func TestHostPath(t *testing.T) { vars: map[string]string{"v1": "bbb", "v2": "222"}, host: "aaa.bbb.ccc", path: "/111/222/333", + path_template: `/111/{v2:[0-9]{3}}/333`, + host_template: `aaa.{v1:[a-z]{3}}.ccc`, shouldMatch: false, }, { @@ -469,6 +508,8 @@ func TestHostPath(t *testing.T) { vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"}, host: "aaa.bbb.ccc", path: "/111/222/333", + path_template: `/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}`, + host_template: `{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}`, shouldMatch: true, }, { @@ -478,12 +519,15 @@ func TestHostPath(t *testing.T) { vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"}, host: "aaa.bbb.ccc", path: "/111/222/333", + path_template: `/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}`, + host_template: `{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}`, shouldMatch: false, }, } for _, test := range tests { testRoute(t, test) + testTemplate(t, test) } } @@ -541,6 +585,7 @@ func TestHeaders(t *testing.T) { for _, test := range tests { testRoute(t, test) + testTemplate(t, test) } } @@ -578,6 +623,7 @@ func TestMethods(t *testing.T) { for _, test := range tests { testRoute(t, test) + testTemplate(t, test) } } @@ -599,6 +645,8 @@ func TestQueries(t *testing.T) { vars: map[string]string{}, host: "", path: "", + path_template: `/api`, + host_template: `www.example.com`, shouldMatch: true, }, { @@ -608,6 +656,8 @@ func TestQueries(t *testing.T) { vars: map[string]string{}, host: "", path: "", + path_template: `/api`, + host_template: `www.example.com`, shouldMatch: true, }, { @@ -803,6 +853,7 @@ func TestQueries(t *testing.T) { for _, test := range tests { testRoute(t, test) + testTemplate(t, test) } } @@ -839,6 +890,7 @@ func TestSchemes(t *testing.T) { } for _, test := range tests { testRoute(t, test) + testTemplate(t, test) } } @@ -873,6 +925,7 @@ func TestMatcherFunc(t *testing.T) { for _, test := range tests { testRoute(t, test) + testTemplate(t, test) } } @@ -887,6 +940,7 @@ func TestBuildVarsFunc(t *testing.T) { }), request: newRequest("GET", "http://localhost/111/2"), path: "/111/3a", + path_template: `/111/{v1:\d}{v2:.*}`, shouldMatch: true, }, { @@ -900,12 +954,14 @@ func TestBuildVarsFunc(t *testing.T) { }), request: newRequest("GET", "http://localhost/1/a"), path: "/2/b", + path_template: `/{v1:\d}/{v2:\w}`, shouldMatch: true, }, } for _, test := range tests { testRoute(t, test) + testTemplate(t, test) } } @@ -920,6 +976,8 @@ func TestSubRouter(t *testing.T) { vars: map[string]string{"v1": "aaa", "v2": "bbb"}, host: "aaa.google.com", path: "/bbb", + path_template: `/{v2:[a-z]+}`, + host_template: `{v1:[a-z]+}.google.com`, shouldMatch: true, }, { @@ -928,6 +986,8 @@ func TestSubRouter(t *testing.T) { vars: map[string]string{"v1": "aaa", "v2": "bbb"}, host: "aaa.google.com", path: "/bbb", + path_template: `/{v2:[a-z]+}`, + host_template: `{v1:[a-z]+}.google.com`, shouldMatch: false, }, { @@ -936,6 +996,7 @@ func TestSubRouter(t *testing.T) { vars: map[string]string{"v1": "bar", "v2": "ding"}, host: "", path: "/foo/bar/baz/ding", + path_template: `/foo/{v1}/baz/{v2}`, shouldMatch: true, }, { @@ -944,12 +1005,14 @@ func TestSubRouter(t *testing.T) { vars: map[string]string{"v1": "bar", "v2": "ding"}, host: "", path: "/foo/bar/baz/ding", + path_template: `/foo/{v1}/baz/{v2}`, shouldMatch: false, }, } for _, test := range tests { testRoute(t, test) + testTemplate(t, test) } } @@ -1045,6 +1108,7 @@ func TestStrictSlash(t *testing.T) { for _, test := range tests { testRoute(t, test) + testTemplate(t, test) } } @@ -1152,14 +1216,13 @@ func TestSubrouterErrorHandling(t *testing.T) { // ---------------------------------------------------------------------------- func getRouteTemplate(route *Route) string { - host, path := "none", "none" - if route.regexp != nil { - if route.regexp.host != nil { - host = route.regexp.host.template - } - if route.regexp.path != nil { - path = route.regexp.path.template - } + host, err := route.GetHostTemplate() + if err != nil { + host = "none" + } + path, err := route.GetPathTemplate() + if err != nil { + path = "none" } return fmt.Sprintf("Host: %v, Path: %v", host, path) } @@ -1221,6 +1284,29 @@ func testRoute(t *testing.T, test routeTest) { } } +func testTemplate(t *testing.T, test routeTest) { + route := test.route + path_template := test.path_template + if len(path_template) == 0 { + path_template = test.path + } + host_template := test.host_template + if len(host_template) == 0 { + host_template = test.host + } + + path_tmpl, path_err := route.GetPathTemplate() + if path_err == nil && path_tmpl != path_template { + t.Errorf("(%v) GetPathTemplate not equal: expected %v, got %v", test.title, path_template, path_tmpl) + } + + + host_tmpl, host_err := route.GetHostTemplate() + if host_err == nil && host_tmpl != host_template { + t.Errorf("(%v) GetHostTemplate not equal: expected %v, got %v", test.title, host_template, host_tmpl) + } +} + // Tests that the context is cleared or not cleared properly depending on // the configuration of the router func TestKeepContext(t *testing.T) { diff --git a/route.go b/route.go index cb0e57d..37d0336 100644 --- a/route.go +++ b/route.go @@ -532,10 +532,11 @@ func (r *Route) URLPath(pairs ...string) (*url.URL, error) { }, nil } -// URLPathTemplate returns the template used to match against for the route -// +// GetPathTemplate and GetHostTemplate returns the template used to match against for the route +// This is userful for building simple REST API documentation, +// and instrumentation for services like New Relic to ensure consistent reporting // The route must have a path defined. -func (r *Route) URLPathTemplate() (string, error) { +func (r *Route) GetPathTemplate() (string, error) { if r.err != nil { return "", r.err } @@ -545,6 +546,16 @@ func (r *Route) URLPathTemplate() (string, error) { return r.regexp.path.template, nil } +func (r *Route) GetHostTemplate() (string, error) { + if r.err != nil { + return "", r.err + } + if r.regexp == nil || r.regexp.host == nil { + return "", errors.New("mux: route doesn't have a host") + } + return r.regexp.host.template, nil +} + // 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) { From f84ab9ab620f5ac2d587d5688b7b0820b5bdf9bf Mon Sep 17 00:00:00 2001 From: Dj Gilcrease Date: Sun, 28 Feb 2016 14:46:18 -0800 Subject: [PATCH 87/91] Fix go fmt issues --- mux_test.go | 545 ++++++++++++++++++++++++++-------------------------- 1 file changed, 272 insertions(+), 273 deletions(-) diff --git a/mux_test.go b/mux_test.go index 6fe249a..8912d09 100644 --- a/mux_test.go +++ b/mux_test.go @@ -32,8 +32,8 @@ type routeTest struct { vars map[string]string // the expected vars of the match host string // the expected host of the match path string // the expected path of the match - path_template string // the expected path template to match - host_template string // the expected host template to match + path_template string // the expected path template to match + host_template string // the expected host template to match shouldMatch bool // whether the request is expected to match the route at all shouldRedirect bool // whether the request should result in a redirect } @@ -115,124 +115,124 @@ func TestHost(t *testing.T) { shouldMatch: false, }, { - title: "Host route with pattern, match", - route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"), - request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), - vars: map[string]string{"v1": "bbb"}, - host: "aaa.bbb.ccc", - path: "", + title: "Host route with pattern, match", + route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"), + request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), + vars: map[string]string{"v1": "bbb"}, + host: "aaa.bbb.ccc", + path: "", host_template: `aaa.{v1:[a-z]{3}}.ccc`, - shouldMatch: true, + 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: "", + 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: "", host_template: `aaa.{v1:[a-z]{2}(b|c)}.ccc`, - shouldMatch: true, + shouldMatch: true, }, { - title: "Host route with pattern, wrong host in request URL", - route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"), - request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), - vars: map[string]string{"v1": "bbb"}, - host: "aaa.bbb.ccc", - path: "", + title: "Host route with pattern, wrong host in request URL", + route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"), + request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), + vars: map[string]string{"v1": "bbb"}, + host: "aaa.bbb.ccc", + path: "", host_template: `aaa.{v1:[a-z]{3}}.ccc`, - shouldMatch: false, + shouldMatch: false, }, { - title: "Host route with multiple patterns, match", - route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}"), - request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), - vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"}, - host: "aaa.bbb.ccc", - path: "", + title: "Host route with multiple patterns, match", + route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}"), + request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), + vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"}, + host: "aaa.bbb.ccc", + path: "", host_template: `{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}`, - shouldMatch: true, + shouldMatch: true, }, { - title: "Host route with multiple patterns, wrong host in request URL", - route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}"), - request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), - vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"}, - host: "aaa.bbb.ccc", - path: "", + title: "Host route with multiple patterns, wrong host in request URL", + route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}"), + request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), + vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"}, + host: "aaa.bbb.ccc", + path: "", host_template: `{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}`, - shouldMatch: false, + 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: "", + 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: "", host_template: `aaa.{v-1:[a-z]{3}}.ccc`, - shouldMatch: true, + 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: "", + 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: "", host_template: `aaa.{v-1:[a-z]{2}(b|c)}.ccc`, - shouldMatch: true, + 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: "", + 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: "", host_template: `{v-1:[a-z]{3}}.{v-2:[a-z]{3}}.{v-3:[a-z]{3}}`, - shouldMatch: true, + shouldMatch: true, }, { - title: "Path route with single pattern with pipe, match", - route: new(Route).Path("/{category:a|b/c}"), - request: newRequest("GET", "http://localhost/a"), - vars: map[string]string{"category": "a"}, - host: "", - path: "/a", + title: "Path route with single pattern with pipe, match", + route: new(Route).Path("/{category:a|b/c}"), + request: newRequest("GET", "http://localhost/a"), + vars: map[string]string{"category": "a"}, + host: "", + path: "/a", path_template: `/{category:a|b/c}`, - shouldMatch: true, + shouldMatch: true, }, { - title: "Path route with single pattern with pipe, match", - route: new(Route).Path("/{category:a|b/c}"), - request: newRequest("GET", "http://localhost/b/c"), - vars: map[string]string{"category": "b/c"}, - host: "", - path: "/b/c", + title: "Path route with single pattern with pipe, match", + route: new(Route).Path("/{category:a|b/c}"), + request: newRequest("GET", "http://localhost/b/c"), + vars: map[string]string{"category": "b/c"}, + host: "", + path: "/b/c", path_template: `/{category:a|b/c}`, - shouldMatch: true, + shouldMatch: true, }, { - 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", + 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", path_template: `/{category:a|b/c}/{product}/{id:[0-9]+}`, - shouldMatch: true, + shouldMatch: true, }, { - 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/b/c/product_name/1"), - vars: map[string]string{"category": "b/c", "product": "product_name", "id": "1"}, - host: "", - path: "/b/c/product_name/1", + 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/b/c/product_name/1"), + vars: map[string]string{"category": "b/c", "product": "product_name", "id": "1"}, + host: "", + path: "/b/c/product_name/1", path_template: `/{category:a|b/c}/{product}/{id:[0-9]+}`, - shouldMatch: true, + shouldMatch: true, }, } for _, test := range tests { @@ -262,24 +262,24 @@ func TestPath(t *testing.T) { shouldMatch: true, }, { - title: "Path route, do not match with trailing slash in path", - route: new(Route).Path("/111/"), - request: newRequest("GET", "http://localhost/111"), - vars: map[string]string{}, - host: "", - path: "/111", + title: "Path route, do not match with trailing slash in path", + route: new(Route).Path("/111/"), + request: newRequest("GET", "http://localhost/111"), + vars: map[string]string{}, + host: "", + path: "/111", path_template: `/111/`, - shouldMatch: false, + shouldMatch: false, }, { - title: "Path route, do not match with trailing slash in request", - route: new(Route).Path("/111"), - request: newRequest("GET", "http://localhost/111/"), - vars: map[string]string{}, - host: "", - path: "/111/", + title: "Path route, do not match with trailing slash in request", + route: new(Route).Path("/111"), + request: newRequest("GET", "http://localhost/111/"), + vars: map[string]string{}, + host: "", + path: "/111/", path_template: `/111`, - shouldMatch: false, + shouldMatch: false, }, { title: "Path route, wrong path in request in request URL", @@ -291,84 +291,84 @@ func TestPath(t *testing.T) { shouldMatch: false, }, { - title: "Path route with pattern, match", - route: new(Route).Path("/111/{v1:[0-9]{3}}/333"), - request: newRequest("GET", "http://localhost/111/222/333"), - vars: map[string]string{"v1": "222"}, - host: "", - path: "/111/222/333", + title: "Path route with pattern, match", + route: new(Route).Path("/111/{v1:[0-9]{3}}/333"), + request: newRequest("GET", "http://localhost/111/222/333"), + vars: map[string]string{"v1": "222"}, + host: "", + path: "/111/222/333", path_template: `/111/{v1:[0-9]{3}}/333`, - shouldMatch: true, + shouldMatch: true, }, { - title: "Path route with pattern, URL in request does not match", - route: new(Route).Path("/111/{v1:[0-9]{3}}/333"), - request: newRequest("GET", "http://localhost/111/aaa/333"), - vars: map[string]string{"v1": "222"}, - host: "", - path: "/111/222/333", + title: "Path route with pattern, URL in request does not match", + route: new(Route).Path("/111/{v1:[0-9]{3}}/333"), + request: newRequest("GET", "http://localhost/111/aaa/333"), + vars: map[string]string{"v1": "222"}, + host: "", + path: "/111/222/333", path_template: `/111/{v1:[0-9]{3}}/333`, - shouldMatch: false, + shouldMatch: false, }, { - title: "Path route with multiple patterns, match", - route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"), - request: newRequest("GET", "http://localhost/111/222/333"), - vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"}, - host: "", - path: "/111/222/333", + title: "Path route with multiple patterns, match", + route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"), + request: newRequest("GET", "http://localhost/111/222/333"), + vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"}, + host: "", + path: "/111/222/333", path_template: `/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}`, - shouldMatch: true, + shouldMatch: true, }, { - title: "Path route with multiple patterns, URL in request does not match", - route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"), - request: newRequest("GET", "http://localhost/111/aaa/333"), - vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"}, - host: "", - path: "/111/222/333", + title: "Path route with multiple patterns, URL in request does not match", + route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"), + request: newRequest("GET", "http://localhost/111/aaa/333"), + vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"}, + host: "", + path: "/111/222/333", path_template: `/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}`, - shouldMatch: false, + 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", + 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", path_template: `/{category:a|(b/c)}/{product}/{id:[0-9]+}`, - shouldMatch: true, + 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", + 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", path_template: `/111/{v-1:[0-9]{3}}/333`, - shouldMatch: true, + 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", + 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", path_template: `/{v-1:[0-9]{3}}/{v-2:[0-9]{3}}/{v-3:[0-9]{3}}`, - shouldMatch: true, + 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", + 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", path_template: `/{product-category:a|(b/c)}/{product-name}/{product-id:[0-9]+}`, - shouldMatch: true, + shouldMatch: true, }, } @@ -408,44 +408,44 @@ func TestPathPrefix(t *testing.T) { shouldMatch: false, }, { - title: "PathPrefix route with pattern, match", - route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"), - request: newRequest("GET", "http://localhost/111/222/333"), - vars: map[string]string{"v1": "222"}, - host: "", - path: "/111/222", + title: "PathPrefix route with pattern, match", + route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"), + request: newRequest("GET", "http://localhost/111/222/333"), + vars: map[string]string{"v1": "222"}, + host: "", + path: "/111/222", path_template: `/111/{v1:[0-9]{3}}`, - shouldMatch: true, + shouldMatch: true, }, { - title: "PathPrefix route with pattern, URL prefix in request does not match", - route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"), - request: newRequest("GET", "http://localhost/111/aaa/333"), - vars: map[string]string{"v1": "222"}, - host: "", - path: "/111/222", + title: "PathPrefix route with pattern, URL prefix in request does not match", + route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"), + request: newRequest("GET", "http://localhost/111/aaa/333"), + vars: map[string]string{"v1": "222"}, + host: "", + path: "/111/222", path_template: `/111/{v1:[0-9]{3}}`, - shouldMatch: false, + shouldMatch: false, }, { - title: "PathPrefix route with multiple patterns, match", - route: new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}"), - request: newRequest("GET", "http://localhost/111/222/333"), - vars: map[string]string{"v1": "111", "v2": "222"}, - host: "", - path: "/111/222", + title: "PathPrefix route with multiple patterns, match", + route: new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}"), + request: newRequest("GET", "http://localhost/111/222/333"), + vars: map[string]string{"v1": "111", "v2": "222"}, + host: "", + path: "/111/222", path_template: `/{v1:[0-9]{3}}/{v2:[0-9]{3}}`, - shouldMatch: true, + shouldMatch: true, }, { - title: "PathPrefix route with multiple patterns, URL prefix in request does not match", - route: new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}"), - request: newRequest("GET", "http://localhost/111/aaa/333"), - vars: map[string]string{"v1": "111", "v2": "222"}, - host: "", - path: "/111/222", + title: "PathPrefix route with multiple patterns, URL prefix in request does not match", + route: new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}"), + request: newRequest("GET", "http://localhost/111/aaa/333"), + vars: map[string]string{"v1": "111", "v2": "222"}, + host: "", + path: "/111/222", path_template: `/{v1:[0-9]{3}}/{v2:[0-9]{3}}`, - shouldMatch: false, + shouldMatch: false, }, } @@ -458,70 +458,70 @@ func TestPathPrefix(t *testing.T) { func TestHostPath(t *testing.T) { tests := []routeTest{ { - title: "Host and Path route, match", - route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"), - request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), - vars: map[string]string{}, - host: "", - path: "", + title: "Host and Path route, match", + route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"), + request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), + vars: map[string]string{}, + host: "", + path: "", path_template: `/111/222/333`, host_template: `aaa.bbb.ccc`, - shouldMatch: true, + shouldMatch: true, }, { - title: "Host and Path route, wrong host in request URL", - route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"), - request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), - vars: map[string]string{}, - host: "", - path: "", + title: "Host and Path route, wrong host in request URL", + route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"), + request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), + vars: map[string]string{}, + host: "", + path: "", path_template: `/111/222/333`, host_template: `aaa.bbb.ccc`, - shouldMatch: false, + shouldMatch: false, }, { - title: "Host and Path route with pattern, match", - route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), - request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), - vars: map[string]string{"v1": "bbb", "v2": "222"}, - host: "aaa.bbb.ccc", - path: "/111/222/333", + title: "Host and Path route with pattern, match", + route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), + request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), + vars: map[string]string{"v1": "bbb", "v2": "222"}, + host: "aaa.bbb.ccc", + path: "/111/222/333", path_template: `/111/{v2:[0-9]{3}}/333`, host_template: `aaa.{v1:[a-z]{3}}.ccc`, - shouldMatch: true, + shouldMatch: true, }, { - title: "Host and Path route with pattern, URL in request does not match", - route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), - request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), - vars: map[string]string{"v1": "bbb", "v2": "222"}, - host: "aaa.bbb.ccc", - path: "/111/222/333", + title: "Host and Path route with pattern, URL in request does not match", + route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), + request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), + vars: map[string]string{"v1": "bbb", "v2": "222"}, + host: "aaa.bbb.ccc", + path: "/111/222/333", path_template: `/111/{v2:[0-9]{3}}/333`, host_template: `aaa.{v1:[a-z]{3}}.ccc`, - shouldMatch: false, + shouldMatch: false, }, { - title: "Host and Path route with multiple patterns, match", - route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}"), - request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), - vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"}, - host: "aaa.bbb.ccc", - path: "/111/222/333", + title: "Host and Path route with multiple patterns, match", + route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}"), + request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), + vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"}, + host: "aaa.bbb.ccc", + path: "/111/222/333", path_template: `/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}`, host_template: `{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}`, - shouldMatch: true, + shouldMatch: true, }, { - title: "Host and Path route with multiple patterns, URL in request does not match", - route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}"), - request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), - vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"}, - host: "aaa.bbb.ccc", - path: "/111/222/333", + title: "Host and Path route with multiple patterns, URL in request does not match", + route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}"), + request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), + vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"}, + host: "aaa.bbb.ccc", + path: "/111/222/333", path_template: `/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}`, host_template: `{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}`, - shouldMatch: false, + shouldMatch: false, }, } @@ -639,26 +639,26 @@ func TestQueries(t *testing.T) { shouldMatch: true, }, { - title: "Queries route, match with a query string", - route: new(Route).Host("www.example.com").Path("/api").Queries("foo", "bar", "baz", "ding"), - request: newRequest("GET", "http://www.example.com/api?foo=bar&baz=ding"), - vars: map[string]string{}, - host: "", - path: "", + title: "Queries route, match with a query string", + route: new(Route).Host("www.example.com").Path("/api").Queries("foo", "bar", "baz", "ding"), + request: newRequest("GET", "http://www.example.com/api?foo=bar&baz=ding"), + vars: map[string]string{}, + host: "", + path: "", path_template: `/api`, host_template: `www.example.com`, - shouldMatch: true, + shouldMatch: true, }, { - title: "Queries route, match with a query string out of order", - route: new(Route).Host("www.example.com").Path("/api").Queries("foo", "bar", "baz", "ding"), - request: newRequest("GET", "http://www.example.com/api?baz=ding&foo=bar"), - vars: map[string]string{}, - host: "", - path: "", + title: "Queries route, match with a query string out of order", + route: new(Route).Host("www.example.com").Path("/api").Queries("foo", "bar", "baz", "ding"), + request: newRequest("GET", "http://www.example.com/api?baz=ding&foo=bar"), + vars: map[string]string{}, + host: "", + path: "", path_template: `/api`, host_template: `www.example.com`, - shouldMatch: true, + shouldMatch: true, }, { title: "Queries route, bad query", @@ -938,10 +938,10 @@ func TestBuildVarsFunc(t *testing.T) { vars["v2"] = "a" return vars }), - request: newRequest("GET", "http://localhost/111/2"), - path: "/111/3a", + request: newRequest("GET", "http://localhost/111/2"), + path: "/111/3a", path_template: `/111/{v1:\d}{v2:.*}`, - shouldMatch: true, + shouldMatch: true, }, { title: "BuildVarsFunc set on route and parent route", @@ -952,10 +952,10 @@ func TestBuildVarsFunc(t *testing.T) { vars["v2"] = "b" return vars }), - request: newRequest("GET", "http://localhost/1/a"), - path: "/2/b", + request: newRequest("GET", "http://localhost/1/a"), + path: "/2/b", path_template: `/{v1:\d}/{v2:\w}`, - shouldMatch: true, + shouldMatch: true, }, } @@ -971,42 +971,42 @@ func TestSubRouter(t *testing.T) { tests := []routeTest{ { - route: subrouter1.Path("/{v2:[a-z]+}"), - request: newRequest("GET", "http://aaa.google.com/bbb"), - vars: map[string]string{"v1": "aaa", "v2": "bbb"}, - host: "aaa.google.com", - path: "/bbb", + route: subrouter1.Path("/{v2:[a-z]+}"), + request: newRequest("GET", "http://aaa.google.com/bbb"), + vars: map[string]string{"v1": "aaa", "v2": "bbb"}, + host: "aaa.google.com", + path: "/bbb", path_template: `/{v2:[a-z]+}`, host_template: `{v1:[a-z]+}.google.com`, - shouldMatch: true, + shouldMatch: true, }, { - route: subrouter1.Path("/{v2:[a-z]+}"), - request: newRequest("GET", "http://111.google.com/111"), - vars: map[string]string{"v1": "aaa", "v2": "bbb"}, - host: "aaa.google.com", - path: "/bbb", + route: subrouter1.Path("/{v2:[a-z]+}"), + request: newRequest("GET", "http://111.google.com/111"), + vars: map[string]string{"v1": "aaa", "v2": "bbb"}, + host: "aaa.google.com", + path: "/bbb", path_template: `/{v2:[a-z]+}`, host_template: `{v1:[a-z]+}.google.com`, - shouldMatch: false, + shouldMatch: false, }, { - route: subrouter2.Path("/baz/{v2}"), - request: newRequest("GET", "http://localhost/foo/bar/baz/ding"), - vars: map[string]string{"v1": "bar", "v2": "ding"}, - host: "", - path: "/foo/bar/baz/ding", + route: subrouter2.Path("/baz/{v2}"), + request: newRequest("GET", "http://localhost/foo/bar/baz/ding"), + vars: map[string]string{"v1": "bar", "v2": "ding"}, + host: "", + path: "/foo/bar/baz/ding", path_template: `/foo/{v1}/baz/{v2}`, - shouldMatch: true, + shouldMatch: true, }, { - route: subrouter2.Path("/baz/{v2}"), - request: newRequest("GET", "http://localhost/foo/bar"), - vars: map[string]string{"v1": "bar", "v2": "ding"}, - host: "", - path: "/foo/bar/baz/ding", + route: subrouter2.Path("/baz/{v2}"), + request: newRequest("GET", "http://localhost/foo/bar"), + vars: map[string]string{"v1": "bar", "v2": "ding"}, + host: "", + path: "/foo/bar/baz/ding", path_template: `/foo/{v1}/baz/{v2}`, - shouldMatch: false, + shouldMatch: false, }, } @@ -1296,13 +1296,12 @@ func testTemplate(t *testing.T, test routeTest) { } path_tmpl, path_err := route.GetPathTemplate() - if path_err == nil && path_tmpl != path_template { + if path_err == nil && path_tmpl != path_template { t.Errorf("(%v) GetPathTemplate not equal: expected %v, got %v", test.title, path_template, path_tmpl) } - host_tmpl, host_err := route.GetHostTemplate() - if host_err == nil && host_tmpl != host_template { + if host_err == nil && host_tmpl != host_template { t.Errorf("(%v) GetHostTemplate not equal: expected %v, got %v", test.title, host_template, host_tmpl) } } From 02c98b3f736c94c45d11bef7b60023f752c2d21d Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Sun, 28 Feb 2016 19:31:51 -0800 Subject: [PATCH 88/91] [docs] Satisfied golint. - SkipRouter should also be ErrRouterSkipped (or similar) but a change would break the public API. --- doc.go | 2 +- mux.go | 2 +- old_test.go | 6 +++--- regexp.go | 19 ++++++++++--------- route.go | 6 ++++-- 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/doc.go b/doc.go index 49798cb..835f534 100644 --- a/doc.go +++ b/doc.go @@ -3,7 +3,7 @@ // license that can be found in the LICENSE file. /* -Package gorilla/mux implements a request router and dispatcher. +Package 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 diff --git a/mux.go b/mux.go index aabe995..fbb7f19 100644 --- a/mux.go +++ b/mux.go @@ -236,7 +236,7 @@ func (r *Router) Schemes(schemes ...string) *Route { return r.NewRoute().Schemes(schemes...) } -// BuildVars registers a new route with a custom function for modifying +// BuildVarsFunc registers a new route with a custom function for modifying // route variables before building a URL. func (r *Router) BuildVarsFunc(f BuildVarsFunc) *Route { return r.NewRoute().BuildVarsFunc(f) diff --git a/old_test.go b/old_test.go index 755db48..25ccd49 100644 --- a/old_test.go +++ b/old_test.go @@ -576,10 +576,10 @@ func TestSubRouting(t *testing.T) { } u, _ := router.Get("products").URL() - builtUrl := u.String() + builtURL := u.String() // Yay, subroute aware of the domain when building! - if builtUrl != url { - t.Errorf("Expected %q, got %q.", url, builtUrl) + if builtURL != url { + t.Errorf("Expected %q, got %q.", url, builtURL) } } diff --git a/regexp.go b/regexp.go index 3c3a31b..16d5338 100644 --- a/regexp.go +++ b/regexp.go @@ -151,10 +151,11 @@ func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool { if !r.matchHost { if r.matchQuery { return r.matchQueryString(req) - } else { - return r.regexp.MatchString(req.URL.Path) } + + return r.regexp.MatchString(req.URL.Path) } + return r.regexp.MatchString(getHost(req)) } @@ -184,10 +185,10 @@ func (r *routeRegexp) url(values map[string]string) (string, error) { return rv, nil } -// getUrlQuery returns a single query parameter from a request URL. +// getURLQuery returns a single query parameter from a request URL. // For a URL with foo=bar&baz=ding, we return only the relevant key // value pair for the routeRegexp. -func (r *routeRegexp) getUrlQuery(req *http.Request) string { +func (r *routeRegexp) getURLQuery(req *http.Request) string { if !r.matchQuery { return "" } @@ -201,14 +202,14 @@ func (r *routeRegexp) getUrlQuery(req *http.Request) string { } func (r *routeRegexp) matchQueryString(req *http.Request) bool { - return r.regexp.MatchString(r.getUrlQuery(req)) + return r.regexp.MatchString(r.getURLQuery(req)) } // braceIndices returns the first level curly brace indices from a string. // It returns an error in case of unbalanced braces. func braceIndices(s string) ([]int, error) { var level, idx int - idxs := make([]int, 0) + var idxs []int for i := 0; i < len(s); i++ { switch s[i] { case '{': @@ -278,10 +279,10 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) } // Store query string variables. for _, q := range v.queries { - queryUrl := q.getUrlQuery(req) - matches := q.regexp.FindStringSubmatchIndex(queryUrl) + queryURL := q.getURLQuery(req) + matches := q.regexp.FindStringSubmatchIndex(queryURL) if len(matches) > 0 { - extractVars(queryUrl, matches, q.varsN, m.Vars) + extractVars(queryURL, matches, q.varsN, m.Vars) } } } diff --git a/route.go b/route.go index 913432c..224949d 100644 --- a/route.go +++ b/route.go @@ -217,8 +217,9 @@ 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 +// HeadersRegexp 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") @@ -263,6 +264,7 @@ func (r *Route) Host(tpl string) *Route { // MatcherFunc is the function signature used by custom matchers. type MatcherFunc func(*http.Request, *RouteMatch) bool +// Match returns the match for a given request. func (m MatcherFunc) Match(r *http.Request, match *RouteMatch) bool { return m(r, match) } From f7ac7251e3b615c63207d481413aab0726812499 Mon Sep 17 00:00:00 2001 From: Dj Gilcrease Date: Mon, 29 Feb 2016 07:54:15 -0800 Subject: [PATCH 89/91] Update doc strings based on feedback --- route.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/route.go b/route.go index 37d0336..6e63389 100644 --- a/route.go +++ b/route.go @@ -532,10 +532,11 @@ func (r *Route) URLPath(pairs ...string) (*url.URL, error) { }, nil } -// GetPathTemplate and GetHostTemplate returns the template used to match against for the route -// This is userful for building simple REST API documentation, -// and instrumentation for services like New Relic to ensure consistent reporting -// The route must have a path defined. +// GetPathTemplate returns the template used to build the +// route match. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not define a path. func (r *Route) GetPathTemplate() (string, error) { if r.err != nil { return "", r.err @@ -546,6 +547,11 @@ func (r *Route) GetPathTemplate() (string, error) { return r.regexp.path.template, nil } +// GetHostTemplate returns the template used to build the +// route match. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not define a host. func (r *Route) GetHostTemplate() (string, error) { if r.err != nil { return "", r.err From 65c2651643a9eb5438d10f1c492ba892fe96244f Mon Sep 17 00:00:00 2001 From: JP Robinson Date: Mon, 7 Mar 2016 16:38:03 -0500 Subject: [PATCH 90/91] fixing regexp changes from PR #144 --- mux_test.go | 10 ++++++++++ regexp.go | 7 ++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/mux_test.go b/mux_test.go index 8912d09..a44d03f 100644 --- a/mux_test.go +++ b/mux_test.go @@ -370,6 +370,16 @@ func TestPath(t *testing.T) { path_template: `/{product-category:a|(b/c)}/{product-name}/{product-id:[0-9]+}`, shouldMatch: true, }, + { + title: "Path route with multiple hyphenated names and patterns with pipe and case insensitive, match", + route: new(Route).Path("/{type:(?i:daily|mini|variety)}-{date:\\d{4,4}-\\d{2,2}-\\d{2,2}}"), + request: newRequest("GET", "http://localhost/daily-2016-01-01"), + vars: map[string]string{"type": "daily", "date": "2016-01-01"}, + host: "", + path: "/daily-2016-01-01", + path_template: `/{type:(?i:daily|mini|variety)}-{date:\d{4,4}-\d{2,2}-\d{2,2}}`, + shouldMatch: true, + }, } for _, test := range tests { diff --git a/regexp.go b/regexp.go index 16d5338..08710bc 100644 --- a/regexp.go +++ b/regexp.go @@ -73,11 +73,8 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash tpl[idxs[i]:end]) } // Build the regexp pattern. - 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) - } + fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), varGroupName(i/2), patt) + // Build the reverse template. fmt.Fprintf(reverse, "%s%%s", raw) From 8e05c69b5a3a77a2eafb48df5f230250d2752718 Mon Sep 17 00:00:00 2001 From: Raniere Fernandes de Medeiros Date: Thu, 17 Mar 2016 18:23:30 -0300 Subject: [PATCH 91/91] old_test: const DefaultRemoteAddr is unused. --- old_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/old_test.go b/old_test.go index 25ccd49..c385a25 100644 --- a/old_test.go +++ b/old_test.go @@ -36,10 +36,6 @@ func NewRecorder() *ResponseRecorder { } } -// DefaultRemoteAddr is the default remote address to return in RemoteAddr if -// an explicit DefaultRemoteAddr isn't set on ResponseRecorder. -const DefaultRemoteAddr = "1.2.3.4" - // Header returns the response headers. func (rw *ResponseRecorder) Header() http.Header { return rw.HeaderMap