├── .travis.yml ├── README.md ├── bench_test.go ├── LICENSE ├── doc.go ├── regexp.go ├── mux.go ├── route.go ├── old_test.go └── mux_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.0 5 | - 1.1 6 | - 1.2 7 | - tip 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mux 2 | === 3 | [![Build Status](https://travis-ci.org/gorilla/mux.png?branch=master)](https://travis-ci.org/gorilla/mux) 4 | 5 | gorilla/mux is a powerful URL router and dispatcher. 6 | 7 | Read the full documentation here: http://www.gorillatoolkit.org/pkg/mux 8 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package mux 6 | 7 | import ( 8 | "net/http" 9 | "testing" 10 | ) 11 | 12 | func BenchmarkMux(b *testing.B) { 13 | router := new(Router) 14 | handler := func(w http.ResponseWriter, r *http.Request) {} 15 | router.HandleFunc("/v1/{v1}", handler) 16 | 17 | request, _ := http.NewRequest("GET", "/v1/anything", nil) 18 | for i := 0; i < b.N; i++ { 19 | router.ServeHTTP(nil, request) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Rodrigo Moraes. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package gorilla/mux implements a request router and dispatcher. 7 | 8 | The name mux stands for "HTTP request multiplexer". Like the standard 9 | http.ServeMux, mux.Router matches incoming requests against a list of 10 | registered routes and calls a handler for the route that matches the URL 11 | or other conditions. The main features are: 12 | 13 | * Requests can be matched based on URL host, path, path prefix, schemes, 14 | header and query values, HTTP methods or using custom matchers. 15 | * URL hosts and paths can have variables with an optional regular 16 | expression. 17 | * Registered URLs can be built, or "reversed", which helps maintaining 18 | references to resources. 19 | * Routes can be used as subrouters: nested routes are only tested if the 20 | parent route matches. This is useful to define groups of routes that 21 | share common conditions like a host, a path prefix or other repeated 22 | attributes. As a bonus, this optimizes request matching. 23 | * It implements the http.Handler interface so it is compatible with the 24 | standard http.ServeMux. 25 | 26 | Let's start registering a couple of URL paths and handlers: 27 | 28 | func main() { 29 | r := mux.NewRouter() 30 | r.HandleFunc("/", HomeHandler) 31 | r.HandleFunc("/products", ProductsHandler) 32 | r.HandleFunc("/articles", ArticlesHandler) 33 | http.Handle("/", r) 34 | } 35 | 36 | Here we register three routes mapping URL paths to handlers. This is 37 | equivalent to how http.HandleFunc() works: if an incoming request URL matches 38 | one of the paths, the corresponding handler is called passing 39 | (http.ResponseWriter, *http.Request) as parameters. 40 | 41 | Paths can have variables. They are defined using the format {name} or 42 | {name:pattern}. If a regular expression pattern is not defined, the matched 43 | variable will be anything until the next slash. For example: 44 | 45 | r := mux.NewRouter() 46 | r.HandleFunc("/products/{key}", ProductHandler) 47 | r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler) 48 | r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) 49 | 50 | The names are used to create a map of route variables which can be retrieved 51 | calling mux.Vars(): 52 | 53 | vars := mux.Vars(request) 54 | category := vars["category"] 55 | 56 | And this is all you need to know about the basic usage. More advanced options 57 | are explained below. 58 | 59 | Routes can also be restricted to a domain or subdomain. Just define a host 60 | pattern to be matched. They can also have variables: 61 | 62 | r := mux.NewRouter() 63 | // Only matches if domain is "www.domain.com". 64 | r.Host("www.domain.com") 65 | // Matches a dynamic subdomain. 66 | r.Host("{subdomain:[a-z]+}.domain.com") 67 | 68 | There are several other matchers that can be added. To match path prefixes: 69 | 70 | r.PathPrefix("/products/") 71 | 72 | ...or HTTP methods: 73 | 74 | r.Methods("GET", "POST") 75 | 76 | ...or URL schemes: 77 | 78 | r.Schemes("https") 79 | 80 | ...or header values: 81 | 82 | r.Headers("X-Requested-With", "XMLHttpRequest") 83 | 84 | ...or query values: 85 | 86 | r.Queries("key", "value") 87 | 88 | ...or to use a custom matcher function: 89 | 90 | r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool { 91 | return r.ProtoMajor == 0 92 | }) 93 | 94 | ...and finally, it is possible to combine several matchers in a single route: 95 | 96 | r.HandleFunc("/products", ProductsHandler). 97 | Host("www.domain.com"). 98 | Methods("GET"). 99 | Schemes("http") 100 | 101 | Setting the same matching conditions again and again can be boring, so we have 102 | a way to group several routes that share the same requirements. 103 | We call it "subrouting". 104 | 105 | For example, let's say we have several URLs that should only match when the 106 | host is "www.domain.com". Create a route for that host and get a "subrouter" 107 | from it: 108 | 109 | r := mux.NewRouter() 110 | s := r.Host("www.domain.com").Subrouter() 111 | 112 | Then register routes in the subrouter: 113 | 114 | s.HandleFunc("/products/", ProductsHandler) 115 | s.HandleFunc("/products/{key}", ProductHandler) 116 | s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) 117 | 118 | The three URL paths we registered above will only be tested if the domain is 119 | "www.domain.com", because the subrouter is tested first. This is not 120 | only convenient, but also optimizes request matching. You can create 121 | subrouters combining any attribute matchers accepted by a route. 122 | 123 | Subrouters can be used to create domain or path "namespaces": you define 124 | subrouters in a central place and then parts of the app can register its 125 | paths relatively to a given subrouter. 126 | 127 | There's one more thing about subroutes. When a subrouter has a path prefix, 128 | the inner routes use it as base for their paths: 129 | 130 | r := mux.NewRouter() 131 | s := r.PathPrefix("/products").Subrouter() 132 | // "/products/" 133 | s.HandleFunc("/", ProductsHandler) 134 | // "/products/{key}/" 135 | s.HandleFunc("/{key}/", ProductHandler) 136 | // "/products/{key}/details" 137 | s.HandleFunc("/{key}/details", ProductDetailsHandler) 138 | 139 | Now let's see how to build registered URLs. 140 | 141 | Routes can be named. All routes that define a name can have their URLs built, 142 | or "reversed". We define a name calling Name() on a route. For example: 143 | 144 | r := mux.NewRouter() 145 | r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). 146 | Name("article") 147 | 148 | To build a URL, get the route and call the URL() method, passing a sequence of 149 | key/value pairs for the route variables. For the previous route, we would do: 150 | 151 | url, err := r.Get("article").URL("category", "technology", "id", "42") 152 | 153 | ...and the result will be a url.URL with the following path: 154 | 155 | "/articles/technology/42" 156 | 157 | This also works for host variables: 158 | 159 | r := mux.NewRouter() 160 | r.Host("{subdomain}.domain.com"). 161 | Path("/articles/{category}/{id:[0-9]+}"). 162 | HandlerFunc(ArticleHandler). 163 | Name("article") 164 | 165 | // url.String() will be "http://news.domain.com/articles/technology/42" 166 | url, err := r.Get("article").URL("subdomain", "news", 167 | "category", "technology", 168 | "id", "42") 169 | 170 | All variables defined in the route are required, and their values must 171 | conform to the corresponding patterns. These requirements guarantee that a 172 | generated URL will always match a registered route -- the only exception is 173 | for explicitly defined "build-only" routes which never match. 174 | 175 | There's also a way to build only the URL host or path for a route: 176 | use the methods URLHost() or URLPath() instead. For the previous route, 177 | we would do: 178 | 179 | // "http://news.domain.com/" 180 | host, err := r.Get("article").URLHost("subdomain", "news") 181 | 182 | // "/articles/technology/42" 183 | path, err := r.Get("article").URLPath("category", "technology", "id", "42") 184 | 185 | And if you use subrouters, host and path defined separately can be built 186 | as well: 187 | 188 | r := mux.NewRouter() 189 | s := r.Host("{subdomain}.domain.com").Subrouter() 190 | s.Path("/articles/{category}/{id:[0-9]+}"). 191 | HandlerFunc(ArticleHandler). 192 | Name("article") 193 | 194 | // "http://news.domain.com/articles/technology/42" 195 | url, err := r.Get("article").URL("subdomain", "news", 196 | "category", "technology", 197 | "id", "42") 198 | */ 199 | package mux 200 | -------------------------------------------------------------------------------- /regexp.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package mux 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "net/http" 11 | "net/url" 12 | "regexp" 13 | "strings" 14 | ) 15 | 16 | // newRouteRegexp parses a route template and returns a routeRegexp, 17 | // used to match a host or path. 18 | // 19 | // It will extract named variables, assemble a regexp to be matched, create 20 | // a "reverse" template to build URLs and compile regexps to validate variable 21 | // values used in URL building. 22 | // 23 | // Previously we accepted only Python-like identifiers for variable 24 | // names ([a-zA-Z_][a-zA-Z0-9_]*), but currently the only restriction is that 25 | // name and pattern can't be empty, and names can't contain a colon. 26 | func newRouteRegexp(tpl string, matchHost, matchPrefix, strictSlash bool) (*routeRegexp, error) { 27 | // Check if it is well-formed. 28 | idxs, errBraces := braceIndices(tpl) 29 | if errBraces != nil { 30 | return nil, errBraces 31 | } 32 | // Backup the original. 33 | template := tpl 34 | // Now let's parse it. 35 | defaultPattern := "[^/]+" 36 | if matchHost { 37 | defaultPattern = "[^.]+" 38 | matchPrefix, strictSlash = false, false 39 | } 40 | if matchPrefix { 41 | strictSlash = false 42 | } 43 | // Set a flag for strictSlash. 44 | endSlash := false 45 | if strictSlash && strings.HasSuffix(tpl, "/") { 46 | tpl = tpl[:len(tpl)-1] 47 | endSlash = true 48 | } 49 | varsN := make([]string, len(idxs)/2) 50 | varsR := make([]*regexp.Regexp, len(idxs)/2) 51 | pattern := bytes.NewBufferString("^") 52 | reverse := bytes.NewBufferString("") 53 | var end int 54 | var err error 55 | for i := 0; i < len(idxs); i += 2 { 56 | // Set all values we are interested in. 57 | raw := tpl[end:idxs[i]] 58 | end = idxs[i+1] 59 | parts := strings.SplitN(tpl[idxs[i]+1:end-1], ":", 2) 60 | name := parts[0] 61 | patt := defaultPattern 62 | if len(parts) == 2 { 63 | patt = parts[1] 64 | } 65 | // Name or pattern can't be empty. 66 | if name == "" || patt == "" { 67 | return nil, fmt.Errorf("mux: missing name or pattern in %q", 68 | tpl[idxs[i]:end]) 69 | } 70 | // Build the regexp pattern. 71 | fmt.Fprintf(pattern, "%s(%s)", regexp.QuoteMeta(raw), patt) 72 | // Build the reverse template. 73 | fmt.Fprintf(reverse, "%s%%s", raw) 74 | // Append variable name and compiled pattern. 75 | varsN[i/2] = name 76 | varsR[i/2], err = regexp.Compile(fmt.Sprintf("^%s$", patt)) 77 | if err != nil { 78 | return nil, err 79 | } 80 | } 81 | // Add the remaining. 82 | raw := tpl[end:] 83 | pattern.WriteString(regexp.QuoteMeta(raw)) 84 | if strictSlash { 85 | pattern.WriteString("[/]?") 86 | } 87 | if !matchPrefix { 88 | pattern.WriteByte('$') 89 | } 90 | reverse.WriteString(raw) 91 | if endSlash { 92 | reverse.WriteByte('/') 93 | } 94 | // Compile full regexp. 95 | reg, errCompile := regexp.Compile(pattern.String()) 96 | if errCompile != nil { 97 | return nil, errCompile 98 | } 99 | // Done! 100 | return &routeRegexp{ 101 | template: template, 102 | matchHost: matchHost, 103 | strictSlash: strictSlash, 104 | regexp: reg, 105 | reverse: reverse.String(), 106 | varsN: varsN, 107 | varsR: varsR, 108 | }, nil 109 | } 110 | 111 | // routeRegexp stores a regexp to match a host or path and information to 112 | // collect and validate route variables. 113 | type routeRegexp struct { 114 | // The unmodified template. 115 | template string 116 | // True for host match, false for path match. 117 | matchHost bool 118 | // The strictSlash value defined on the route, but disabled if PathPrefix was used. 119 | strictSlash bool 120 | // Expanded regexp. 121 | regexp *regexp.Regexp 122 | // Reverse template. 123 | reverse string 124 | // Variable names. 125 | varsN []string 126 | // Variable regexps (validators). 127 | varsR []*regexp.Regexp 128 | } 129 | 130 | // Match matches the regexp against the URL host or path. 131 | func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool { 132 | if !r.matchHost { 133 | return r.regexp.MatchString(req.URL.Path) 134 | } 135 | return r.regexp.MatchString(getHost(req)) 136 | } 137 | 138 | // url builds a URL part using the given values. 139 | func (r *routeRegexp) url(pairs ...string) (string, error) { 140 | values, err := mapFromPairs(pairs...) 141 | if err != nil { 142 | return "", err 143 | } 144 | urlValues := make([]interface{}, len(r.varsN)) 145 | for k, v := range r.varsN { 146 | value, ok := values[v] 147 | if !ok { 148 | return "", fmt.Errorf("mux: missing route variable %q", v) 149 | } 150 | urlValues[k] = value 151 | } 152 | rv := fmt.Sprintf(r.reverse, urlValues...) 153 | if !r.regexp.MatchString(rv) { 154 | // The URL is checked against the full regexp, instead of checking 155 | // individual variables. This is faster but to provide a good error 156 | // message, we check individual regexps if the URL doesn't match. 157 | for k, v := range r.varsN { 158 | if !r.varsR[k].MatchString(values[v]) { 159 | return "", fmt.Errorf( 160 | "mux: variable %q doesn't match, expected %q", values[v], 161 | r.varsR[k].String()) 162 | } 163 | } 164 | } 165 | return rv, nil 166 | } 167 | 168 | // braceIndices returns the first level curly brace indices from a string. 169 | // It returns an error in case of unbalanced braces. 170 | func braceIndices(s string) ([]int, error) { 171 | var level, idx int 172 | idxs := make([]int, 0) 173 | for i := 0; i < len(s); i++ { 174 | switch s[i] { 175 | case '{': 176 | if level++; level == 1 { 177 | idx = i 178 | } 179 | case '}': 180 | if level--; level == 0 { 181 | idxs = append(idxs, idx, i+1) 182 | } else if level < 0 { 183 | return nil, fmt.Errorf("mux: unbalanced braces in %q", s) 184 | } 185 | } 186 | } 187 | if level != 0 { 188 | return nil, fmt.Errorf("mux: unbalanced braces in %q", s) 189 | } 190 | return idxs, nil 191 | } 192 | 193 | // ---------------------------------------------------------------------------- 194 | // routeRegexpGroup 195 | // ---------------------------------------------------------------------------- 196 | 197 | // routeRegexpGroup groups the route matchers that carry variables. 198 | type routeRegexpGroup struct { 199 | host *routeRegexp 200 | path *routeRegexp 201 | } 202 | 203 | // setMatch extracts the variables from the URL once a route matches. 204 | func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) { 205 | // Store host variables. 206 | if v.host != nil { 207 | hostVars := v.host.regexp.FindStringSubmatch(getHost(req)) 208 | if hostVars != nil { 209 | for k, v := range v.host.varsN { 210 | m.Vars[v] = hostVars[k+1] 211 | } 212 | } 213 | } 214 | // Store path variables. 215 | if v.path != nil { 216 | pathVars := v.path.regexp.FindStringSubmatch(req.URL.Path) 217 | if pathVars != nil { 218 | for k, v := range v.path.varsN { 219 | m.Vars[v] = pathVars[k+1] 220 | } 221 | // Check if we should redirect. 222 | if v.path.strictSlash { 223 | p1 := strings.HasSuffix(req.URL.Path, "/") 224 | p2 := strings.HasSuffix(v.path.template, "/") 225 | if p1 != p2 { 226 | u, _ := url.Parse(req.URL.String()) 227 | if p1 { 228 | u.Path = u.Path[:len(u.Path)-1] 229 | } else { 230 | u.Path += "/" 231 | } 232 | m.Handler = http.RedirectHandler(u.String(), 301) 233 | } 234 | } 235 | } 236 | } 237 | } 238 | 239 | // getHost tries its best to return the request host. 240 | func getHost(r *http.Request) string { 241 | if !r.URL.IsAbs() { 242 | host := r.Host 243 | // Slice off any port information. 244 | if i := strings.Index(host, ":"); i != -1 { 245 | host = host[:i] 246 | } 247 | return host 248 | } 249 | return r.URL.Host 250 | } 251 | -------------------------------------------------------------------------------- /mux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package mux 6 | 7 | import ( 8 | "fmt" 9 | "net/http" 10 | "path" 11 | 12 | "github.com/gorilla/context" 13 | ) 14 | 15 | // NewRouter returns a new router instance. 16 | func NewRouter() *Router { 17 | return &Router{namedRoutes: make(map[string]*Route), KeepContext: false} 18 | } 19 | 20 | // Router registers routes to be matched and dispatches a handler. 21 | // 22 | // It implements the http.Handler interface, so it can be registered to serve 23 | // requests: 24 | // 25 | // var router = mux.NewRouter() 26 | // 27 | // func main() { 28 | // http.Handle("/", router) 29 | // } 30 | // 31 | // Or, for Google App Engine, register it in a init() function: 32 | // 33 | // func init() { 34 | // http.Handle("/", router) 35 | // } 36 | // 37 | // This will send all incoming requests to the router. 38 | type Router struct { 39 | // Configurable Handler to be used when no route matches. 40 | NotFoundHandler http.Handler 41 | // Parent route, if this is a subrouter. 42 | parent parentRoute 43 | // Routes to be matched, in order. 44 | routes []*Route 45 | // Routes by name for URL building. 46 | namedRoutes map[string]*Route 47 | // See Router.StrictSlash(). This defines the flag for new routes. 48 | strictSlash bool 49 | // If true, do not clear the request context after handling the request 50 | KeepContext bool 51 | } 52 | 53 | // Match matches registered routes against the request. 54 | func (r *Router) Match(req *http.Request, match *RouteMatch) bool { 55 | for _, route := range r.routes { 56 | if route.Match(req, match) { 57 | return true 58 | } 59 | } 60 | return false 61 | } 62 | 63 | // ServeHTTP dispatches the handler registered in the matched route. 64 | // 65 | // When there is a match, the route variables can be retrieved calling 66 | // mux.Vars(request). 67 | func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { 68 | // Clean path to canonical form and redirect. 69 | if p := cleanPath(req.URL.Path); p != req.URL.Path { 70 | 71 | // Added 3 lines (Philip Schlump) - It was droping the query string and #whatever from query. 72 | // This matches with fix in go 1.2 r.c. 4 for same problem. Go Issue: 73 | // http://code.google.com/p/go/issues/detail?id=5252 74 | url := *req.URL 75 | url.Path = p 76 | p = url.String() 77 | 78 | w.Header().Set("Location", p) 79 | w.WriteHeader(http.StatusMovedPermanently) 80 | return 81 | } 82 | var match RouteMatch 83 | var handler http.Handler 84 | if r.Match(req, &match) { 85 | handler = match.Handler 86 | setVars(req, match.Vars) 87 | setCurrentRoute(req, match.Route) 88 | } 89 | if handler == nil { 90 | if r.NotFoundHandler == nil { 91 | r.NotFoundHandler = http.NotFoundHandler() 92 | } 93 | handler = r.NotFoundHandler 94 | } 95 | if !r.KeepContext { 96 | defer context.Clear(req) 97 | } 98 | handler.ServeHTTP(w, req) 99 | } 100 | 101 | // Get returns a route registered with the given name. 102 | func (r *Router) Get(name string) *Route { 103 | return r.getNamedRoutes()[name] 104 | } 105 | 106 | // GetRoute returns a route registered with the given name. This method 107 | // was renamed to Get() and remains here for backwards compatibility. 108 | func (r *Router) GetRoute(name string) *Route { 109 | return r.getNamedRoutes()[name] 110 | } 111 | 112 | // StrictSlash defines the trailing slash behavior for new routes. The initial 113 | // value is false. 114 | // 115 | // When true, if the route path is "/path/", accessing "/path" will redirect 116 | // to the former and vice versa. In other words, your application will always 117 | // see the path as specified in the route. 118 | // 119 | // When false, if the route path is "/path", accessing "/path/" will not match 120 | // this route and vice versa. 121 | // 122 | // Special case: when a route sets a path prefix using the PathPrefix() method, 123 | // strict slash is ignored for that route because the redirect behavior can't 124 | // be determined from a prefix alone. However, any subrouters created from that 125 | // route inherit the original StrictSlash setting. 126 | func (r *Router) StrictSlash(value bool) *Router { 127 | r.strictSlash = value 128 | return r 129 | } 130 | 131 | // ---------------------------------------------------------------------------- 132 | // parentRoute 133 | // ---------------------------------------------------------------------------- 134 | 135 | // getNamedRoutes returns the map where named routes are registered. 136 | func (r *Router) getNamedRoutes() map[string]*Route { 137 | if r.namedRoutes == nil { 138 | if r.parent != nil { 139 | r.namedRoutes = r.parent.getNamedRoutes() 140 | } else { 141 | r.namedRoutes = make(map[string]*Route) 142 | } 143 | } 144 | return r.namedRoutes 145 | } 146 | 147 | // getRegexpGroup returns regexp definitions from the parent route, if any. 148 | func (r *Router) getRegexpGroup() *routeRegexpGroup { 149 | if r.parent != nil { 150 | return r.parent.getRegexpGroup() 151 | } 152 | return nil 153 | } 154 | 155 | // ---------------------------------------------------------------------------- 156 | // Route factories 157 | // ---------------------------------------------------------------------------- 158 | 159 | // NewRoute registers an empty route. 160 | func (r *Router) NewRoute() *Route { 161 | route := &Route{parent: r, strictSlash: r.strictSlash} 162 | r.routes = append(r.routes, route) 163 | return route 164 | } 165 | 166 | // Handle registers a new route with a matcher for the URL path. 167 | // See Route.Path() and Route.Handler(). 168 | func (r *Router) Handle(path string, handler http.Handler) *Route { 169 | return r.NewRoute().Path(path).Handler(handler) 170 | } 171 | 172 | // HandleFunc registers a new route with a matcher for the URL path. 173 | // See Route.Path() and Route.HandlerFunc(). 174 | func (r *Router) HandleFunc(path string, f func(http.ResponseWriter, 175 | *http.Request)) *Route { 176 | return r.NewRoute().Path(path).HandlerFunc(f) 177 | } 178 | 179 | // Headers registers a new route with a matcher for request header values. 180 | // See Route.Headers(). 181 | func (r *Router) Headers(pairs ...string) *Route { 182 | return r.NewRoute().Headers(pairs...) 183 | } 184 | 185 | // Host registers a new route with a matcher for the URL host. 186 | // See Route.Host(). 187 | func (r *Router) Host(tpl string) *Route { 188 | return r.NewRoute().Host(tpl) 189 | } 190 | 191 | // MatcherFunc registers a new route with a custom matcher function. 192 | // See Route.MatcherFunc(). 193 | func (r *Router) MatcherFunc(f MatcherFunc) *Route { 194 | return r.NewRoute().MatcherFunc(f) 195 | } 196 | 197 | // Methods registers a new route with a matcher for HTTP methods. 198 | // See Route.Methods(). 199 | func (r *Router) Methods(methods ...string) *Route { 200 | return r.NewRoute().Methods(methods...) 201 | } 202 | 203 | // Path registers a new route with a matcher for the URL path. 204 | // See Route.Path(). 205 | func (r *Router) Path(tpl string) *Route { 206 | return r.NewRoute().Path(tpl) 207 | } 208 | 209 | // PathPrefix registers a new route with a matcher for the URL path prefix. 210 | // See Route.PathPrefix(). 211 | func (r *Router) PathPrefix(tpl string) *Route { 212 | return r.NewRoute().PathPrefix(tpl) 213 | } 214 | 215 | // Queries registers a new route with a matcher for URL query values. 216 | // See Route.Queries(). 217 | func (r *Router) Queries(pairs ...string) *Route { 218 | return r.NewRoute().Queries(pairs...) 219 | } 220 | 221 | // Schemes registers a new route with a matcher for URL schemes. 222 | // See Route.Schemes(). 223 | func (r *Router) Schemes(schemes ...string) *Route { 224 | return r.NewRoute().Schemes(schemes...) 225 | } 226 | 227 | // ---------------------------------------------------------------------------- 228 | // Context 229 | // ---------------------------------------------------------------------------- 230 | 231 | // RouteMatch stores information about a matched route. 232 | type RouteMatch struct { 233 | Route *Route 234 | Handler http.Handler 235 | Vars map[string]string 236 | } 237 | 238 | type contextKey int 239 | 240 | const ( 241 | varsKey contextKey = iota 242 | routeKey 243 | ) 244 | 245 | // Vars returns the route variables for the current request, if any. 246 | func Vars(r *http.Request) map[string]string { 247 | if rv := context.Get(r, varsKey); rv != nil { 248 | return rv.(map[string]string) 249 | } 250 | return nil 251 | } 252 | 253 | // CurrentRoute returns the matched route for the current request, if any. 254 | func CurrentRoute(r *http.Request) *Route { 255 | if rv := context.Get(r, routeKey); rv != nil { 256 | return rv.(*Route) 257 | } 258 | return nil 259 | } 260 | 261 | func setVars(r *http.Request, val interface{}) { 262 | context.Set(r, varsKey, val) 263 | } 264 | 265 | func setCurrentRoute(r *http.Request, val interface{}) { 266 | context.Set(r, routeKey, val) 267 | } 268 | 269 | // ---------------------------------------------------------------------------- 270 | // Helpers 271 | // ---------------------------------------------------------------------------- 272 | 273 | // cleanPath returns the canonical path for p, eliminating . and .. elements. 274 | // Borrowed from the net/http package. 275 | func cleanPath(p string) string { 276 | if p == "" { 277 | return "/" 278 | } 279 | if p[0] != '/' { 280 | p = "/" + p 281 | } 282 | np := path.Clean(p) 283 | // path.Clean removes trailing slash except for root; 284 | // put the trailing slash back if necessary. 285 | if p[len(p)-1] == '/' && np != "/" { 286 | np += "/" 287 | } 288 | return np 289 | } 290 | 291 | // uniqueVars returns an error if two slices contain duplicated strings. 292 | func uniqueVars(s1, s2 []string) error { 293 | for _, v1 := range s1 { 294 | for _, v2 := range s2 { 295 | if v1 == v2 { 296 | return fmt.Errorf("mux: duplicated route variable %q", v2) 297 | } 298 | } 299 | } 300 | return nil 301 | } 302 | 303 | // mapFromPairs converts variadic string parameters to a string map. 304 | func mapFromPairs(pairs ...string) (map[string]string, error) { 305 | length := len(pairs) 306 | if length%2 != 0 { 307 | return nil, fmt.Errorf( 308 | "mux: number of parameters must be multiple of 2, got %v", pairs) 309 | } 310 | m := make(map[string]string, length/2) 311 | for i := 0; i < length; i += 2 { 312 | m[pairs[i]] = pairs[i+1] 313 | } 314 | return m, nil 315 | } 316 | 317 | // matchInArray returns true if the given string value is in the array. 318 | func matchInArray(arr []string, value string) bool { 319 | for _, v := range arr { 320 | if v == value { 321 | return true 322 | } 323 | } 324 | return false 325 | } 326 | 327 | // matchMap returns true if the given key/value pairs exist in a given map. 328 | func matchMap(toCheck map[string]string, toMatch map[string][]string, 329 | canonicalKey bool) bool { 330 | for k, v := range toCheck { 331 | // Check if key exists. 332 | if canonicalKey { 333 | k = http.CanonicalHeaderKey(k) 334 | } 335 | if values := toMatch[k]; values == nil { 336 | return false 337 | } else if v != "" { 338 | // If value was defined as an empty string we only check that the 339 | // key exists. Otherwise we also check for equality. 340 | valueExists := false 341 | for _, value := range values { 342 | if v == value { 343 | valueExists = true 344 | break 345 | } 346 | } 347 | if !valueExists { 348 | return false 349 | } 350 | } 351 | } 352 | return true 353 | } 354 | -------------------------------------------------------------------------------- /route.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package mux 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "net/http" 11 | "net/url" 12 | "strings" 13 | ) 14 | 15 | // Route stores information to match a request and build URLs. 16 | type Route struct { 17 | // Parent where the route was registered (a Router). 18 | parent parentRoute 19 | // Request handler for the route. 20 | handler http.Handler 21 | // List of matchers. 22 | matchers []matcher 23 | // Manager for the variables from host and path. 24 | regexp *routeRegexpGroup 25 | // If true, when the path pattern is "/path/", accessing "/path" will 26 | // redirect to the former and vice versa. 27 | strictSlash bool 28 | // If true, this route never matches: it is only used to build URLs. 29 | buildOnly bool 30 | // The name used to build URLs. 31 | name string 32 | // Error resulted from building a route. 33 | err error 34 | } 35 | 36 | // Match matches the route against the request. 37 | func (r *Route) Match(req *http.Request, match *RouteMatch) bool { 38 | if r.buildOnly || r.err != nil { 39 | return false 40 | } 41 | // Match everything. 42 | for _, m := range r.matchers { 43 | if matched := m.Match(req, match); !matched { 44 | return false 45 | } 46 | } 47 | // Yay, we have a match. Let's collect some info about it. 48 | if match.Route == nil { 49 | match.Route = r 50 | } 51 | if match.Handler == nil { 52 | match.Handler = r.handler 53 | } 54 | if match.Vars == nil { 55 | match.Vars = make(map[string]string) 56 | } 57 | // Set variables. 58 | if r.regexp != nil { 59 | r.regexp.setMatch(req, match, r) 60 | } 61 | return true 62 | } 63 | 64 | // ---------------------------------------------------------------------------- 65 | // Route attributes 66 | // ---------------------------------------------------------------------------- 67 | 68 | // GetError returns an error resulted from building the route, if any. 69 | func (r *Route) GetError() error { 70 | return r.err 71 | } 72 | 73 | // BuildOnly sets the route to never match: it is only used to build URLs. 74 | func (r *Route) BuildOnly() *Route { 75 | r.buildOnly = true 76 | return r 77 | } 78 | 79 | // Handler -------------------------------------------------------------------- 80 | 81 | // Handler sets a handler for the route. 82 | func (r *Route) Handler(handler http.Handler) *Route { 83 | if r.err == nil { 84 | r.handler = handler 85 | } 86 | return r 87 | } 88 | 89 | // HandlerFunc sets a handler function for the route. 90 | func (r *Route) HandlerFunc(f func(http.ResponseWriter, *http.Request)) *Route { 91 | return r.Handler(http.HandlerFunc(f)) 92 | } 93 | 94 | // GetHandler returns the handler for the route, if any. 95 | func (r *Route) GetHandler() http.Handler { 96 | return r.handler 97 | } 98 | 99 | // Name ----------------------------------------------------------------------- 100 | 101 | // Name sets the name for the route, used to build URLs. 102 | // If the name was registered already it will be overwritten. 103 | func (r *Route) Name(name string) *Route { 104 | if r.name != "" { 105 | r.err = fmt.Errorf("mux: route already has name %q, can't set %q", 106 | r.name, name) 107 | } 108 | if r.err == nil { 109 | r.name = name 110 | r.getNamedRoutes()[name] = r 111 | } 112 | return r 113 | } 114 | 115 | // GetName returns the name for the route, if any. 116 | func (r *Route) GetName() string { 117 | return r.name 118 | } 119 | 120 | // ---------------------------------------------------------------------------- 121 | // Matchers 122 | // ---------------------------------------------------------------------------- 123 | 124 | // matcher types try to match a request. 125 | type matcher interface { 126 | Match(*http.Request, *RouteMatch) bool 127 | } 128 | 129 | // addMatcher adds a matcher to the route. 130 | func (r *Route) addMatcher(m matcher) *Route { 131 | if r.err == nil { 132 | r.matchers = append(r.matchers, m) 133 | } 134 | return r 135 | } 136 | 137 | // addRegexpMatcher adds a host or path matcher and builder to a route. 138 | func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix bool) error { 139 | if r.err != nil { 140 | return r.err 141 | } 142 | r.regexp = r.getRegexpGroup() 143 | if !matchHost { 144 | if len(tpl) == 0 || tpl[0] != '/' { 145 | return fmt.Errorf("mux: path must start with a slash, got %q", tpl) 146 | } 147 | if r.regexp.path != nil { 148 | tpl = strings.TrimRight(r.regexp.path.template, "/") + tpl 149 | } 150 | } 151 | rr, err := newRouteRegexp(tpl, matchHost, matchPrefix, r.strictSlash) 152 | if err != nil { 153 | return err 154 | } 155 | if matchHost { 156 | if r.regexp.path != nil { 157 | if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil { 158 | return err 159 | } 160 | } 161 | r.regexp.host = rr 162 | } else { 163 | if r.regexp.host != nil { 164 | if err = uniqueVars(rr.varsN, r.regexp.host.varsN); err != nil { 165 | return err 166 | } 167 | } 168 | r.regexp.path = rr 169 | } 170 | r.addMatcher(rr) 171 | return nil 172 | } 173 | 174 | // Headers -------------------------------------------------------------------- 175 | 176 | // headerMatcher matches the request against header values. 177 | type headerMatcher map[string]string 178 | 179 | func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool { 180 | return matchMap(m, r.Header, true) 181 | } 182 | 183 | // Headers adds a matcher for request header values. 184 | // It accepts a sequence of key/value pairs to be matched. For example: 185 | // 186 | // r := mux.NewRouter() 187 | // r.Headers("Content-Type", "application/json", 188 | // "X-Requested-With", "XMLHttpRequest") 189 | // 190 | // The above route will only match if both request header values match. 191 | // 192 | // It the value is an empty string, it will match any value if the key is set. 193 | func (r *Route) Headers(pairs ...string) *Route { 194 | if r.err == nil { 195 | var headers map[string]string 196 | headers, r.err = mapFromPairs(pairs...) 197 | return r.addMatcher(headerMatcher(headers)) 198 | } 199 | return r 200 | } 201 | 202 | // Host ----------------------------------------------------------------------- 203 | 204 | // Host adds a matcher for the URL host. 205 | // It accepts a template with zero or more URL variables enclosed by {}. 206 | // Variables can define an optional regexp pattern to me matched: 207 | // 208 | // - {name} matches anything until the next dot. 209 | // 210 | // - {name:pattern} matches the given regexp pattern. 211 | // 212 | // For example: 213 | // 214 | // r := mux.NewRouter() 215 | // r.Host("www.domain.com") 216 | // r.Host("{subdomain}.domain.com") 217 | // r.Host("{subdomain:[a-z]+}.domain.com") 218 | // 219 | // Variable names must be unique in a given route. They can be retrieved 220 | // calling mux.Vars(request). 221 | func (r *Route) Host(tpl string) *Route { 222 | r.err = r.addRegexpMatcher(tpl, true, false) 223 | return r 224 | } 225 | 226 | // MatcherFunc ---------------------------------------------------------------- 227 | 228 | // MatcherFunc is the function signature used by custom matchers. 229 | type MatcherFunc func(*http.Request, *RouteMatch) bool 230 | 231 | func (m MatcherFunc) Match(r *http.Request, match *RouteMatch) bool { 232 | return m(r, match) 233 | } 234 | 235 | // MatcherFunc adds a custom function to be used as request matcher. 236 | func (r *Route) MatcherFunc(f MatcherFunc) *Route { 237 | return r.addMatcher(f) 238 | } 239 | 240 | // Methods -------------------------------------------------------------------- 241 | 242 | // methodMatcher matches the request against HTTP methods. 243 | type methodMatcher []string 244 | 245 | func (m methodMatcher) Match(r *http.Request, match *RouteMatch) bool { 246 | return matchInArray(m, r.Method) 247 | } 248 | 249 | // Methods adds a matcher for HTTP methods. 250 | // It accepts a sequence of one or more methods to be matched, e.g.: 251 | // "GET", "POST", "PUT". 252 | func (r *Route) Methods(methods ...string) *Route { 253 | for k, v := range methods { 254 | methods[k] = strings.ToUpper(v) 255 | } 256 | return r.addMatcher(methodMatcher(methods)) 257 | } 258 | 259 | // Path ----------------------------------------------------------------------- 260 | 261 | // Path adds a matcher for the URL path. 262 | // It accepts a template with zero or more URL variables enclosed by {}. The 263 | // template must start with a "/". 264 | // Variables can define an optional regexp pattern to me matched: 265 | // 266 | // - {name} matches anything until the next slash. 267 | // 268 | // - {name:pattern} matches the given regexp pattern. 269 | // 270 | // For example: 271 | // 272 | // r := mux.NewRouter() 273 | // r.Path("/products/").Handler(ProductsHandler) 274 | // r.Path("/products/{key}").Handler(ProductsHandler) 275 | // r.Path("/articles/{category}/{id:[0-9]+}"). 276 | // Handler(ArticleHandler) 277 | // 278 | // Variable names must be unique in a given route. They can be retrieved 279 | // calling mux.Vars(request). 280 | func (r *Route) Path(tpl string) *Route { 281 | r.err = r.addRegexpMatcher(tpl, false, false) 282 | return r 283 | } 284 | 285 | // PathPrefix ----------------------------------------------------------------- 286 | 287 | // PathPrefix adds a matcher for the URL path prefix. This matches if the given 288 | // template is a prefix of the full URL path. See Route.Path() for details on 289 | // the tpl argument. 290 | // 291 | // Note that it does not treat slashes specially ("/foobar/" will be matched by 292 | // the prefix "/foo") so you may want to use a trailing slash here. 293 | // 294 | // Also note that the setting of Router.StrictSlash() has no effect on routes 295 | // with a PathPrefix matcher. 296 | func (r *Route) PathPrefix(tpl string) *Route { 297 | r.err = r.addRegexpMatcher(tpl, false, true) 298 | return r 299 | } 300 | 301 | // Query ---------------------------------------------------------------------- 302 | 303 | // queryMatcher matches the request against URL queries. 304 | type queryMatcher map[string]string 305 | 306 | func (m queryMatcher) Match(r *http.Request, match *RouteMatch) bool { 307 | return matchMap(m, r.URL.Query(), false) 308 | } 309 | 310 | // Queries adds a matcher for URL query values. 311 | // It accepts a sequence of key/value pairs. For example: 312 | // 313 | // r := mux.NewRouter() 314 | // r.Queries("foo", "bar", "baz", "ding") 315 | // 316 | // The above route will only match if the URL contains the defined queries 317 | // values, e.g.: ?foo=bar&baz=ding. 318 | // 319 | // It the value is an empty string, it will match any value if the key is set. 320 | func (r *Route) Queries(pairs ...string) *Route { 321 | if r.err == nil { 322 | var queries map[string]string 323 | queries, r.err = mapFromPairs(pairs...) 324 | return r.addMatcher(queryMatcher(queries)) 325 | } 326 | return r 327 | } 328 | 329 | // Schemes -------------------------------------------------------------------- 330 | 331 | // schemeMatcher matches the request against URL schemes. 332 | type schemeMatcher []string 333 | 334 | func (m schemeMatcher) Match(r *http.Request, match *RouteMatch) bool { 335 | return matchInArray(m, r.URL.Scheme) 336 | } 337 | 338 | // Schemes adds a matcher for URL schemes. 339 | // It accepts a sequence of schemes to be matched, e.g.: "http", "https". 340 | func (r *Route) Schemes(schemes ...string) *Route { 341 | for k, v := range schemes { 342 | schemes[k] = strings.ToLower(v) 343 | } 344 | return r.addMatcher(schemeMatcher(schemes)) 345 | } 346 | 347 | // Subrouter ------------------------------------------------------------------ 348 | 349 | // Subrouter creates a subrouter for the route. 350 | // 351 | // It will test the inner routes only if the parent route matched. For example: 352 | // 353 | // r := mux.NewRouter() 354 | // s := r.Host("www.domain.com").Subrouter() 355 | // s.HandleFunc("/products/", ProductsHandler) 356 | // s.HandleFunc("/products/{key}", ProductHandler) 357 | // s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) 358 | // 359 | // Here, the routes registered in the subrouter won't be tested if the host 360 | // doesn't match. 361 | func (r *Route) Subrouter() *Router { 362 | router := &Router{parent: r, strictSlash: r.strictSlash} 363 | r.addMatcher(router) 364 | return router 365 | } 366 | 367 | // ---------------------------------------------------------------------------- 368 | // URL building 369 | // ---------------------------------------------------------------------------- 370 | 371 | // URL builds a URL for the route. 372 | // 373 | // It accepts a sequence of key/value pairs for the route variables. For 374 | // example, given this route: 375 | // 376 | // r := mux.NewRouter() 377 | // r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). 378 | // Name("article") 379 | // 380 | // ...a URL for it can be built using: 381 | // 382 | // url, err := r.Get("article").URL("category", "technology", "id", "42") 383 | // 384 | // ...which will return an url.URL with the following path: 385 | // 386 | // "/articles/technology/42" 387 | // 388 | // This also works for host variables: 389 | // 390 | // r := mux.NewRouter() 391 | // r.Host("{subdomain}.domain.com"). 392 | // HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). 393 | // Name("article") 394 | // 395 | // // url.String() will be "http://news.domain.com/articles/technology/42" 396 | // url, err := r.Get("article").URL("subdomain", "news", 397 | // "category", "technology", 398 | // "id", "42") 399 | // 400 | // All variables defined in the route are required, and their values must 401 | // conform to the corresponding patterns. 402 | func (r *Route) URL(pairs ...string) (*url.URL, error) { 403 | if r.err != nil { 404 | return nil, r.err 405 | } 406 | if r.regexp == nil { 407 | return nil, errors.New("mux: route doesn't have a host or path") 408 | } 409 | var scheme, host, path string 410 | var err error 411 | if r.regexp.host != nil { 412 | // Set a default scheme. 413 | scheme = "http" 414 | if host, err = r.regexp.host.url(pairs...); err != nil { 415 | return nil, err 416 | } 417 | } 418 | if r.regexp.path != nil { 419 | if path, err = r.regexp.path.url(pairs...); err != nil { 420 | return nil, err 421 | } 422 | } 423 | return &url.URL{ 424 | Scheme: scheme, 425 | Host: host, 426 | Path: path, 427 | }, nil 428 | } 429 | 430 | // URLHost builds the host part of the URL for a route. See Route.URL(). 431 | // 432 | // The route must have a host defined. 433 | func (r *Route) URLHost(pairs ...string) (*url.URL, error) { 434 | if r.err != nil { 435 | return nil, r.err 436 | } 437 | if r.regexp == nil || r.regexp.host == nil { 438 | return nil, errors.New("mux: route doesn't have a host") 439 | } 440 | host, err := r.regexp.host.url(pairs...) 441 | if err != nil { 442 | return nil, err 443 | } 444 | return &url.URL{ 445 | Scheme: "http", 446 | Host: host, 447 | }, nil 448 | } 449 | 450 | // URLPath builds the path part of the URL for a route. See Route.URL(). 451 | // 452 | // The route must have a path defined. 453 | func (r *Route) URLPath(pairs ...string) (*url.URL, error) { 454 | if r.err != nil { 455 | return nil, r.err 456 | } 457 | if r.regexp == nil || r.regexp.path == nil { 458 | return nil, errors.New("mux: route doesn't have a path") 459 | } 460 | path, err := r.regexp.path.url(pairs...) 461 | if err != nil { 462 | return nil, err 463 | } 464 | return &url.URL{ 465 | Path: path, 466 | }, nil 467 | } 468 | 469 | // ---------------------------------------------------------------------------- 470 | // parentRoute 471 | // ---------------------------------------------------------------------------- 472 | 473 | // parentRoute allows routes to know about parent host and path definitions. 474 | type parentRoute interface { 475 | getNamedRoutes() map[string]*Route 476 | getRegexpGroup() *routeRegexpGroup 477 | } 478 | 479 | // getNamedRoutes returns the map where named routes are registered. 480 | func (r *Route) getNamedRoutes() map[string]*Route { 481 | if r.parent == nil { 482 | // During tests router is not always set. 483 | r.parent = NewRouter() 484 | } 485 | return r.parent.getNamedRoutes() 486 | } 487 | 488 | // getRegexpGroup returns regexp definitions from this route. 489 | func (r *Route) getRegexpGroup() *routeRegexpGroup { 490 | if r.regexp == nil { 491 | if r.parent == nil { 492 | // During tests router is not always set. 493 | r.parent = NewRouter() 494 | } 495 | regexp := r.parent.getRegexpGroup() 496 | if regexp == nil { 497 | r.regexp = new(routeRegexpGroup) 498 | } else { 499 | // Copy. 500 | r.regexp = &routeRegexpGroup{ 501 | host: regexp.host, 502 | path: regexp.path, 503 | } 504 | } 505 | } 506 | return r.regexp 507 | } 508 | -------------------------------------------------------------------------------- /old_test.go: -------------------------------------------------------------------------------- 1 | // Old tests ported to Go1. This is a mess. Want to drop it one day. 2 | 3 | // Copyright 2011 Gorilla Authors. All rights reserved. 4 | // Use of this source code is governed by a BSD-style 5 | // license that can be found in the LICENSE file. 6 | 7 | package mux 8 | 9 | import ( 10 | "bytes" 11 | "net/http" 12 | "testing" 13 | ) 14 | 15 | // ---------------------------------------------------------------------------- 16 | // ResponseRecorder 17 | // ---------------------------------------------------------------------------- 18 | // Copyright 2009 The Go Authors. All rights reserved. 19 | // Use of this source code is governed by a BSD-style 20 | // license that can be found in the LICENSE file. 21 | 22 | // ResponseRecorder is an implementation of http.ResponseWriter that 23 | // records its mutations for later inspection in tests. 24 | type ResponseRecorder struct { 25 | Code int // the HTTP response code from WriteHeader 26 | HeaderMap http.Header // the HTTP response headers 27 | Body *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to 28 | Flushed bool 29 | } 30 | 31 | // NewRecorder returns an initialized ResponseRecorder. 32 | func NewRecorder() *ResponseRecorder { 33 | return &ResponseRecorder{ 34 | HeaderMap: make(http.Header), 35 | Body: new(bytes.Buffer), 36 | } 37 | } 38 | 39 | // DefaultRemoteAddr is the default remote address to return in RemoteAddr if 40 | // an explicit DefaultRemoteAddr isn't set on ResponseRecorder. 41 | const DefaultRemoteAddr = "1.2.3.4" 42 | 43 | // Header returns the response headers. 44 | func (rw *ResponseRecorder) Header() http.Header { 45 | return rw.HeaderMap 46 | } 47 | 48 | // Write always succeeds and writes to rw.Body, if not nil. 49 | func (rw *ResponseRecorder) Write(buf []byte) (int, error) { 50 | if rw.Body != nil { 51 | rw.Body.Write(buf) 52 | } 53 | if rw.Code == 0 { 54 | rw.Code = http.StatusOK 55 | } 56 | return len(buf), nil 57 | } 58 | 59 | // WriteHeader sets rw.Code. 60 | func (rw *ResponseRecorder) WriteHeader(code int) { 61 | rw.Code = code 62 | } 63 | 64 | // Flush sets rw.Flushed to true. 65 | func (rw *ResponseRecorder) Flush() { 66 | rw.Flushed = true 67 | } 68 | 69 | // ---------------------------------------------------------------------------- 70 | 71 | func TestRouteMatchers(t *testing.T) { 72 | var scheme, host, path, query, method string 73 | var headers map[string]string 74 | var resultVars map[bool]map[string]string 75 | 76 | router := NewRouter() 77 | router.NewRoute().Host("{var1}.google.com"). 78 | Path("/{var2:[a-z]+}/{var3:[0-9]+}"). 79 | Queries("foo", "bar"). 80 | Methods("GET"). 81 | Schemes("https"). 82 | Headers("x-requested-with", "XMLHttpRequest") 83 | router.NewRoute().Host("www.{var4}.com"). 84 | PathPrefix("/foo/{var5:[a-z]+}/{var6:[0-9]+}"). 85 | Queries("baz", "ding"). 86 | Methods("POST"). 87 | Schemes("http"). 88 | Headers("Content-Type", "application/json") 89 | 90 | reset := func() { 91 | // Everything match. 92 | scheme = "https" 93 | host = "www.google.com" 94 | path = "/product/42" 95 | query = "?foo=bar" 96 | method = "GET" 97 | headers = map[string]string{"X-Requested-With": "XMLHttpRequest"} 98 | resultVars = map[bool]map[string]string{ 99 | true: {"var1": "www", "var2": "product", "var3": "42"}, 100 | false: {}, 101 | } 102 | } 103 | 104 | reset2 := func() { 105 | // Everything match. 106 | scheme = "http" 107 | host = "www.google.com" 108 | path = "/foo/product/42/path/that/is/ignored" 109 | query = "?baz=ding" 110 | method = "POST" 111 | headers = map[string]string{"Content-Type": "application/json"} 112 | resultVars = map[bool]map[string]string{ 113 | true: {"var4": "google", "var5": "product", "var6": "42"}, 114 | false: {}, 115 | } 116 | } 117 | 118 | match := func(shouldMatch bool) { 119 | url := scheme + "://" + host + path + query 120 | request, _ := http.NewRequest(method, url, nil) 121 | for key, value := range headers { 122 | request.Header.Add(key, value) 123 | } 124 | 125 | var routeMatch RouteMatch 126 | matched := router.Match(request, &routeMatch) 127 | if matched != shouldMatch { 128 | // Need better messages. :) 129 | if matched { 130 | t.Errorf("Should match.") 131 | } else { 132 | t.Errorf("Should not match.") 133 | } 134 | } 135 | 136 | if matched { 137 | currentRoute := routeMatch.Route 138 | if currentRoute == nil { 139 | t.Errorf("Expected a current route.") 140 | } 141 | vars := routeMatch.Vars 142 | expectedVars := resultVars[shouldMatch] 143 | if len(vars) != len(expectedVars) { 144 | t.Errorf("Expected vars: %v Got: %v.", expectedVars, vars) 145 | } 146 | for name, value := range vars { 147 | if expectedVars[name] != value { 148 | t.Errorf("Expected vars: %v Got: %v.", expectedVars, vars) 149 | } 150 | } 151 | } 152 | } 153 | 154 | // 1st route -------------------------------------------------------------- 155 | 156 | // Everything match. 157 | reset() 158 | match(true) 159 | 160 | // Scheme doesn't match. 161 | reset() 162 | scheme = "http" 163 | match(false) 164 | 165 | // Host doesn't match. 166 | reset() 167 | host = "www.mygoogle.com" 168 | match(false) 169 | 170 | // Path doesn't match. 171 | reset() 172 | path = "/product/notdigits" 173 | match(false) 174 | 175 | // Query doesn't match. 176 | reset() 177 | query = "?foo=baz" 178 | match(false) 179 | 180 | // Method doesn't match. 181 | reset() 182 | method = "POST" 183 | match(false) 184 | 185 | // Header doesn't match. 186 | reset() 187 | headers = map[string]string{} 188 | match(false) 189 | 190 | // Everything match, again. 191 | reset() 192 | match(true) 193 | 194 | // 2nd route -------------------------------------------------------------- 195 | 196 | // Everything match. 197 | reset2() 198 | match(true) 199 | 200 | // Scheme doesn't match. 201 | reset2() 202 | scheme = "https" 203 | match(false) 204 | 205 | // Host doesn't match. 206 | reset2() 207 | host = "sub.google.com" 208 | match(false) 209 | 210 | // Path doesn't match. 211 | reset2() 212 | path = "/bar/product/42" 213 | match(false) 214 | 215 | // Query doesn't match. 216 | reset2() 217 | query = "?foo=baz" 218 | match(false) 219 | 220 | // Method doesn't match. 221 | reset2() 222 | method = "GET" 223 | match(false) 224 | 225 | // Header doesn't match. 226 | reset2() 227 | headers = map[string]string{} 228 | match(false) 229 | 230 | // Everything match, again. 231 | reset2() 232 | match(true) 233 | } 234 | 235 | type headerMatcherTest struct { 236 | matcher headerMatcher 237 | headers map[string]string 238 | result bool 239 | } 240 | 241 | var headerMatcherTests = []headerMatcherTest{ 242 | { 243 | matcher: headerMatcher(map[string]string{"x-requested-with": "XMLHttpRequest"}), 244 | headers: map[string]string{"X-Requested-With": "XMLHttpRequest"}, 245 | result: true, 246 | }, 247 | { 248 | matcher: headerMatcher(map[string]string{"x-requested-with": ""}), 249 | headers: map[string]string{"X-Requested-With": "anything"}, 250 | result: true, 251 | }, 252 | { 253 | matcher: headerMatcher(map[string]string{"x-requested-with": "XMLHttpRequest"}), 254 | headers: map[string]string{}, 255 | result: false, 256 | }, 257 | } 258 | 259 | type hostMatcherTest struct { 260 | matcher *Route 261 | url string 262 | vars map[string]string 263 | result bool 264 | } 265 | 266 | var hostMatcherTests = []hostMatcherTest{ 267 | { 268 | 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]}"), 269 | url: "http://abc.def.ghi/", 270 | vars: map[string]string{"foo": "abc", "bar": "def", "baz": "ghi"}, 271 | result: true, 272 | }, 273 | { 274 | 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]}"), 275 | url: "http://a.b.c/", 276 | vars: map[string]string{"foo": "abc", "bar": "def", "baz": "ghi"}, 277 | result: false, 278 | }, 279 | } 280 | 281 | type methodMatcherTest struct { 282 | matcher methodMatcher 283 | method string 284 | result bool 285 | } 286 | 287 | var methodMatcherTests = []methodMatcherTest{ 288 | { 289 | matcher: methodMatcher([]string{"GET", "POST", "PUT"}), 290 | method: "GET", 291 | result: true, 292 | }, 293 | { 294 | matcher: methodMatcher([]string{"GET", "POST", "PUT"}), 295 | method: "POST", 296 | result: true, 297 | }, 298 | { 299 | matcher: methodMatcher([]string{"GET", "POST", "PUT"}), 300 | method: "PUT", 301 | result: true, 302 | }, 303 | { 304 | matcher: methodMatcher([]string{"GET", "POST", "PUT"}), 305 | method: "DELETE", 306 | result: false, 307 | }, 308 | } 309 | 310 | type pathMatcherTest struct { 311 | matcher *Route 312 | url string 313 | vars map[string]string 314 | result bool 315 | } 316 | 317 | var pathMatcherTests = []pathMatcherTest{ 318 | { 319 | 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]}"), 320 | url: "http://localhost:8080/123/456/789", 321 | vars: map[string]string{"foo": "123", "bar": "456", "baz": "789"}, 322 | result: true, 323 | }, 324 | { 325 | 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]}"), 326 | url: "http://localhost:8080/1/2/3", 327 | vars: map[string]string{"foo": "123", "bar": "456", "baz": "789"}, 328 | result: false, 329 | }, 330 | } 331 | 332 | type queryMatcherTest struct { 333 | matcher queryMatcher 334 | url string 335 | result bool 336 | } 337 | 338 | var queryMatcherTests = []queryMatcherTest{ 339 | { 340 | matcher: queryMatcher(map[string]string{"foo": "bar", "baz": "ding"}), 341 | url: "http://localhost:8080/?foo=bar&baz=ding", 342 | result: true, 343 | }, 344 | { 345 | matcher: queryMatcher(map[string]string{"foo": "", "baz": ""}), 346 | url: "http://localhost:8080/?foo=anything&baz=anything", 347 | result: true, 348 | }, 349 | { 350 | matcher: queryMatcher(map[string]string{"foo": "ding", "baz": "bar"}), 351 | url: "http://localhost:8080/?foo=bar&baz=ding", 352 | result: false, 353 | }, 354 | { 355 | matcher: queryMatcher(map[string]string{"bar": "foo", "ding": "baz"}), 356 | url: "http://localhost:8080/?foo=bar&baz=ding", 357 | result: false, 358 | }, 359 | } 360 | 361 | type schemeMatcherTest struct { 362 | matcher schemeMatcher 363 | url string 364 | result bool 365 | } 366 | 367 | var schemeMatcherTests = []schemeMatcherTest{ 368 | { 369 | matcher: schemeMatcher([]string{"http", "https"}), 370 | url: "http://localhost:8080/", 371 | result: true, 372 | }, 373 | { 374 | matcher: schemeMatcher([]string{"http", "https"}), 375 | url: "https://localhost:8080/", 376 | result: true, 377 | }, 378 | { 379 | matcher: schemeMatcher([]string{"https"}), 380 | url: "http://localhost:8080/", 381 | result: false, 382 | }, 383 | { 384 | matcher: schemeMatcher([]string{"http"}), 385 | url: "https://localhost:8080/", 386 | result: false, 387 | }, 388 | } 389 | 390 | type urlBuildingTest struct { 391 | route *Route 392 | vars []string 393 | url string 394 | } 395 | 396 | var urlBuildingTests = []urlBuildingTest{ 397 | { 398 | route: new(Route).Host("foo.domain.com"), 399 | vars: []string{}, 400 | url: "http://foo.domain.com", 401 | }, 402 | { 403 | route: new(Route).Host("{subdomain}.domain.com"), 404 | vars: []string{"subdomain", "bar"}, 405 | url: "http://bar.domain.com", 406 | }, 407 | { 408 | route: new(Route).Host("foo.domain.com").Path("/articles"), 409 | vars: []string{}, 410 | url: "http://foo.domain.com/articles", 411 | }, 412 | { 413 | route: new(Route).Path("/articles"), 414 | vars: []string{}, 415 | url: "/articles", 416 | }, 417 | { 418 | route: new(Route).Path("/articles/{category}/{id:[0-9]+}"), 419 | vars: []string{"category", "technology", "id", "42"}, 420 | url: "/articles/technology/42", 421 | }, 422 | { 423 | route: new(Route).Host("{subdomain}.domain.com").Path("/articles/{category}/{id:[0-9]+}"), 424 | vars: []string{"subdomain", "foo", "category", "technology", "id", "42"}, 425 | url: "http://foo.domain.com/articles/technology/42", 426 | }, 427 | } 428 | 429 | func TestHeaderMatcher(t *testing.T) { 430 | for _, v := range headerMatcherTests { 431 | request, _ := http.NewRequest("GET", "http://localhost:8080/", nil) 432 | for key, value := range v.headers { 433 | request.Header.Add(key, value) 434 | } 435 | var routeMatch RouteMatch 436 | result := v.matcher.Match(request, &routeMatch) 437 | if result != v.result { 438 | if v.result { 439 | t.Errorf("%#v: should match %v.", v.matcher, request.Header) 440 | } else { 441 | t.Errorf("%#v: should not match %v.", v.matcher, request.Header) 442 | } 443 | } 444 | } 445 | } 446 | 447 | func TestHostMatcher(t *testing.T) { 448 | for _, v := range hostMatcherTests { 449 | request, _ := http.NewRequest("GET", v.url, nil) 450 | var routeMatch RouteMatch 451 | result := v.matcher.Match(request, &routeMatch) 452 | vars := routeMatch.Vars 453 | if result != v.result { 454 | if v.result { 455 | t.Errorf("%#v: should match %v.", v.matcher, v.url) 456 | } else { 457 | t.Errorf("%#v: should not match %v.", v.matcher, v.url) 458 | } 459 | } 460 | if result { 461 | if len(vars) != len(v.vars) { 462 | t.Errorf("%#v: vars length should be %v, got %v.", v.matcher, len(v.vars), len(vars)) 463 | } 464 | for name, value := range vars { 465 | if v.vars[name] != value { 466 | t.Errorf("%#v: expected value %v for key %v, got %v.", v.matcher, v.vars[name], name, value) 467 | } 468 | } 469 | } else { 470 | if len(vars) != 0 { 471 | t.Errorf("%#v: vars length should be 0, got %v.", v.matcher, len(vars)) 472 | } 473 | } 474 | } 475 | } 476 | 477 | func TestMethodMatcher(t *testing.T) { 478 | for _, v := range methodMatcherTests { 479 | request, _ := http.NewRequest(v.method, "http://localhost:8080/", nil) 480 | var routeMatch RouteMatch 481 | result := v.matcher.Match(request, &routeMatch) 482 | if result != v.result { 483 | if v.result { 484 | t.Errorf("%#v: should match %v.", v.matcher, v.method) 485 | } else { 486 | t.Errorf("%#v: should not match %v.", v.matcher, v.method) 487 | } 488 | } 489 | } 490 | } 491 | 492 | func TestPathMatcher(t *testing.T) { 493 | for _, v := range pathMatcherTests { 494 | request, _ := http.NewRequest("GET", v.url, nil) 495 | var routeMatch RouteMatch 496 | result := v.matcher.Match(request, &routeMatch) 497 | vars := routeMatch.Vars 498 | if result != v.result { 499 | if v.result { 500 | t.Errorf("%#v: should match %v.", v.matcher, v.url) 501 | } else { 502 | t.Errorf("%#v: should not match %v.", v.matcher, v.url) 503 | } 504 | } 505 | if result { 506 | if len(vars) != len(v.vars) { 507 | t.Errorf("%#v: vars length should be %v, got %v.", v.matcher, len(v.vars), len(vars)) 508 | } 509 | for name, value := range vars { 510 | if v.vars[name] != value { 511 | t.Errorf("%#v: expected value %v for key %v, got %v.", v.matcher, v.vars[name], name, value) 512 | } 513 | } 514 | } else { 515 | if len(vars) != 0 { 516 | t.Errorf("%#v: vars length should be 0, got %v.", v.matcher, len(vars)) 517 | } 518 | } 519 | } 520 | } 521 | 522 | func TestQueryMatcher(t *testing.T) { 523 | for _, v := range queryMatcherTests { 524 | request, _ := http.NewRequest("GET", v.url, nil) 525 | var routeMatch RouteMatch 526 | result := v.matcher.Match(request, &routeMatch) 527 | if result != v.result { 528 | if v.result { 529 | t.Errorf("%#v: should match %v.", v.matcher, v.url) 530 | } else { 531 | t.Errorf("%#v: should not match %v.", v.matcher, v.url) 532 | } 533 | } 534 | } 535 | } 536 | 537 | func TestSchemeMatcher(t *testing.T) { 538 | for _, v := range queryMatcherTests { 539 | request, _ := http.NewRequest("GET", v.url, nil) 540 | var routeMatch RouteMatch 541 | result := v.matcher.Match(request, &routeMatch) 542 | if result != v.result { 543 | if v.result { 544 | t.Errorf("%#v: should match %v.", v.matcher, v.url) 545 | } else { 546 | t.Errorf("%#v: should not match %v.", v.matcher, v.url) 547 | } 548 | } 549 | } 550 | } 551 | 552 | func TestUrlBuilding(t *testing.T) { 553 | 554 | for _, v := range urlBuildingTests { 555 | u, _ := v.route.URL(v.vars...) 556 | url := u.String() 557 | if url != v.url { 558 | t.Errorf("expected %v, got %v", v.url, url) 559 | /* 560 | reversePath := "" 561 | reverseHost := "" 562 | if v.route.pathTemplate != nil { 563 | reversePath = v.route.pathTemplate.Reverse 564 | } 565 | if v.route.hostTemplate != nil { 566 | reverseHost = v.route.hostTemplate.Reverse 567 | } 568 | 569 | t.Errorf("%#v:\nexpected: %q\ngot: %q\nreverse path: %q\nreverse host: %q", v.route, v.url, url, reversePath, reverseHost) 570 | */ 571 | } 572 | } 573 | 574 | ArticleHandler := func(w http.ResponseWriter, r *http.Request) { 575 | } 576 | 577 | router := NewRouter() 578 | router.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).Name("article") 579 | 580 | url, _ := router.Get("article").URL("category", "technology", "id", "42") 581 | expected := "/articles/technology/42" 582 | if url.String() != expected { 583 | t.Errorf("Expected %v, got %v", expected, url.String()) 584 | } 585 | } 586 | 587 | func TestMatchedRouteName(t *testing.T) { 588 | routeName := "stock" 589 | router := NewRouter() 590 | route := router.NewRoute().Path("/products/").Name(routeName) 591 | 592 | url := "http://www.domain.com/products/" 593 | request, _ := http.NewRequest("GET", url, nil) 594 | var rv RouteMatch 595 | ok := router.Match(request, &rv) 596 | 597 | if !ok || rv.Route != route { 598 | t.Errorf("Expected same route, got %+v.", rv.Route) 599 | } 600 | 601 | retName := rv.Route.GetName() 602 | if retName != routeName { 603 | t.Errorf("Expected %q, got %q.", routeName, retName) 604 | } 605 | } 606 | 607 | func TestSubRouting(t *testing.T) { 608 | // Example from docs. 609 | router := NewRouter() 610 | subrouter := router.NewRoute().Host("www.domain.com").Subrouter() 611 | route := subrouter.NewRoute().Path("/products/").Name("products") 612 | 613 | url := "http://www.domain.com/products/" 614 | request, _ := http.NewRequest("GET", url, nil) 615 | var rv RouteMatch 616 | ok := router.Match(request, &rv) 617 | 618 | if !ok || rv.Route != route { 619 | t.Errorf("Expected same route, got %+v.", rv.Route) 620 | } 621 | 622 | u, _ := router.Get("products").URL() 623 | builtUrl := u.String() 624 | // Yay, subroute aware of the domain when building! 625 | if builtUrl != url { 626 | t.Errorf("Expected %q, got %q.", url, builtUrl) 627 | } 628 | } 629 | 630 | func TestVariableNames(t *testing.T) { 631 | route := new(Route).Host("{arg1}.domain.com").Path("/{arg1}/{arg2:[0-9]+}") 632 | if route.err == nil { 633 | t.Errorf("Expected error for duplicated variable names") 634 | } 635 | } 636 | 637 | func TestRedirectSlash(t *testing.T) { 638 | var route *Route 639 | var routeMatch RouteMatch 640 | r := NewRouter() 641 | 642 | r.StrictSlash(false) 643 | route = r.NewRoute() 644 | if route.strictSlash != false { 645 | t.Errorf("Expected false redirectSlash.") 646 | } 647 | 648 | r.StrictSlash(true) 649 | route = r.NewRoute() 650 | if route.strictSlash != true { 651 | t.Errorf("Expected true redirectSlash.") 652 | } 653 | 654 | route = new(Route) 655 | route.strictSlash = true 656 | route.Path("/{arg1}/{arg2:[0-9]+}/") 657 | request, _ := http.NewRequest("GET", "http://localhost/foo/123", nil) 658 | routeMatch = RouteMatch{} 659 | _ = route.Match(request, &routeMatch) 660 | vars := routeMatch.Vars 661 | if vars["arg1"] != "foo" { 662 | t.Errorf("Expected foo.") 663 | } 664 | if vars["arg2"] != "123" { 665 | t.Errorf("Expected 123.") 666 | } 667 | rsp := NewRecorder() 668 | routeMatch.Handler.ServeHTTP(rsp, request) 669 | if rsp.HeaderMap.Get("Location") != "http://localhost/foo/123/" { 670 | t.Errorf("Expected redirect header.") 671 | } 672 | 673 | route = new(Route) 674 | route.strictSlash = true 675 | route.Path("/{arg1}/{arg2:[0-9]+}") 676 | request, _ = http.NewRequest("GET", "http://localhost/foo/123/", nil) 677 | routeMatch = RouteMatch{} 678 | _ = route.Match(request, &routeMatch) 679 | vars = routeMatch.Vars 680 | if vars["arg1"] != "foo" { 681 | t.Errorf("Expected foo.") 682 | } 683 | if vars["arg2"] != "123" { 684 | t.Errorf("Expected 123.") 685 | } 686 | rsp = NewRecorder() 687 | routeMatch.Handler.ServeHTTP(rsp, request) 688 | if rsp.HeaderMap.Get("Location") != "http://localhost/foo/123" { 689 | t.Errorf("Expected redirect header.") 690 | } 691 | } 692 | 693 | // Test for the new regexp library, still not available in stable Go. 694 | func TestNewRegexp(t *testing.T) { 695 | var p *routeRegexp 696 | var matches []string 697 | 698 | tests := map[string]map[string][]string{ 699 | "/{foo:a{2}}": { 700 | "/a": nil, 701 | "/aa": {"aa"}, 702 | "/aaa": nil, 703 | "/aaaa": nil, 704 | }, 705 | "/{foo:a{2,}}": { 706 | "/a": nil, 707 | "/aa": {"aa"}, 708 | "/aaa": {"aaa"}, 709 | "/aaaa": {"aaaa"}, 710 | }, 711 | "/{foo:a{2,3}}": { 712 | "/a": nil, 713 | "/aa": {"aa"}, 714 | "/aaa": {"aaa"}, 715 | "/aaaa": nil, 716 | }, 717 | "/{foo:[a-z]{3}}/{bar:[a-z]{2}}": { 718 | "/a": nil, 719 | "/ab": nil, 720 | "/abc": nil, 721 | "/abcd": nil, 722 | "/abc/ab": {"abc", "ab"}, 723 | "/abc/abc": nil, 724 | "/abcd/ab": nil, 725 | }, 726 | `/{foo:\w{3,}}/{bar:\d{2,}}`: { 727 | "/a": nil, 728 | "/ab": nil, 729 | "/abc": nil, 730 | "/abc/1": nil, 731 | "/abc/12": {"abc", "12"}, 732 | "/abcd/12": {"abcd", "12"}, 733 | "/abcd/123": {"abcd", "123"}, 734 | }, 735 | } 736 | 737 | for pattern, paths := range tests { 738 | p, _ = newRouteRegexp(pattern, false, false, false) 739 | for path, result := range paths { 740 | matches = p.regexp.FindStringSubmatch(path) 741 | if result == nil { 742 | if matches != nil { 743 | t.Errorf("%v should not match %v.", pattern, path) 744 | } 745 | } else { 746 | if len(matches) != len(result)+1 { 747 | t.Errorf("Expected %v matches, got %v.", len(result)+1, len(matches)) 748 | } else { 749 | for k, v := range result { 750 | if matches[k+1] != v { 751 | t.Errorf("Expected %v, got %v.", v, matches[k+1]) 752 | } 753 | } 754 | } 755 | } 756 | } 757 | } 758 | } 759 | -------------------------------------------------------------------------------- /mux_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package mux 6 | 7 | import ( 8 | "fmt" 9 | "net/http" 10 | "testing" 11 | 12 | "github.com/gorilla/context" 13 | ) 14 | 15 | type routeTest struct { 16 | title string // title of the test 17 | route *Route // the route being tested 18 | request *http.Request // a request to test the route 19 | vars map[string]string // the expected vars of the match 20 | host string // the expected host of the match 21 | path string // the expected path of the match 22 | shouldMatch bool // whether the request is expected to match the route at all 23 | shouldRedirect bool // whether the request should result in a redirect 24 | } 25 | 26 | func TestHost(t *testing.T) { 27 | // newRequestHost a new request with a method, url, and host header 28 | newRequestHost := func(method, url, host string) *http.Request { 29 | req, err := http.NewRequest(method, url, nil) 30 | if err != nil { 31 | panic(err) 32 | } 33 | req.Host = host 34 | return req 35 | } 36 | 37 | tests := []routeTest{ 38 | { 39 | title: "Host route match", 40 | route: new(Route).Host("aaa.bbb.ccc"), 41 | request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), 42 | vars: map[string]string{}, 43 | host: "aaa.bbb.ccc", 44 | path: "", 45 | shouldMatch: true, 46 | }, 47 | { 48 | title: "Host route, wrong host in request URL", 49 | route: new(Route).Host("aaa.bbb.ccc"), 50 | request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), 51 | vars: map[string]string{}, 52 | host: "aaa.bbb.ccc", 53 | path: "", 54 | shouldMatch: false, 55 | }, 56 | { 57 | title: "Host route with port, match", 58 | route: new(Route).Host("aaa.bbb.ccc:1234"), 59 | request: newRequest("GET", "http://aaa.bbb.ccc:1234/111/222/333"), 60 | vars: map[string]string{}, 61 | host: "aaa.bbb.ccc:1234", 62 | path: "", 63 | shouldMatch: true, 64 | }, 65 | { 66 | title: "Host route with port, wrong port in request URL", 67 | route: new(Route).Host("aaa.bbb.ccc:1234"), 68 | request: newRequest("GET", "http://aaa.bbb.ccc:9999/111/222/333"), 69 | vars: map[string]string{}, 70 | host: "aaa.bbb.ccc:1234", 71 | path: "", 72 | shouldMatch: false, 73 | }, 74 | { 75 | title: "Host route, match with host in request header", 76 | route: new(Route).Host("aaa.bbb.ccc"), 77 | request: newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc"), 78 | vars: map[string]string{}, 79 | host: "aaa.bbb.ccc", 80 | path: "", 81 | shouldMatch: true, 82 | }, 83 | { 84 | title: "Host route, wrong host in request header", 85 | route: new(Route).Host("aaa.bbb.ccc"), 86 | request: newRequestHost("GET", "/111/222/333", "aaa.222.ccc"), 87 | vars: map[string]string{}, 88 | host: "aaa.bbb.ccc", 89 | path: "", 90 | shouldMatch: false, 91 | }, 92 | // 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}, 93 | { 94 | title: "Host route with port, wrong host in request header", 95 | route: new(Route).Host("aaa.bbb.ccc:1234"), 96 | request: newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc:9999"), 97 | vars: map[string]string{}, 98 | host: "aaa.bbb.ccc:1234", 99 | path: "", 100 | shouldMatch: false, 101 | }, 102 | { 103 | title: "Host route with pattern, match", 104 | route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"), 105 | request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), 106 | vars: map[string]string{"v1": "bbb"}, 107 | host: "aaa.bbb.ccc", 108 | path: "", 109 | shouldMatch: true, 110 | }, 111 | { 112 | title: "Host route with pattern, wrong host in request URL", 113 | route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"), 114 | request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), 115 | vars: map[string]string{"v1": "bbb"}, 116 | host: "aaa.bbb.ccc", 117 | path: "", 118 | shouldMatch: false, 119 | }, 120 | { 121 | title: "Host route with multiple patterns, match", 122 | route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}"), 123 | request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), 124 | vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"}, 125 | host: "aaa.bbb.ccc", 126 | path: "", 127 | shouldMatch: true, 128 | }, 129 | { 130 | title: "Host route with multiple patterns, wrong host in request URL", 131 | route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}"), 132 | request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), 133 | vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"}, 134 | host: "aaa.bbb.ccc", 135 | path: "", 136 | shouldMatch: false, 137 | }, 138 | } 139 | for _, test := range tests { 140 | testRoute(t, test) 141 | } 142 | } 143 | 144 | func TestPath(t *testing.T) { 145 | tests := []routeTest{ 146 | { 147 | title: "Path route, match", 148 | route: new(Route).Path("/111/222/333"), 149 | request: newRequest("GET", "http://localhost/111/222/333"), 150 | vars: map[string]string{}, 151 | host: "", 152 | path: "/111/222/333", 153 | shouldMatch: true, 154 | }, 155 | { 156 | title: "Path route, match with trailing slash in request and path", 157 | route: new(Route).Path("/111/"), 158 | request: newRequest("GET", "http://localhost/111/"), 159 | vars: map[string]string{}, 160 | host: "", 161 | path: "/111/", 162 | shouldMatch: true, 163 | }, 164 | { 165 | title: "Path route, do not match with trailing slash in path", 166 | route: new(Route).Path("/111/"), 167 | request: newRequest("GET", "http://localhost/111"), 168 | vars: map[string]string{}, 169 | host: "", 170 | path: "/111", 171 | shouldMatch: false, 172 | }, 173 | { 174 | title: "Path route, do not match with trailing slash in request", 175 | route: new(Route).Path("/111"), 176 | request: newRequest("GET", "http://localhost/111/"), 177 | vars: map[string]string{}, 178 | host: "", 179 | path: "/111/", 180 | shouldMatch: false, 181 | }, 182 | { 183 | title: "Path route, wrong path in request in request URL", 184 | route: new(Route).Path("/111/222/333"), 185 | request: newRequest("GET", "http://localhost/1/2/3"), 186 | vars: map[string]string{}, 187 | host: "", 188 | path: "/111/222/333", 189 | shouldMatch: false, 190 | }, 191 | { 192 | title: "Path route with pattern, match", 193 | route: new(Route).Path("/111/{v1:[0-9]{3}}/333"), 194 | request: newRequest("GET", "http://localhost/111/222/333"), 195 | vars: map[string]string{"v1": "222"}, 196 | host: "", 197 | path: "/111/222/333", 198 | shouldMatch: true, 199 | }, 200 | { 201 | title: "Path route with pattern, URL in request does not match", 202 | route: new(Route).Path("/111/{v1:[0-9]{3}}/333"), 203 | request: newRequest("GET", "http://localhost/111/aaa/333"), 204 | vars: map[string]string{"v1": "222"}, 205 | host: "", 206 | path: "/111/222/333", 207 | shouldMatch: false, 208 | }, 209 | { 210 | title: "Path route with multiple patterns, match", 211 | route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"), 212 | request: newRequest("GET", "http://localhost/111/222/333"), 213 | vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"}, 214 | host: "", 215 | path: "/111/222/333", 216 | shouldMatch: true, 217 | }, 218 | { 219 | title: "Path route with multiple patterns, URL in request does not match", 220 | route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"), 221 | request: newRequest("GET", "http://localhost/111/aaa/333"), 222 | vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"}, 223 | host: "", 224 | path: "/111/222/333", 225 | shouldMatch: false, 226 | }, 227 | } 228 | 229 | for _, test := range tests { 230 | testRoute(t, test) 231 | } 232 | } 233 | 234 | func TestPathPrefix(t *testing.T) { 235 | tests := []routeTest{ 236 | { 237 | title: "PathPrefix route, match", 238 | route: new(Route).PathPrefix("/111"), 239 | request: newRequest("GET", "http://localhost/111/222/333"), 240 | vars: map[string]string{}, 241 | host: "", 242 | path: "/111", 243 | shouldMatch: true, 244 | }, 245 | { 246 | title: "PathPrefix route, match substring", 247 | route: new(Route).PathPrefix("/1"), 248 | request: newRequest("GET", "http://localhost/111/222/333"), 249 | vars: map[string]string{}, 250 | host: "", 251 | path: "/1", 252 | shouldMatch: true, 253 | }, 254 | { 255 | title: "PathPrefix route, URL prefix in request does not match", 256 | route: new(Route).PathPrefix("/111"), 257 | request: newRequest("GET", "http://localhost/1/2/3"), 258 | vars: map[string]string{}, 259 | host: "", 260 | path: "/111", 261 | shouldMatch: false, 262 | }, 263 | { 264 | title: "PathPrefix route with pattern, match", 265 | route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"), 266 | request: newRequest("GET", "http://localhost/111/222/333"), 267 | vars: map[string]string{"v1": "222"}, 268 | host: "", 269 | path: "/111/222", 270 | shouldMatch: true, 271 | }, 272 | { 273 | title: "PathPrefix route with pattern, URL prefix in request does not match", 274 | route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"), 275 | request: newRequest("GET", "http://localhost/111/aaa/333"), 276 | vars: map[string]string{"v1": "222"}, 277 | host: "", 278 | path: "/111/222", 279 | shouldMatch: false, 280 | }, 281 | { 282 | title: "PathPrefix route with multiple patterns, match", 283 | route: new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}"), 284 | request: newRequest("GET", "http://localhost/111/222/333"), 285 | vars: map[string]string{"v1": "111", "v2": "222"}, 286 | host: "", 287 | path: "/111/222", 288 | shouldMatch: true, 289 | }, 290 | { 291 | title: "PathPrefix route with multiple patterns, URL prefix in request does not match", 292 | route: new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}"), 293 | request: newRequest("GET", "http://localhost/111/aaa/333"), 294 | vars: map[string]string{"v1": "111", "v2": "222"}, 295 | host: "", 296 | path: "/111/222", 297 | shouldMatch: false, 298 | }, 299 | } 300 | 301 | for _, test := range tests { 302 | testRoute(t, test) 303 | } 304 | } 305 | 306 | func TestHostPath(t *testing.T) { 307 | tests := []routeTest{ 308 | { 309 | title: "Host and Path route, match", 310 | route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"), 311 | request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), 312 | vars: map[string]string{}, 313 | host: "", 314 | path: "", 315 | shouldMatch: true, 316 | }, 317 | { 318 | title: "Host and Path route, wrong host in request URL", 319 | route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"), 320 | request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), 321 | vars: map[string]string{}, 322 | host: "", 323 | path: "", 324 | shouldMatch: false, 325 | }, 326 | { 327 | title: "Host and Path route with pattern, match", 328 | route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), 329 | request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), 330 | vars: map[string]string{"v1": "bbb", "v2": "222"}, 331 | host: "aaa.bbb.ccc", 332 | path: "/111/222/333", 333 | shouldMatch: true, 334 | }, 335 | { 336 | title: "Host and Path route with pattern, URL in request does not match", 337 | route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), 338 | request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), 339 | vars: map[string]string{"v1": "bbb", "v2": "222"}, 340 | host: "aaa.bbb.ccc", 341 | path: "/111/222/333", 342 | shouldMatch: false, 343 | }, 344 | { 345 | title: "Host and Path route with multiple patterns, match", 346 | 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}}"), 347 | request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), 348 | vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"}, 349 | host: "aaa.bbb.ccc", 350 | path: "/111/222/333", 351 | shouldMatch: true, 352 | }, 353 | { 354 | title: "Host and Path route with multiple patterns, URL in request does not match", 355 | 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}}"), 356 | request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), 357 | vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"}, 358 | host: "aaa.bbb.ccc", 359 | path: "/111/222/333", 360 | shouldMatch: false, 361 | }, 362 | } 363 | 364 | for _, test := range tests { 365 | testRoute(t, test) 366 | } 367 | } 368 | 369 | func TestHeaders(t *testing.T) { 370 | // newRequestHeaders creates a new request with a method, url, and headers 371 | newRequestHeaders := func(method, url string, headers map[string]string) *http.Request { 372 | req, err := http.NewRequest(method, url, nil) 373 | if err != nil { 374 | panic(err) 375 | } 376 | for k, v := range headers { 377 | req.Header.Add(k, v) 378 | } 379 | return req 380 | } 381 | 382 | tests := []routeTest{ 383 | { 384 | title: "Headers route, match", 385 | route: new(Route).Headers("foo", "bar", "baz", "ding"), 386 | request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "bar", "baz": "ding"}), 387 | vars: map[string]string{}, 388 | host: "", 389 | path: "", 390 | shouldMatch: true, 391 | }, 392 | { 393 | title: "Headers route, bad header values", 394 | route: new(Route).Headers("foo", "bar", "baz", "ding"), 395 | request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "bar", "baz": "dong"}), 396 | vars: map[string]string{}, 397 | host: "", 398 | path: "", 399 | shouldMatch: false, 400 | }, 401 | } 402 | 403 | for _, test := range tests { 404 | testRoute(t, test) 405 | } 406 | 407 | } 408 | 409 | func TestMethods(t *testing.T) { 410 | tests := []routeTest{ 411 | { 412 | title: "Methods route, match GET", 413 | route: new(Route).Methods("GET", "POST"), 414 | request: newRequest("GET", "http://localhost"), 415 | vars: map[string]string{}, 416 | host: "", 417 | path: "", 418 | shouldMatch: true, 419 | }, 420 | { 421 | title: "Methods route, match POST", 422 | route: new(Route).Methods("GET", "POST"), 423 | request: newRequest("POST", "http://localhost"), 424 | vars: map[string]string{}, 425 | host: "", 426 | path: "", 427 | shouldMatch: true, 428 | }, 429 | { 430 | title: "Methods route, bad method", 431 | route: new(Route).Methods("GET", "POST"), 432 | request: newRequest("PUT", "http://localhost"), 433 | vars: map[string]string{}, 434 | host: "", 435 | path: "", 436 | shouldMatch: false, 437 | }, 438 | } 439 | 440 | for _, test := range tests { 441 | testRoute(t, test) 442 | } 443 | } 444 | 445 | func TestQueries(t *testing.T) { 446 | tests := []routeTest{ 447 | { 448 | title: "Queries route, match", 449 | route: new(Route).Queries("foo", "bar", "baz", "ding"), 450 | request: newRequest("GET", "http://localhost?foo=bar&baz=ding"), 451 | vars: map[string]string{}, 452 | host: "", 453 | path: "", 454 | shouldMatch: true, 455 | }, 456 | { 457 | title: "Queries route, match with a query string", 458 | route: new(Route).Host("www.example.com").Path("/api").Queries("foo", "bar", "baz", "ding"), 459 | request: newRequest("GET", "http://www.example.com/api?foo=bar&baz=ding"), 460 | vars: map[string]string{}, 461 | host: "", 462 | path: "", 463 | shouldMatch: true, 464 | }, 465 | { 466 | title: "Queries route, bad query", 467 | route: new(Route).Queries("foo", "bar", "baz", "ding"), 468 | request: newRequest("GET", "http://localhost?foo=bar&baz=dong"), 469 | vars: map[string]string{}, 470 | host: "", 471 | path: "", 472 | shouldMatch: false, 473 | }, 474 | } 475 | 476 | for _, test := range tests { 477 | testRoute(t, test) 478 | } 479 | } 480 | 481 | func TestSchemes(t *testing.T) { 482 | tests := []routeTest{ 483 | // Schemes 484 | { 485 | title: "Schemes route, match https", 486 | route: new(Route).Schemes("https", "ftp"), 487 | request: newRequest("GET", "https://localhost"), 488 | vars: map[string]string{}, 489 | host: "", 490 | path: "", 491 | shouldMatch: true, 492 | }, 493 | { 494 | title: "Schemes route, match ftp", 495 | route: new(Route).Schemes("https", "ftp"), 496 | request: newRequest("GET", "ftp://localhost"), 497 | vars: map[string]string{}, 498 | host: "", 499 | path: "", 500 | shouldMatch: true, 501 | }, 502 | { 503 | title: "Schemes route, bad scheme", 504 | route: new(Route).Schemes("https", "ftp"), 505 | request: newRequest("GET", "http://localhost"), 506 | vars: map[string]string{}, 507 | host: "", 508 | path: "", 509 | shouldMatch: false, 510 | }, 511 | } 512 | for _, test := range tests { 513 | testRoute(t, test) 514 | } 515 | } 516 | 517 | func TestMatcherFunc(t *testing.T) { 518 | m := func(r *http.Request, m *RouteMatch) bool { 519 | if r.URL.Host == "aaa.bbb.ccc" { 520 | return true 521 | } 522 | return false 523 | } 524 | 525 | tests := []routeTest{ 526 | { 527 | title: "MatchFunc route, match", 528 | route: new(Route).MatcherFunc(m), 529 | request: newRequest("GET", "http://aaa.bbb.ccc"), 530 | vars: map[string]string{}, 531 | host: "", 532 | path: "", 533 | shouldMatch: true, 534 | }, 535 | { 536 | title: "MatchFunc route, non-match", 537 | route: new(Route).MatcherFunc(m), 538 | request: newRequest("GET", "http://aaa.222.ccc"), 539 | vars: map[string]string{}, 540 | host: "", 541 | path: "", 542 | shouldMatch: false, 543 | }, 544 | } 545 | 546 | for _, test := range tests { 547 | testRoute(t, test) 548 | } 549 | } 550 | 551 | func TestSubRouter(t *testing.T) { 552 | subrouter1 := new(Route).Host("{v1:[a-z]+}.google.com").Subrouter() 553 | subrouter2 := new(Route).PathPrefix("/foo/{v1}").Subrouter() 554 | 555 | tests := []routeTest{ 556 | { 557 | route: subrouter1.Path("/{v2:[a-z]+}"), 558 | request: newRequest("GET", "http://aaa.google.com/bbb"), 559 | vars: map[string]string{"v1": "aaa", "v2": "bbb"}, 560 | host: "aaa.google.com", 561 | path: "/bbb", 562 | shouldMatch: true, 563 | }, 564 | { 565 | route: subrouter1.Path("/{v2:[a-z]+}"), 566 | request: newRequest("GET", "http://111.google.com/111"), 567 | vars: map[string]string{"v1": "aaa", "v2": "bbb"}, 568 | host: "aaa.google.com", 569 | path: "/bbb", 570 | shouldMatch: false, 571 | }, 572 | { 573 | route: subrouter2.Path("/baz/{v2}"), 574 | request: newRequest("GET", "http://localhost/foo/bar/baz/ding"), 575 | vars: map[string]string{"v1": "bar", "v2": "ding"}, 576 | host: "", 577 | path: "/foo/bar/baz/ding", 578 | shouldMatch: true, 579 | }, 580 | { 581 | route: subrouter2.Path("/baz/{v2}"), 582 | request: newRequest("GET", "http://localhost/foo/bar"), 583 | vars: map[string]string{"v1": "bar", "v2": "ding"}, 584 | host: "", 585 | path: "/foo/bar/baz/ding", 586 | shouldMatch: false, 587 | }, 588 | } 589 | 590 | for _, test := range tests { 591 | testRoute(t, test) 592 | } 593 | } 594 | 595 | func TestNamedRoutes(t *testing.T) { 596 | r1 := NewRouter() 597 | r1.NewRoute().Name("a") 598 | r1.NewRoute().Name("b") 599 | r1.NewRoute().Name("c") 600 | 601 | r2 := r1.NewRoute().Subrouter() 602 | r2.NewRoute().Name("d") 603 | r2.NewRoute().Name("e") 604 | r2.NewRoute().Name("f") 605 | 606 | r3 := r2.NewRoute().Subrouter() 607 | r3.NewRoute().Name("g") 608 | r3.NewRoute().Name("h") 609 | r3.NewRoute().Name("i") 610 | 611 | if r1.namedRoutes == nil || len(r1.namedRoutes) != 9 { 612 | t.Errorf("Expected 9 named routes, got %v", r1.namedRoutes) 613 | } else if r1.Get("i") == nil { 614 | t.Errorf("Subroute name not registered") 615 | } 616 | } 617 | 618 | func TestStrictSlash(t *testing.T) { 619 | r := NewRouter() 620 | r.StrictSlash(true) 621 | 622 | tests := []routeTest{ 623 | { 624 | title: "Redirect path without slash", 625 | route: r.NewRoute().Path("/111/"), 626 | request: newRequest("GET", "http://localhost/111"), 627 | vars: map[string]string{}, 628 | host: "", 629 | path: "/111/", 630 | shouldMatch: true, 631 | shouldRedirect: true, 632 | }, 633 | { 634 | title: "Do not redirect path with slash", 635 | route: r.NewRoute().Path("/111/"), 636 | request: newRequest("GET", "http://localhost/111/"), 637 | vars: map[string]string{}, 638 | host: "", 639 | path: "/111/", 640 | shouldMatch: true, 641 | shouldRedirect: false, 642 | }, 643 | { 644 | title: "Redirect path with slash", 645 | route: r.NewRoute().Path("/111"), 646 | request: newRequest("GET", "http://localhost/111/"), 647 | vars: map[string]string{}, 648 | host: "", 649 | path: "/111", 650 | shouldMatch: true, 651 | shouldRedirect: true, 652 | }, 653 | { 654 | title: "Do not redirect path without slash", 655 | route: r.NewRoute().Path("/111"), 656 | request: newRequest("GET", "http://localhost/111"), 657 | vars: map[string]string{}, 658 | host: "", 659 | path: "/111", 660 | shouldMatch: true, 661 | shouldRedirect: false, 662 | }, 663 | { 664 | title: "Propagate StrictSlash to subrouters", 665 | route: r.NewRoute().PathPrefix("/static/").Subrouter().Path("/images/"), 666 | request: newRequest("GET", "http://localhost/static/images"), 667 | vars: map[string]string{}, 668 | host: "", 669 | path: "/static/images/", 670 | shouldMatch: true, 671 | shouldRedirect: true, 672 | }, 673 | { 674 | title: "Ignore StrictSlash for path prefix", 675 | route: r.NewRoute().PathPrefix("/static/"), 676 | request: newRequest("GET", "http://localhost/static/logo.png"), 677 | vars: map[string]string{}, 678 | host: "", 679 | path: "/static/", 680 | shouldMatch: true, 681 | shouldRedirect: false, 682 | }, 683 | } 684 | 685 | for _, test := range tests { 686 | testRoute(t, test) 687 | } 688 | } 689 | 690 | // ---------------------------------------------------------------------------- 691 | // Helpers 692 | // ---------------------------------------------------------------------------- 693 | 694 | func getRouteTemplate(route *Route) string { 695 | host, path := "none", "none" 696 | if route.regexp != nil { 697 | if route.regexp.host != nil { 698 | host = route.regexp.host.template 699 | } 700 | if route.regexp.path != nil { 701 | path = route.regexp.path.template 702 | } 703 | } 704 | return fmt.Sprintf("Host: %v, Path: %v", host, path) 705 | } 706 | 707 | func testRoute(t *testing.T, test routeTest) { 708 | request := test.request 709 | route := test.route 710 | vars := test.vars 711 | shouldMatch := test.shouldMatch 712 | host := test.host 713 | path := test.path 714 | url := test.host + test.path 715 | shouldRedirect := test.shouldRedirect 716 | 717 | var match RouteMatch 718 | ok := route.Match(request, &match) 719 | if ok != shouldMatch { 720 | msg := "Should match" 721 | if !shouldMatch { 722 | msg = "Should not match" 723 | } 724 | t.Errorf("(%v) %v:\nRoute: %#v\nRequest: %#v\nVars: %v\n", test.title, msg, route, request, vars) 725 | return 726 | } 727 | if shouldMatch { 728 | if test.vars != nil && !stringMapEqual(test.vars, match.Vars) { 729 | t.Errorf("(%v) Vars not equal: expected %v, got %v", test.title, vars, match.Vars) 730 | return 731 | } 732 | if host != "" { 733 | u, _ := test.route.URLHost(mapToPairs(match.Vars)...) 734 | if host != u.Host { 735 | t.Errorf("(%v) URLHost not equal: expected %v, got %v -- %v", test.title, host, u.Host, getRouteTemplate(route)) 736 | return 737 | } 738 | } 739 | if path != "" { 740 | u, _ := route.URLPath(mapToPairs(match.Vars)...) 741 | if path != u.Path { 742 | t.Errorf("(%v) URLPath not equal: expected %v, got %v -- %v", test.title, path, u.Path, getRouteTemplate(route)) 743 | return 744 | } 745 | } 746 | if url != "" { 747 | u, _ := route.URL(mapToPairs(match.Vars)...) 748 | if url != u.Host+u.Path { 749 | t.Errorf("(%v) URL not equal: expected %v, got %v -- %v", test.title, url, u.Host+u.Path, getRouteTemplate(route)) 750 | return 751 | } 752 | } 753 | if shouldRedirect && match.Handler == nil { 754 | t.Errorf("(%v) Did not redirect", test.title) 755 | return 756 | } 757 | if !shouldRedirect && match.Handler != nil { 758 | t.Errorf("(%v) Unexpected redirect", test.title) 759 | return 760 | } 761 | } 762 | } 763 | 764 | // Tests that the context is cleared or not cleared properly depending on 765 | // the configuration of the router 766 | func TestKeepContext(t *testing.T) { 767 | func1 := func(w http.ResponseWriter, r *http.Request) {} 768 | 769 | r := NewRouter() 770 | r.HandleFunc("/", func1).Name("func1") 771 | 772 | req, _ := http.NewRequest("GET", "http://localhost/", nil) 773 | context.Set(req, "t", 1) 774 | 775 | res := new(http.ResponseWriter) 776 | r.ServeHTTP(*res, req) 777 | 778 | if _, ok := context.GetOk(req, "t"); ok { 779 | t.Error("Context should have been cleared at end of request") 780 | } 781 | 782 | r.KeepContext = true 783 | 784 | req, _ = http.NewRequest("GET", "http://localhost/", nil) 785 | context.Set(req, "t", 1) 786 | 787 | r.ServeHTTP(*res, req) 788 | if _, ok := context.GetOk(req, "t"); !ok { 789 | t.Error("Context should NOT have been cleared at end of request") 790 | } 791 | 792 | } 793 | 794 | type TestA301ResponseWriter struct { 795 | hh http.Header 796 | status int 797 | } 798 | 799 | func (ho TestA301ResponseWriter) Header() http.Header { 800 | return http.Header(ho.hh) 801 | } 802 | 803 | func (ho TestA301ResponseWriter) Write(b []byte) (int, error) { 804 | return 0, nil 805 | } 806 | 807 | func (ho TestA301ResponseWriter) WriteHeader(code int) { 808 | ho.status = code 809 | } 810 | 811 | func Test301Redirect(t *testing.T) { 812 | m := make(http.Header) 813 | 814 | func1 := func(w http.ResponseWriter, r *http.Request) {} 815 | func2 := func(w http.ResponseWriter, r *http.Request) {} 816 | 817 | r := NewRouter() 818 | r.HandleFunc("/api/", func2).Name("func2") 819 | r.HandleFunc("/", func1).Name("func1") 820 | 821 | req, _ := http.NewRequest("GET", "http://localhost//api/?abc=def", nil) 822 | 823 | res := TestA301ResponseWriter{ 824 | hh: m, 825 | status: 0, 826 | } 827 | r.ServeHTTP(&res, req) 828 | 829 | if "http://localhost/api/?abc=def" != res.hh["Location"][0] { 830 | t.Errorf("Should have complete URL with query string") 831 | } 832 | } 833 | 834 | // https://plus.google.com/101022900381697718949/posts/eWy6DjFJ6uW 835 | func TestSubrouterHeader(t *testing.T) { 836 | expected := "func1 response" 837 | func1 := func(w http.ResponseWriter, r *http.Request) { 838 | fmt.Fprint(w, expected) 839 | } 840 | func2 := func(http.ResponseWriter, *http.Request) {} 841 | 842 | r := NewRouter() 843 | s := r.Headers("SomeSpecialHeader", "").Subrouter() 844 | s.HandleFunc("/", func1).Name("func1") 845 | r.HandleFunc("/", func2).Name("func2") 846 | 847 | req, _ := http.NewRequest("GET", "http://localhost/", nil) 848 | req.Header.Add("SomeSpecialHeader", "foo") 849 | match := new(RouteMatch) 850 | matched := r.Match(req, match) 851 | if !matched { 852 | t.Errorf("Should match request") 853 | } 854 | if match.Route.GetName() != "func1" { 855 | t.Errorf("Expecting func1 handler, got %s", match.Route.GetName()) 856 | } 857 | resp := NewRecorder() 858 | match.Handler.ServeHTTP(resp, req) 859 | if resp.Body.String() != expected { 860 | t.Errorf("Expecting %q", expected) 861 | } 862 | } 863 | 864 | // mapToPairs converts a string map to a slice of string pairs 865 | func mapToPairs(m map[string]string) []string { 866 | var i int 867 | p := make([]string, len(m)*2) 868 | for k, v := range m { 869 | p[i] = k 870 | p[i+1] = v 871 | i += 2 872 | } 873 | return p 874 | } 875 | 876 | // stringMapEqual checks the equality of two string maps 877 | func stringMapEqual(m1, m2 map[string]string) bool { 878 | nil1 := m1 == nil 879 | nil2 := m2 == nil 880 | if nil1 != nil2 || len(m1) != len(m2) { 881 | return false 882 | } 883 | for k, v := range m1 { 884 | if v != m2[k] { 885 | return false 886 | } 887 | } 888 | return true 889 | } 890 | 891 | // newRequest is a helper function to create a new request with a method and url 892 | func newRequest(method, url string) *http.Request { 893 | req, err := http.NewRequest(method, url, nil) 894 | if err != nil { 895 | panic(err) 896 | } 897 | return req 898 | } 899 | --------------------------------------------------------------------------------