master
commit
a17126e581
@ -0,0 +1,7 @@ |
||||
language: go |
||||
|
||||
go: |
||||
- 1.0 |
||||
- 1.1 |
||||
- 1.2 |
||||
- tip |
@ -0,0 +1,27 @@ |
||||
Copyright (c) 2012 Rodrigo Moraes. All rights reserved. |
||||
|
||||
Redistribution and use in source and binary forms, with or without |
||||
modification, are permitted provided that the following conditions are |
||||
met: |
||||
|
||||
* Redistributions of source code must retain the above copyright |
||||
notice, this list of conditions and the following disclaimer. |
||||
* Redistributions in binary form must reproduce the above |
||||
copyright notice, this list of conditions and the following disclaimer |
||||
in the documentation and/or other materials provided with the |
||||
distribution. |
||||
* Neither the name of Google Inc. nor the names of its |
||||
contributors may be used to endorse or promote products derived from |
||||
this software without specific prior written permission. |
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
@ -0,0 +1,7 @@ |
||||
mux |
||||
=== |
||||
[![Build Status](https://travis-ci.org/gorilla/mux.png?branch=master)](https://travis-ci.org/gorilla/mux) |
||||
|
||||
gorilla/mux is a powerful URL router and dispatcher. |
||||
|
||||
Read the full documentation here: http://www.gorillatoolkit.org/pkg/mux |
@ -0,0 +1,21 @@ |
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mux |
||||
|
||||
import ( |
||||
"net/http" |
||||
"testing" |
||||
) |
||||
|
||||
func BenchmarkMux(b *testing.B) { |
||||
router := new(Router) |
||||
handler := func(w http.ResponseWriter, r *http.Request) {} |
||||
router.HandleFunc("/v1/{v1}", handler) |
||||
|
||||
request, _ := http.NewRequest("GET", "/v1/anything", nil) |
||||
for i := 0; i < b.N; i++ { |
||||
router.ServeHTTP(nil, request) |
||||
} |
||||
} |
@ -0,0 +1,199 @@ |
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/* |
||||
Package gorilla/mux implements a request router and dispatcher. |
||||
|
||||
The name mux stands for "HTTP request multiplexer". Like the standard |
||||
http.ServeMux, mux.Router matches incoming requests against a list of |
||||
registered routes and calls a handler for the route that matches the URL |
||||
or other conditions. The main features are: |
||||
|
||||
* Requests can be matched based on URL host, path, path prefix, schemes, |
||||
header and query values, HTTP methods or using custom matchers. |
||||
* URL hosts and paths can have variables with an optional regular |
||||
expression. |
||||
* Registered URLs can be built, or "reversed", which helps maintaining |
||||
references to resources. |
||||
* Routes can be used as subrouters: nested routes are only tested if the |
||||
parent route matches. This is useful to define groups of routes that |
||||
share common conditions like a host, a path prefix or other repeated |
||||
attributes. As a bonus, this optimizes request matching. |
||||
* It implements the http.Handler interface so it is compatible with the |
||||
standard http.ServeMux. |
||||
|
||||
Let's start registering a couple of URL paths and handlers: |
||||
|
||||
func main() { |
||||
r := mux.NewRouter() |
||||
r.HandleFunc("/", HomeHandler) |
||||
r.HandleFunc("/products", ProductsHandler) |
||||
r.HandleFunc("/articles", ArticlesHandler) |
||||
http.Handle("/", r) |
||||
} |
||||
|
||||
Here we register three routes mapping URL paths to handlers. This is |
||||
equivalent to how http.HandleFunc() works: if an incoming request URL matches |
||||
one of the paths, the corresponding handler is called passing |
||||
(http.ResponseWriter, *http.Request) as parameters. |
||||
|
||||
Paths can have variables. They are defined using the format {name} or |
||||
{name:pattern}. If a regular expression pattern is not defined, the matched |
||||
variable will be anything until the next slash. For example: |
||||
|
||||
r := mux.NewRouter() |
||||
r.HandleFunc("/products/{key}", ProductHandler) |
||||
r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler) |
||||
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) |
||||
|
||||
The names are used to create a map of route variables which can be retrieved |
||||
calling mux.Vars(): |
||||
|
||||
vars := mux.Vars(request) |
||||
category := vars["category"] |
||||
|
||||
And this is all you need to know about the basic usage. More advanced options |
||||
are explained below. |
||||
|
||||
Routes can also be restricted to a domain or subdomain. Just define a host |
||||
pattern to be matched. They can also have variables: |
||||
|
||||
r := mux.NewRouter() |
||||
// Only matches if domain is "www.domain.com".
|
||||
r.Host("www.domain.com") |
||||
// Matches a dynamic subdomain.
|
||||
r.Host("{subdomain:[a-z]+}.domain.com") |
||||
|
||||
There are several other matchers that can be added. To match path prefixes: |
||||
|
||||
r.PathPrefix("/products/") |
||||
|
||||
...or HTTP methods: |
||||
|
||||
r.Methods("GET", "POST") |
||||
|
||||
...or URL schemes: |
||||
|
||||
r.Schemes("https") |
||||
|
||||
...or header values: |
||||
|
||||
r.Headers("X-Requested-With", "XMLHttpRequest") |
||||
|
||||
...or query values: |
||||
|
||||
r.Queries("key", "value") |
||||
|
||||
...or to use a custom matcher function: |
||||
|
||||
r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool { |
||||
return r.ProtoMajor == 0 |
||||
}) |
||||
|
||||
...and finally, it is possible to combine several matchers in a single route: |
||||
|
||||
r.HandleFunc("/products", ProductsHandler). |
||||
Host("www.domain.com"). |
||||
Methods("GET"). |
||||
Schemes("http") |
||||
|
||||
Setting the same matching conditions again and again can be boring, so we have |
||||
a way to group several routes that share the same requirements. |
||||
We call it "subrouting". |
||||
|
||||
For example, let's say we have several URLs that should only match when the |
||||
host is "www.domain.com". Create a route for that host and get a "subrouter" |
||||
from it: |
||||
|
||||
r := mux.NewRouter() |
||||
s := r.Host("www.domain.com").Subrouter() |
||||
|
||||
Then register routes in the subrouter: |
||||
|
||||
s.HandleFunc("/products/", ProductsHandler) |
||||
s.HandleFunc("/products/{key}", ProductHandler) |
||||
s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) |
||||
|
||||
The three URL paths we registered above will only be tested if the domain is |
||||
"www.domain.com", because the subrouter is tested first. This is not |
||||
only convenient, but also optimizes request matching. You can create |
||||
subrouters combining any attribute matchers accepted by a route. |
||||
|
||||
Subrouters can be used to create domain or path "namespaces": you define |
||||
subrouters in a central place and then parts of the app can register its |
||||
paths relatively to a given subrouter. |
||||
|
||||
There's one more thing about subroutes. When a subrouter has a path prefix, |
||||
the inner routes use it as base for their paths: |
||||
|
||||
r := mux.NewRouter() |
||||
s := r.PathPrefix("/products").Subrouter() |
||||
// "/products/"
|
||||
s.HandleFunc("/", ProductsHandler) |
||||
// "/products/{key}/"
|
||||
s.HandleFunc("/{key}/", ProductHandler) |
||||
// "/products/{key}/details"
|
||||
s.HandleFunc("/{key}/details", ProductDetailsHandler) |
||||
|
||||
Now let's see how to build registered URLs. |
||||
|
||||
Routes can be named. All routes that define a name can have their URLs built, |
||||
or "reversed". We define a name calling Name() on a route. For example: |
||||
|
||||
r := mux.NewRouter() |
||||
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). |
||||
Name("article") |
||||
|
||||
To build a URL, get the route and call the URL() method, passing a sequence of |
||||
key/value pairs for the route variables. For the previous route, we would do: |
||||
|
||||
url, err := r.Get("article").URL("category", "technology", "id", "42") |
||||
|
||||
...and the result will be a url.URL with the following path: |
||||
|
||||
"/articles/technology/42" |
||||
|
||||
This also works for host variables: |
||||
|
||||
r := mux.NewRouter() |
||||
r.Host("{subdomain}.domain.com"). |
||||
Path("/articles/{category}/{id:[0-9]+}"). |
||||
HandlerFunc(ArticleHandler). |
||||
Name("article") |
||||
|
||||
// url.String() will be "http://news.domain.com/articles/technology/42"
|
||||
url, err := r.Get("article").URL("subdomain", "news", |
||||
"category", "technology", |
||||
"id", "42") |
||||
|
||||
All variables defined in the route are required, and their values must |
||||
conform to the corresponding patterns. These requirements guarantee that a |
||||
generated URL will always match a registered route -- the only exception is |
||||
for explicitly defined "build-only" routes which never match. |
||||
|
||||
There's also a way to build only the URL host or path for a route: |
||||
use the methods URLHost() or URLPath() instead. For the previous route, |
||||
we would do: |
||||
|
||||
// "http://news.domain.com/"
|
||||
host, err := r.Get("article").URLHost("subdomain", "news") |
||||
|
||||
// "/articles/technology/42"
|
||||
path, err := r.Get("article").URLPath("category", "technology", "id", "42") |
||||
|
||||
And if you use subrouters, host and path defined separately can be built |
||||
as well: |
||||
|
||||
r := mux.NewRouter() |
||||
s := r.Host("{subdomain}.domain.com").Subrouter() |
||||
s.Path("/articles/{category}/{id:[0-9]+}"). |
||||
HandlerFunc(ArticleHandler). |
||||
Name("article") |
||||
|
||||
// "http://news.domain.com/articles/technology/42"
|
||||
url, err := r.Get("article").URL("subdomain", "news", |
||||
"category", "technology", |
||||
"id", "42") |
||||
*/ |
||||
package mux |
@ -0,0 +1,353 @@ |
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mux |
||||
|
||||
import ( |
||||
"fmt" |
||||
"net/http" |
||||
"path" |
||||
|
||||
"github.com/gorilla/context" |
||||
) |
||||
|
||||
// NewRouter returns a new router instance.
|
||||
func NewRouter() *Router { |
||||
return &Router{namedRoutes: make(map[string]*Route), KeepContext: false} |
||||
} |
||||
|
||||
// Router registers routes to be matched and dispatches a handler.
|
||||
//
|
||||
// It implements the http.Handler interface, so it can be registered to serve
|
||||
// requests:
|
||||
//
|
||||
// var router = mux.NewRouter()
|
||||
//
|
||||
// func main() {
|
||||
// http.Handle("/", router)
|
||||
// }
|
||||
//
|
||||
// Or, for Google App Engine, register it in a init() function:
|
||||
//
|
||||
// func init() {
|
||||
// http.Handle("/", router)
|
||||
// }
|
||||
//
|
||||
// This will send all incoming requests to the router.
|
||||
type Router struct { |
||||
// Configurable Handler to be used when no route matches.
|
||||
NotFoundHandler http.Handler |
||||
// Parent route, if this is a subrouter.
|
||||
parent parentRoute |
||||
// Routes to be matched, in order.
|
||||
routes []*Route |
||||
// Routes by name for URL building.
|
||||
namedRoutes map[string]*Route |
||||
// See Router.StrictSlash(). This defines the flag for new routes.
|
||||
strictSlash bool |
||||
// If true, do not clear the request context after handling the request
|
||||
KeepContext bool |
||||
} |
||||
|
||||
// Match matches registered routes against the request.
|
||||
func (r *Router) Match(req *http.Request, match *RouteMatch) bool { |
||||
for _, route := range r.routes { |
||||
if route.Match(req, match) { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
|
||||
// ServeHTTP dispatches the handler registered in the matched route.
|
||||
//
|
||||
// When there is a match, the route variables can be retrieved calling
|
||||
// mux.Vars(request).
|
||||
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { |
||||
// Clean path to canonical form and redirect.
|
||||
if p := cleanPath(req.URL.Path); p != req.URL.Path { |
||||
|
||||
// Added 3 lines (Philip Schlump) - It was droping the query string and #whatever from query.
|
||||
// This matches with fix in go 1.2 r.c. 4 for same problem. Go Issue:
|
||||
// http://code.google.com/p/go/issues/detail?id=5252
|
||||
url := *req.URL |
||||
url.Path = p |
||||
p = url.String() |
||||
|
||||
w.Header().Set("Location", p) |
||||
w.WriteHeader(http.StatusMovedPermanently) |
||||
return |
||||
} |
||||
var match RouteMatch |
||||
var handler http.Handler |
||||
if r.Match(req, &match) { |
||||
handler = match.Handler |
||||
setVars(req, match.Vars) |
||||
setCurrentRoute(req, match.Route) |
||||
} |
||||
if handler == nil { |
||||
handler = r.NotFoundHandler |
||||
if handler == nil { |
||||
handler = http.NotFoundHandler() |
||||
} |
||||
} |
||||
if !r.KeepContext { |
||||
defer context.Clear(req) |
||||
} |
||||
handler.ServeHTTP(w, req) |
||||
} |
||||
|
||||
// Get returns a route registered with the given name.
|
||||
func (r *Router) Get(name string) *Route { |
||||
return r.getNamedRoutes()[name] |
||||
} |
||||
|
||||
// GetRoute returns a route registered with the given name. This method
|
||||
// was renamed to Get() and remains here for backwards compatibility.
|
||||
func (r *Router) GetRoute(name string) *Route { |
||||
return r.getNamedRoutes()[name] |
||||
} |
||||
|
||||
// StrictSlash defines the trailing slash behavior for new routes. The initial
|
||||
// value is false.
|
||||
//
|
||||
// When true, if the route path is "/path/", accessing "/path" will redirect
|
||||
// to the former and vice versa. In other words, your application will always
|
||||
// see the path as specified in the route.
|
||||
//
|
||||
// When false, if the route path is "/path", accessing "/path/" will not match
|
||||
// this route and vice versa.
|
||||
//
|
||||
// Special case: when a route sets a path prefix using the PathPrefix() method,
|
||||
// strict slash is ignored for that route because the redirect behavior can't
|
||||
// be determined from a prefix alone. However, any subrouters created from that
|
||||
// route inherit the original StrictSlash setting.
|
||||
func (r *Router) StrictSlash(value bool) *Router { |
||||
r.strictSlash = value |
||||
return r |
||||
} |
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// parentRoute
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// getNamedRoutes returns the map where named routes are registered.
|
||||
func (r *Router) getNamedRoutes() map[string]*Route { |
||||
if r.namedRoutes == nil { |
||||
if r.parent != nil { |
||||
r.namedRoutes = r.parent.getNamedRoutes() |
||||
} else { |
||||
r.namedRoutes = make(map[string]*Route) |
||||
} |
||||
} |
||||
return r.namedRoutes |
||||
} |
||||
|
||||
// getRegexpGroup returns regexp definitions from the parent route, if any.
|
||||
func (r *Router) getRegexpGroup() *routeRegexpGroup { |
||||
if r.parent != nil { |
||||
return r.parent.getRegexpGroup() |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Route factories
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// NewRoute registers an empty route.
|
||||
func (r *Router) NewRoute() *Route { |
||||
route := &Route{parent: r, strictSlash: r.strictSlash} |
||||
r.routes = append(r.routes, route) |
||||
return route |
||||
} |
||||
|
||||
// Handle registers a new route with a matcher for the URL path.
|
||||
// See Route.Path() and Route.Handler().
|
||||
func (r *Router) Handle(path string, handler http.Handler) *Route { |
||||
return r.NewRoute().Path(path).Handler(handler) |
||||
} |
||||
|
||||
// HandleFunc registers a new route with a matcher for the URL path.
|
||||
// See Route.Path() and Route.HandlerFunc().
|
||||
func (r *Router) HandleFunc(path string, f func(http.ResponseWriter, |
||||
*http.Request)) *Route { |
||||
return r.NewRoute().Path(path).HandlerFunc(f) |
||||
} |
||||
|
||||
// Headers registers a new route with a matcher for request header values.
|
||||
// See Route.Headers().
|
||||
func (r *Router) Headers(pairs ...string) *Route { |
||||
return r.NewRoute().Headers(pairs...) |
||||
} |
||||
|
||||
// Host registers a new route with a matcher for the URL host.
|
||||
// See Route.Host().
|
||||
func (r *Router) Host(tpl string) *Route { |
||||
return r.NewRoute().Host(tpl) |
||||
} |
||||
|
||||
// MatcherFunc registers a new route with a custom matcher function.
|
||||
// See Route.MatcherFunc().
|
||||
func (r *Router) MatcherFunc(f MatcherFunc) *Route { |
||||
return r.NewRoute().MatcherFunc(f) |
||||
} |
||||
|
||||
// Methods registers a new route with a matcher for HTTP methods.
|
||||
// See Route.Methods().
|
||||
func (r *Router) Methods(methods ...string) *Route { |
||||
return r.NewRoute().Methods(methods...) |
||||
} |
||||
|
||||
// Path registers a new route with a matcher for the URL path.
|
||||
// See Route.Path().
|
||||
func (r *Router) Path(tpl string) *Route { |
||||
return r.NewRoute().Path(tpl) |
||||
} |
||||
|
||||
// PathPrefix registers a new route with a matcher for the URL path prefix.
|
||||
// See Route.PathPrefix().
|
||||
func (r *Router) PathPrefix(tpl string) *Route { |
||||
return r.NewRoute().PathPrefix(tpl) |
||||
} |
||||
|
||||
// Queries registers a new route with a matcher for URL query values.
|
||||
// See Route.Queries().
|
||||
func (r *Router) Queries(pairs ...string) *Route { |
||||
return r.NewRoute().Queries(pairs...) |
||||
} |
||||
|
||||
// Schemes registers a new route with a matcher for URL schemes.
|
||||
// See Route.Schemes().
|
||||
func (r *Router) Schemes(schemes ...string) *Route { |
||||
return r.NewRoute().Schemes(schemes...) |
||||
} |
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Context
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// RouteMatch stores information about a matched route.
|
||||
type RouteMatch struct { |
||||
Route *Route |
||||
Handler http.Handler |
||||
Vars map[string]string |
||||
} |
||||
|
||||
type contextKey int |
||||
|
||||
const ( |
||||
varsKey contextKey = iota |
||||
routeKey |
||||
) |
||||
|
||||
// Vars returns the route variables for the current request, if any.
|
||||
func Vars(r *http.Request) map[string]string { |
||||
if rv := context.Get(r, varsKey); rv != nil { |
||||
return rv.(map[string]string) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// CurrentRoute returns the matched route for the current request, if any.
|
||||
func CurrentRoute(r *http.Request) *Route { |
||||
if rv := context.Get(r, routeKey); rv != nil { |
||||
return rv.(*Route) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func setVars(r *http.Request, val interface{}) { |
||||
context.Set(r, varsKey, val) |
||||
} |
||||
|
||||
func setCurrentRoute(r *http.Request, val interface{}) { |
||||
context.Set(r, routeKey, val) |
||||
} |
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// cleanPath returns the canonical path for p, eliminating . and .. elements.
|
||||
// Borrowed from the net/http package.
|
||||
func cleanPath(p string) string { |
||||
if p == "" { |
||||
return "/" |
||||
} |
||||
if p[0] != '/' { |
||||
p = "/" + p |
||||
} |
||||
np := path.Clean(p) |
||||
// path.Clean removes trailing slash except for root;
|
||||
// put the trailing slash back if necessary.
|
||||
if p[len(p)-1] == '/' && np != "/" { |
||||
np += "/" |
||||
} |
||||
return np |
||||
} |
||||
|
||||
// uniqueVars returns an error if two slices contain duplicated strings.
|
||||
func uniqueVars(s1, s2 []string) error { |
||||
for _, v1 := range s1 { |
||||
for _, v2 := range s2 { |
||||
if v1 == v2 { |
||||
return fmt.Errorf("mux: duplicated route variable %q", v2) |
||||
} |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// mapFromPairs converts variadic string parameters to a string map.
|
||||
func mapFromPairs(pairs ...string) (map[string]string, error) { |
||||
length := len(pairs) |
||||
if length%2 != 0 { |
||||
return nil, fmt.Errorf( |
||||
"mux: number of parameters must be multiple of 2, got %v", pairs) |
||||
} |
||||
m := make(map[string]string, length/2) |
||||
for i := 0; i < length; i += 2 { |
||||
m[pairs[i]] = pairs[i+1] |
||||
} |
||||
return m, nil |
||||
} |
||||
|
||||
// matchInArray returns true if the given string value is in the array.
|
||||
func matchInArray(arr []string, value string) bool { |
||||
for _, v := range arr { |
||||
if v == value { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
|
||||
// matchMap returns true if the given key/value pairs exist in a given map.
|
||||
func matchMap(toCheck map[string]string, toMatch map[string][]string, |
||||
canonicalKey bool) bool { |
||||
for k, v := range toCheck { |
||||
// Check if key exists.
|
||||
if canonicalKey { |
||||
k = http.CanonicalHeaderKey(k) |
||||
} |
||||
if values := toMatch[k]; values == nil { |
||||
return false |
||||
} else if v != "" { |
||||
// If value was defined as an empty string we only check that the
|
||||
// key exists. Otherwise we also check for equality.
|
||||
valueExists := false |
||||
for _, value := range values { |
||||
if v == value { |
||||
valueExists = true |
||||
break |
||||
} |
||||
} |
||||
if !valueExists { |
||||
return false |
||||
} |
||||
} |
||||
} |
||||
return true |
||||
} |
@ -0,0 +1,943 @@ |
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mux |
||||
|
||||
import ( |
||||
"fmt" |
||||
"net/http" |
||||
"testing" |
||||
|
||||
"github.com/gorilla/context" |
||||
) |
||||
|
||||
type routeTest struct { |
||||
title string // title of the test
|
||||
route *Route // the route being tested
|
||||
request *http.Request // a request to test the route
|
||||
vars map[string]string // the expected vars of the match
|
||||
host string // the expected host of the match
|
||||
path string // the expected path of the match
|
||||
shouldMatch bool // whether the request is expected to match the route at all
|
||||
shouldRedirect bool // whether the request should result in a redirect
|
||||
} |
||||
|
||||
func TestHost(t *testing.T) { |
||||
// newRequestHost a new request with a method, url, and host header
|
||||
newRequestHost := func(method, url, host string) *http.Request { |
||||
req, err := http.NewRequest(method, url, nil) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
req.Host = host |
||||
return req |
||||
} |
||||
|
||||
tests := []routeTest{ |
||||
{ |
||||
title: "Host route match", |
||||
route: new(Route).Host("aaa.bbb.ccc"), |
||||
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), |
||||
vars: map[string]string{}, |
||||
host: "aaa.bbb.ccc", |
||||
path: "", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
title: "Host route, wrong host in request URL", |
||||
route: new(Route).Host("aaa.bbb.ccc"), |
||||
request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), |
||||
vars: map[string]string{}, |
||||
host: "aaa.bbb.ccc", |
||||
path: "", |
||||
shouldMatch: false, |
||||
}, |
||||
{ |
||||
title: "Host route with port, match", |
||||
route: new(Route).Host("aaa.bbb.ccc:1234"), |
||||
request: newRequest("GET", "http://aaa.bbb.ccc:1234/111/222/333"), |
||||
vars: map[string]string{}, |
||||
host: "aaa.bbb.ccc:1234", |
||||
path: "", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
title: "Host route with port, wrong port in request URL", |
||||
route: new(Route).Host("aaa.bbb.ccc:1234"), |
||||
request: newRequest("GET", "http://aaa.bbb.ccc:9999/111/222/333"), |
||||
vars: map[string]string{}, |
||||
host: "aaa.bbb.ccc:1234", |
||||
path: "", |
||||
shouldMatch: false, |
||||
}, |
||||
{ |
||||
title: "Host route, match with host in request header", |
||||
route: new(Route).Host("aaa.bbb.ccc"), |
||||
request: newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc"), |
||||
vars: map[string]string{}, |
||||
host: "aaa.bbb.ccc", |
||||
path: "", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
title: "Host route, wrong host in request header", |
||||
route: new(Route).Host("aaa.bbb.ccc"), |
||||
request: newRequestHost("GET", "/111/222/333", "aaa.222.ccc"), |
||||
vars: map[string]string{}, |
||||
host: "aaa.bbb.ccc", |
||||
path: "", |
||||
shouldMatch: false, |
||||
}, |
||||
// BUG {new(Route).Host("aaa.bbb.ccc:1234"), newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc:1234"), map[string]string{}, "aaa.bbb.ccc:1234", "", true},
|
||||
{ |
||||
title: "Host route with port, wrong host in request header", |
||||
route: new(Route).Host("aaa.bbb.ccc:1234"), |
||||
request: newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc:9999"), |
||||
vars: map[string]string{}, |
||||
host: "aaa.bbb.ccc:1234", |
||||
path: "", |
||||
shouldMatch: false, |
||||
}, |
||||
{ |
||||
title: "Host route with pattern, match", |
||||
route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"), |
||||
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), |
||||
vars: map[string]string{"v1": "bbb"}, |
||||
host: "aaa.bbb.ccc", |
||||
path: "", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
title: "Host route with pattern, wrong host in request URL", |
||||
route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"), |
||||
request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), |
||||
vars: map[string]string{"v1": "bbb"}, |
||||
host: "aaa.bbb.ccc", |
||||
path: "", |
||||
shouldMatch: false, |
||||
}, |
||||
{ |
||||
title: "Host route with multiple patterns, match", |
||||
route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}"), |
||||
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), |
||||
vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"}, |
||||
host: "aaa.bbb.ccc", |
||||
path: "", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
title: "Host route with multiple patterns, wrong host in request URL", |
||||
route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}"), |
||||
request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), |
||||
vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"}, |
||||
host: "aaa.bbb.ccc", |
||||
path: "", |
||||
shouldMatch: false, |
||||
}, |
||||
} |
||||
for _, test := range tests { |
||||
testRoute(t, test) |
||||
} |
||||
} |
||||
|
||||
func TestPath(t *testing.T) { |
||||
tests := []routeTest{ |
||||
{ |
||||
title: "Path route, match", |
||||
route: new(Route).Path("/111/222/333"), |
||||
request: newRequest("GET", "http://localhost/111/222/333"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "/111/222/333", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
title: "Path route, match with trailing slash in request and path", |
||||
route: new(Route).Path("/111/"), |
||||
request: newRequest("GET", "http://localhost/111/"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "/111/", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
title: "Path route, do not match with trailing slash in path", |
||||
route: new(Route).Path("/111/"), |
||||
request: newRequest("GET", "http://localhost/111"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "/111", |
||||
shouldMatch: false, |
||||
}, |
||||
{ |
||||
title: "Path route, do not match with trailing slash in request", |
||||
route: new(Route).Path("/111"), |
||||
request: newRequest("GET", "http://localhost/111/"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "/111/", |
||||
shouldMatch: false, |
||||
}, |
||||
{ |
||||
title: "Path route, wrong path in request in request URL", |
||||
route: new(Route).Path("/111/222/333"), |
||||
request: newRequest("GET", "http://localhost/1/2/3"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "/111/222/333", |
||||
shouldMatch: false, |
||||
}, |
||||
{ |
||||
title: "Path route with pattern, match", |
||||
route: new(Route).Path("/111/{v1:[0-9]{3}}/333"), |
||||
request: newRequest("GET", "http://localhost/111/222/333"), |
||||
vars: map[string]string{"v1": "222"}, |
||||
host: "", |
||||
path: "/111/222/333", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
title: "Path route with pattern, URL in request does not match", |
||||
route: new(Route).Path("/111/{v1:[0-9]{3}}/333"), |
||||
request: newRequest("GET", "http://localhost/111/aaa/333"), |
||||
vars: map[string]string{"v1": "222"}, |
||||
host: "", |
||||
path: "/111/222/333", |
||||
shouldMatch: false, |
||||
}, |
||||
{ |
||||
title: "Path route with multiple patterns, match", |
||||
route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"), |
||||
request: newRequest("GET", "http://localhost/111/222/333"), |
||||
vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"}, |
||||
host: "", |
||||
path: "/111/222/333", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
title: "Path route with multiple patterns, URL in request does not match", |
||||
route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"), |
||||
request: newRequest("GET", "http://localhost/111/aaa/333"), |
||||
vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"}, |
||||
host: "", |
||||
path: "/111/222/333", |
||||
shouldMatch: false, |
||||
}, |
||||
} |
||||
|
||||
for _, test := range tests { |
||||
testRoute(t, test) |
||||
} |
||||
} |
||||
|
||||
func TestPathPrefix(t *testing.T) { |
||||
tests := []routeTest{ |
||||
{ |
||||
title: "PathPrefix route, match", |
||||
route: new(Route).PathPrefix("/111"), |
||||
request: newRequest("GET", "http://localhost/111/222/333"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "/111", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
title: "PathPrefix route, match substring", |
||||
route: new(Route).PathPrefix("/1"), |
||||
request: newRequest("GET", "http://localhost/111/222/333"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "/1", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
title: "PathPrefix route, URL prefix in request does not match", |
||||
route: new(Route).PathPrefix("/111"), |
||||
request: newRequest("GET", "http://localhost/1/2/3"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "/111", |
||||
shouldMatch: false, |
||||
}, |
||||
{ |
||||
title: "PathPrefix route with pattern, match", |
||||
route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"), |
||||
request: newRequest("GET", "http://localhost/111/222/333"), |
||||
vars: map[string]string{"v1": "222"}, |
||||
host: "", |
||||
path: "/111/222", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
title: "PathPrefix route with pattern, URL prefix in request does not match", |
||||
route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"), |
||||
request: newRequest("GET", "http://localhost/111/aaa/333"), |
||||
vars: map[string]string{"v1": "222"}, |
||||
host: "", |
||||
path: "/111/222", |
||||
shouldMatch: false, |
||||
}, |
||||
{ |
||||
title: "PathPrefix route with multiple patterns, match", |
||||
route: new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}"), |
||||
request: newRequest("GET", "http://localhost/111/222/333"), |
||||
vars: map[string]string{"v1": "111", "v2": "222"}, |
||||
host: "", |
||||
path: "/111/222", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
title: "PathPrefix route with multiple patterns, URL prefix in request does not match", |
||||
route: new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}"), |
||||
request: newRequest("GET", "http://localhost/111/aaa/333"), |
||||
vars: map[string]string{"v1": "111", "v2": "222"}, |
||||
host: "", |
||||
path: "/111/222", |
||||
shouldMatch: false, |
||||
}, |
||||
} |
||||
|
||||
for _, test := range tests { |
||||
testRoute(t, test) |
||||
} |
||||
} |
||||
|
||||
func TestHostPath(t *testing.T) { |
||||
tests := []routeTest{ |
||||
{ |
||||
title: "Host and Path route, match", |
||||
route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"), |
||||
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
title: "Host and Path route, wrong host in request URL", |
||||
route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"), |
||||
request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "", |
||||
shouldMatch: false, |
||||
}, |
||||
{ |
||||
title: "Host and Path route with pattern, match", |
||||
route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), |
||||
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), |
||||
vars: map[string]string{"v1": "bbb", "v2": "222"}, |
||||
host: "aaa.bbb.ccc", |
||||
path: "/111/222/333", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
title: "Host and Path route with pattern, URL in request does not match", |
||||
route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), |
||||
request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), |
||||
vars: map[string]string{"v1": "bbb", "v2": "222"}, |
||||
host: "aaa.bbb.ccc", |
||||
path: "/111/222/333", |
||||
shouldMatch: false, |
||||
}, |
||||
{ |
||||
title: "Host and Path route with multiple patterns, match", |
||||
route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}"), |
||||
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), |
||||
vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"}, |
||||
host: "aaa.bbb.ccc", |
||||
path: "/111/222/333", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
title: "Host and Path route with multiple patterns, URL in request does not match", |
||||
route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}"), |
||||
request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), |
||||
vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"}, |
||||
host: "aaa.bbb.ccc", |
||||
path: "/111/222/333", |
||||
shouldMatch: false, |
||||
}, |
||||
} |
||||
|
||||
for _, test := range tests { |
||||
testRoute(t, test) |
||||
} |
||||
} |
||||
|
||||
func TestHeaders(t *testing.T) { |
||||
// newRequestHeaders creates a new request with a method, url, and headers
|
||||
newRequestHeaders := func(method, url string, headers map[string]string) *http.Request { |
||||
req, err := http.NewRequest(method, url, nil) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
for k, v := range headers { |
||||
req.Header.Add(k, v) |
||||
} |
||||
return req |
||||
} |
||||
|
||||
tests := []routeTest{ |
||||
{ |
||||
title: "Headers route, match", |
||||
route: new(Route).Headers("foo", "bar", "baz", "ding"), |
||||
request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "bar", "baz": "ding"}), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
title: "Headers route, bad header values", |
||||
route: new(Route).Headers("foo", "bar", "baz", "ding"), |
||||
request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "bar", "baz": "dong"}), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "", |
||||
shouldMatch: false, |
||||
}, |
||||
} |
||||
|
||||
for _, test := range tests { |
||||
testRoute(t, test) |
||||
} |
||||
|
||||
} |
||||
|
||||
func TestMethods(t *testing.T) { |
||||
tests := []routeTest{ |
||||
{ |
||||
title: "Methods route, match GET", |
||||
route: new(Route).Methods("GET", "POST"), |
||||
request: newRequest("GET", "http://localhost"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
title: "Methods route, match POST", |
||||
route: new(Route).Methods("GET", "POST"), |
||||
request: newRequest("POST", "http://localhost"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
title: "Methods route, bad method", |
||||
route: new(Route).Methods("GET", "POST"), |
||||
request: newRequest("PUT", "http://localhost"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "", |
||||
shouldMatch: false, |
||||
}, |
||||
} |
||||
|
||||
for _, test := range tests { |
||||
testRoute(t, test) |
||||
} |
||||
} |
||||
|
||||
func TestQueries(t *testing.T) { |
||||
tests := []routeTest{ |
||||
{ |
||||
title: "Queries route, match", |
||||
route: new(Route).Queries("foo", "bar", "baz", "ding"), |
||||
request: newRequest("GET", "http://localhost?foo=bar&baz=ding"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
title: "Queries route, match with a query string", |
||||
route: new(Route).Host("www.example.com").Path("/api").Queries("foo", "bar", "baz", "ding"), |
||||
request: newRequest("GET", "http://www.example.com/api?foo=bar&baz=ding"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
title: "Queries route, match with a query string out of order", |
||||
route: new(Route).Host("www.example.com").Path("/api").Queries("foo", "bar", "baz", "ding"), |
||||
request: newRequest("GET", "http://www.example.com/api?baz=ding&foo=bar"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
title: "Queries route, bad query", |
||||
route: new(Route).Queries("foo", "bar", "baz", "ding"), |
||||
request: newRequest("GET", "http://localhost?foo=bar&baz=dong"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "", |
||||
shouldMatch: false, |
||||
}, |
||||
{ |
||||
title: "Queries route with pattern, match", |
||||
route: new(Route).Queries("foo", "{v1}"), |
||||
request: newRequest("GET", "http://localhost?foo=bar"), |
||||
vars: map[string]string{"v1": "bar"}, |
||||
host: "", |
||||
path: "", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
title: "Queries route with multiple patterns, match", |
||||
route: new(Route).Queries("foo", "{v1}", "baz", "{v2}"), |
||||
request: newRequest("GET", "http://localhost?foo=bar&baz=ding"), |
||||
vars: map[string]string{"v1": "bar", "v2": "ding"}, |
||||
host: "", |
||||
path: "", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
title: "Queries route with regexp pattern, match", |
||||
route: new(Route).Queries("foo", "{v1:[0-9]+}"), |
||||
request: newRequest("GET", "http://localhost?foo=10"), |
||||
vars: map[string]string{"v1": "10"}, |
||||
host: "", |
||||
path: "", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
title: "Queries route with regexp pattern, regexp does not match", |
||||
route: new(Route).Queries("foo", "{v1:[0-9]+}"), |
||||
request: newRequest("GET", "http://localhost?foo=a"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "", |
||||
shouldMatch: false, |
||||
}, |
||||
} |
||||
|
||||
for _, test := range tests { |
||||
testRoute(t, test) |
||||
} |
||||
} |
||||
|
||||
func TestSchemes(t *testing.T) { |
||||
tests := []routeTest{ |
||||
// Schemes
|
||||
{ |
||||
title: "Schemes route, match https", |
||||
route: new(Route).Schemes("https", "ftp"), |
||||
request: newRequest("GET", "https://localhost"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
title: "Schemes route, match ftp", |
||||
route: new(Route).Schemes("https", "ftp"), |
||||
request: newRequest("GET", "ftp://localhost"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
title: "Schemes route, bad scheme", |
||||
route: new(Route).Schemes("https", "ftp"), |
||||
request: newRequest("GET", "http://localhost"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "", |
||||
shouldMatch: false, |
||||
}, |
||||
} |
||||
for _, test := range tests { |
||||
testRoute(t, test) |
||||
} |
||||
} |
||||
|
||||
func TestMatcherFunc(t *testing.T) { |
||||
m := func(r *http.Request, m *RouteMatch) bool { |
||||
if r.URL.Host == "aaa.bbb.ccc" { |
||||
return true |
||||
} |
||||
return false |
||||
} |
||||
|
||||
tests := []routeTest{ |
||||
{ |
||||
title: "MatchFunc route, match", |
||||
route: new(Route).MatcherFunc(m), |
||||
request: newRequest("GET", "http://aaa.bbb.ccc"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
title: "MatchFunc route, non-match", |
||||
route: new(Route).MatcherFunc(m), |
||||
request: newRequest("GET", "http://aaa.222.ccc"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "", |
||||
shouldMatch: false, |
||||
}, |
||||
} |
||||
|
||||
for _, test := range tests { |
||||
testRoute(t, test) |
||||
} |
||||
} |
||||
|
||||
func TestSubRouter(t *testing.T) { |
||||
subrouter1 := new(Route).Host("{v1:[a-z]+}.google.com").Subrouter() |
||||
subrouter2 := new(Route).PathPrefix("/foo/{v1}").Subrouter() |
||||
|
||||
tests := []routeTest{ |
||||
{ |
||||
route: subrouter1.Path("/{v2:[a-z]+}"), |
||||
request: newRequest("GET", "http://aaa.google.com/bbb"), |
||||
vars: map[string]string{"v1": "aaa", "v2": "bbb"}, |
||||
host: "aaa.google.com", |
||||
path: "/bbb", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
route: subrouter1.Path("/{v2:[a-z]+}"), |
||||
request: newRequest("GET", "http://111.google.com/111"), |
||||
vars: map[string]string{"v1": "aaa", "v2": "bbb"}, |
||||
host: "aaa.google.com", |
||||
path: "/bbb", |
||||
shouldMatch: false, |
||||
}, |
||||
{ |
||||
route: subrouter2.Path("/baz/{v2}"), |
||||
request: newRequest("GET", "http://localhost/foo/bar/baz/ding"), |
||||
vars: map[string]string{"v1": "bar", "v2": "ding"}, |
||||
host: "", |
||||
path: "/foo/bar/baz/ding", |
||||
shouldMatch: true, |
||||
}, |
||||
{ |
||||
route: subrouter2.Path("/baz/{v2}"), |
||||
request: newRequest("GET", "http://localhost/foo/bar"), |
||||
vars: map[string]string{"v1": "bar", "v2": "ding"}, |
||||
host: "", |
||||
path: "/foo/bar/baz/ding", |
||||
shouldMatch: false, |
||||
}, |
||||
} |
||||
|
||||
for _, test := range tests { |
||||
testRoute(t, test) |
||||
} |
||||
} |
||||
|
||||
func TestNamedRoutes(t *testing.T) { |
||||
r1 := NewRouter() |
||||
r1.NewRoute().Name("a") |
||||
r1.NewRoute().Name("b") |
||||
r1.NewRoute().Name("c") |
||||
|
||||
r2 := r1.NewRoute().Subrouter() |
||||
r2.NewRoute().Name("d") |
||||
r2.NewRoute().Name("e") |
||||
r2.NewRoute().Name("f") |
||||
|
||||
r3 := r2.NewRoute().Subrouter() |
||||
r3.NewRoute().Name("g") |
||||
r3.NewRoute().Name("h") |
||||
r3.NewRoute().Name("i") |
||||
|
||||
if r1.namedRoutes == nil || len(r1.namedRoutes) != 9 { |
||||
t.Errorf("Expected 9 named routes, got %v", r1.namedRoutes) |
||||
} else if r1.Get("i") == nil { |
||||
t.Errorf("Subroute name not registered") |
||||
} |
||||
} |
||||
|
||||
func TestStrictSlash(t *testing.T) { |
||||
r := NewRouter() |
||||
r.StrictSlash(true) |
||||
|
||||
tests := []routeTest{ |
||||
{ |
||||
title: "Redirect path without slash", |
||||
route: r.NewRoute().Path("/111/"), |
||||
request: newRequest("GET", "http://localhost/111"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "/111/", |
||||
shouldMatch: true, |
||||
shouldRedirect: true, |
||||
}, |
||||
{ |
||||
title: "Do not redirect path with slash", |
||||
route: r.NewRoute().Path("/111/"), |
||||
request: newRequest("GET", "http://localhost/111/"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "/111/", |
||||
shouldMatch: true, |
||||
shouldRedirect: false, |
||||
}, |
||||
{ |
||||
title: "Redirect path with slash", |
||||
route: r.NewRoute().Path("/111"), |
||||
request: newRequest("GET", "http://localhost/111/"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "/111", |
||||
shouldMatch: true, |
||||
shouldRedirect: true, |
||||
}, |
||||
{ |
||||
title: "Do not redirect path without slash", |
||||
route: r.NewRoute().Path("/111"), |
||||
request: newRequest("GET", "http://localhost/111"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "/111", |
||||
shouldMatch: true, |
||||
shouldRedirect: false, |
||||
}, |
||||
{ |
||||
title: "Propagate StrictSlash to subrouters", |
||||
route: r.NewRoute().PathPrefix("/static/").Subrouter().Path("/images/"), |
||||
request: newRequest("GET", "http://localhost/static/images"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "/static/images/", |
||||
shouldMatch: true, |
||||
shouldRedirect: true, |
||||
}, |
||||
{ |
||||
title: "Ignore StrictSlash for path prefix", |
||||
route: r.NewRoute().PathPrefix("/static/"), |
||||
request: newRequest("GET", "http://localhost/static/logo.png"), |
||||
vars: map[string]string{}, |
||||
host: "", |
||||
path: "/static/", |
||||
shouldMatch: true, |
||||
shouldRedirect: false, |
||||
}, |
||||
} |
||||
|
||||
for _, test := range tests { |
||||
testRoute(t, test) |
||||
} |
||||
} |
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
func getRouteTemplate(route *Route) string { |
||||
host, path := "none", "none" |
||||
if route.regexp != nil { |
||||
if route.regexp.host != nil { |
||||
host = route.regexp.host.template |
||||
} |
||||
if route.regexp.path != nil { |
||||
path = route.regexp.path.template |
||||
} |
||||
} |
||||
return fmt.Sprintf("Host: %v, Path: %v", host, path) |
||||
} |
||||
|
||||
func testRoute(t *testing.T, test routeTest) { |
||||
request := test.request |
||||
route := test.route |
||||
vars := test.vars |
||||
shouldMatch := test.shouldMatch |
||||
host := test.host |
||||
path := test.path |
||||
url := test.host + test.path |
||||
shouldRedirect := test.shouldRedirect |
||||
|
||||
var match RouteMatch |
||||
ok := route.Match(request, &match) |
||||
if ok != shouldMatch { |
||||
msg := "Should match" |
||||
if !shouldMatch { |
||||
msg = "Should not match" |
||||
} |
||||
t.Errorf("(%v) %v:\nRoute: %#v\nRequest: %#v\nVars: %v\n", test.title, msg, route, request, vars) |
||||
return |
||||
} |
||||
if shouldMatch { |
||||
if test.vars != nil && !stringMapEqual(test.vars, match.Vars) { |
||||
t.Errorf("(%v) Vars not equal: expected %v, got %v", test.title, vars, match.Vars) |
||||
return |
||||
} |
||||
if host != "" { |
||||
u, _ := test.route.URLHost(mapToPairs(match.Vars)...) |
||||
if host != u.Host { |
||||
t.Errorf("(%v) URLHost not equal: expected %v, got %v -- %v", test.title, host, u.Host, getRouteTemplate(route)) |
||||
return |
||||
} |
||||
} |
||||
if path != "" { |
||||
u, _ := route.URLPath(mapToPairs(match.Vars)...) |
||||
if path != u.Path { |
||||
t.Errorf("(%v) URLPath not equal: expected %v, got %v -- %v", test.title, path, u.Path, getRouteTemplate(route)) |
||||
return |
||||
} |
||||
} |
||||
if url != "" { |
||||
u, _ := route.URL(mapToPairs(match.Vars)...) |
||||
if url != u.Host+u.Path { |
||||
t.Errorf("(%v) URL not equal: expected %v, got %v -- %v", test.title, url, u.Host+u.Path, getRouteTemplate(route)) |
||||
return |
||||
} |
||||
} |
||||
if shouldRedirect && match.Handler == nil { |
||||
t.Errorf("(%v) Did not redirect", test.title) |
||||
return |
||||
} |
||||
if !shouldRedirect && match.Handler != nil { |
||||
t.Errorf("(%v) Unexpected redirect", test.title) |
||||
return |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Tests that the context is cleared or not cleared properly depending on
|
||||
// the configuration of the router
|
||||
func TestKeepContext(t *testing.T) { |
||||
func1 := func(w http.ResponseWriter, r *http.Request) {} |
||||
|
||||
r := NewRouter() |
||||
r.HandleFunc("/", func1).Name("func1") |
||||
|
||||
req, _ := http.NewRequest("GET", "http://localhost/", nil) |
||||
context.Set(req, "t", 1) |
||||
|
||||
res := new(http.ResponseWriter) |
||||
r.ServeHTTP(*res, req) |
||||
|
||||
if _, ok := context.GetOk(req, "t"); ok { |
||||
t.Error("Context should have been cleared at end of request") |
||||
} |
||||
|
||||
r.KeepContext = true |
||||
|
||||
req, _ = http.NewRequest("GET", "http://localhost/", nil) |
||||
context.Set(req, "t", 1) |
||||
|
||||
r.ServeHTTP(*res, req) |
||||
if _, ok := context.GetOk(req, "t"); !ok { |
||||
t.Error("Context should NOT have been cleared at end of request") |
||||
} |
||||
|
||||
} |
||||
|
||||
type TestA301ResponseWriter struct { |
||||
hh http.Header |
||||
status int |
||||
} |
||||
|
||||
func (ho TestA301ResponseWriter) Header() http.Header { |
||||
return http.Header(ho.hh) |
||||
} |
||||
|
||||
func (ho TestA301ResponseWriter) Write(b []byte) (int, error) { |
||||
return 0, nil |
||||
} |
||||
|
||||
func (ho TestA301ResponseWriter) WriteHeader(code int) { |
||||
ho.status = code |
||||
} |
||||
|
||||
func Test301Redirect(t *testing.T) { |
||||
m := make(http.Header) |
||||
|
||||
func1 := func(w http.ResponseWriter, r *http.Request) {} |
||||
func2 := func(w http.ResponseWriter, r *http.Request) {} |
||||
|
||||
r := NewRouter() |
||||
r.HandleFunc("/api/", func2).Name("func2") |
||||
r.HandleFunc("/", func1).Name("func1") |
||||
|
||||
req, _ := http.NewRequest("GET", "http://localhost//api/?abc=def", nil) |
||||
|
||||
res := TestA301ResponseWriter{ |
||||
hh: m, |
||||
status: 0, |
||||
} |
||||
r.ServeHTTP(&res, req) |
||||
|
||||
if "http://localhost/api/?abc=def" != res.hh["Location"][0] { |
||||
t.Errorf("Should have complete URL with query string") |
||||
} |
||||
} |
||||
|
||||
// https://plus.google.com/101022900381697718949/posts/eWy6DjFJ6uW
|
||||
func TestSubrouterHeader(t *testing.T) { |
||||
expected := "func1 response" |
||||
func1 := func(w http.ResponseWriter, r *http.Request) { |
||||
fmt.Fprint(w, expected) |
||||
} |
||||
func2 := func(http.ResponseWriter, *http.Request) {} |
||||
|
||||
r := NewRouter() |
||||
s := r.Headers("SomeSpecialHeader", "").Subrouter() |
||||
s.HandleFunc("/", func1).Name("func1") |
||||
r.HandleFunc("/", func2).Name("func2") |
||||
|
||||
req, _ := http.NewRequest("GET", "http://localhost/", nil) |
||||
req.Header.Add("SomeSpecialHeader", "foo") |
||||
match := new(RouteMatch) |
||||
matched := r.Match(req, match) |
||||
if !matched { |
||||
t.Errorf("Should match request") |
||||
} |
||||
if match.Route.GetName() != "func1" { |
||||
t.Errorf("Expecting func1 handler, got %s", match.Route.GetName()) |
||||
} |
||||
resp := NewRecorder() |
||||
match.Handler.ServeHTTP(resp, req) |
||||
if resp.Body.String() != expected { |
||||
t.Errorf("Expecting %q", expected) |
||||
} |
||||
} |
||||
|
||||
// mapToPairs converts a string map to a slice of string pairs
|
||||
func mapToPairs(m map[string]string) []string { |
||||
var i int |
||||
p := make([]string, len(m)*2) |
||||
for k, v := range m { |
||||
p[i] = k |
||||
p[i+1] = v |
||||
i += 2 |
||||
} |
||||
return p |
||||
} |
||||
|
||||
// stringMapEqual checks the equality of two string maps
|
||||
func stringMapEqual(m1, m2 map[string]string) bool { |
||||
nil1 := m1 == nil |
||||
nil2 := m2 == nil |
||||
if nil1 != nil2 || len(m1) != len(m2) { |
||||
return false |
||||
} |
||||
for k, v := range m1 { |
||||
if v != m2[k] { |
||||
return false |
||||
} |
||||
} |
||||
return true |
||||
} |
||||
|
||||
// newRequest is a helper function to create a new request with a method and url
|
||||
func newRequest(method, url string) *http.Request { |
||||
req, err := http.NewRequest(method, url, nil) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
return req |
||||
} |
@ -0,0 +1,714 @@ |
||||
// Old tests ported to Go1. This is a mess. Want to drop it one day.
|
||||
|
||||
// Copyright 2011 Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mux |
||||
|
||||
import ( |
||||
"bytes" |
||||
"net/http" |
||||
"testing" |
||||
) |
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// ResponseRecorder
|
||||
// ----------------------------------------------------------------------------
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// ResponseRecorder is an implementation of http.ResponseWriter that
|
||||
// records its mutations for later inspection in tests.
|
||||
type ResponseRecorder struct { |
||||
Code int // the HTTP response code from WriteHeader
|
||||
HeaderMap http.Header // the HTTP response headers
|
||||
Body *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to
|
||||
Flushed bool |
||||
} |
||||
|
||||
// NewRecorder returns an initialized ResponseRecorder.
|
||||
func NewRecorder() *ResponseRecorder { |
||||
return &ResponseRecorder{ |
||||
HeaderMap: make(http.Header), |
||||
Body: new(bytes.Buffer), |
||||
} |
||||
} |
||||
|
||||
// DefaultRemoteAddr is the default remote address to return in RemoteAddr if
|
||||
// an explicit DefaultRemoteAddr isn't set on ResponseRecorder.
|
||||
const DefaultRemoteAddr = "1.2.3.4" |
||||
|
||||
// Header returns the response headers.
|
||||
func (rw *ResponseRecorder) Header() http.Header { |
||||
return rw.HeaderMap |
||||
} |
||||
|
||||
// Write always succeeds and writes to rw.Body, if not nil.
|
||||
func (rw *ResponseRecorder) Write(buf []byte) (int, error) { |
||||
if rw.Body != nil { |
||||
rw.Body.Write(buf) |
||||
} |
||||
if rw.Code == 0 { |
||||
rw.Code = http.StatusOK |
||||
} |
||||
return len(buf), nil |
||||
} |
||||
|
||||
// WriteHeader sets rw.Code.
|
||||
func (rw *ResponseRecorder) WriteHeader(code int) { |
||||
rw.Code = code |
||||
} |
||||
|
||||
// Flush sets rw.Flushed to true.
|
||||
func (rw *ResponseRecorder) Flush() { |
||||
rw.Flushed = true |
||||
} |
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
func TestRouteMatchers(t *testing.T) { |
||||
var scheme, host, path, query, method string |
||||
var headers map[string]string |
||||
var resultVars map[bool]map[string]string |
||||
|
||||
router := NewRouter() |
||||
router.NewRoute().Host("{var1}.google.com"). |
||||
Path("/{var2:[a-z]+}/{var3:[0-9]+}"). |
||||
Queries("foo", "bar"). |
||||
Methods("GET"). |
||||
Schemes("https"). |
||||
Headers("x-requested-with", "XMLHttpRequest") |
||||
router.NewRoute().Host("www.{var4}.com"). |
||||
PathPrefix("/foo/{var5:[a-z]+}/{var6:[0-9]+}"). |
||||
Queries("baz", "ding"). |
||||
Methods("POST"). |
||||
Schemes("http"). |
||||
Headers("Content-Type", "application/json") |
||||
|
||||
reset := func() { |
||||
// Everything match.
|
||||
scheme = "https" |
||||
host = "www.google.com" |
||||
path = "/product/42" |
||||
query = "?foo=bar" |
||||
method = "GET" |
||||
headers = map[string]string{"X-Requested-With": "XMLHttpRequest"} |
||||
resultVars = map[bool]map[string]string{ |
||||
true: {"var1": "www", "var2": "product", "var3": "42"}, |
||||
false: {}, |
||||
} |
||||
} |
||||
|
||||
reset2 := func() { |
||||
// Everything match.
|
||||
scheme = "http" |
||||
host = "www.google.com" |
||||
path = "/foo/product/42/path/that/is/ignored" |
||||
query = "?baz=ding" |
||||
method = "POST" |
||||
headers = map[string]string{"Content-Type": "application/json"} |
||||
resultVars = map[bool]map[string]string{ |
||||
true: {"var4": "google", "var5": "product", "var6": "42"}, |
||||
false: {}, |
||||
} |
||||
} |
||||
|
||||
match := func(shouldMatch bool) { |
||||
url := scheme + "://" + host + path + query |
||||
request, _ := http.NewRequest(method, url, nil) |
||||
for key, value := range headers { |
||||
request.Header.Add(key, value) |
||||
} |
||||
|
||||
var routeMatch RouteMatch |
||||
matched := router.Match(request, &routeMatch) |
||||
if matched != shouldMatch { |
||||
// Need better messages. :)
|
||||
if matched { |
||||
t.Errorf("Should match.") |
||||
} else { |
||||
t.Errorf("Should not match.") |
||||
} |
||||
} |
||||
|
||||
if matched { |
||||
currentRoute := routeMatch.Route |
||||
if currentRoute == nil { |
||||
t.Errorf("Expected a current route.") |
||||
} |
||||
vars := routeMatch.Vars |
||||
expectedVars := resultVars[shouldMatch] |
||||
if len(vars) != len(expectedVars) { |
||||
t.Errorf("Expected vars: %v Got: %v.", expectedVars, vars) |
||||
} |
||||
for name, value := range vars { |
||||
if expectedVars[name] != value { |
||||
t.Errorf("Expected vars: %v Got: %v.", expectedVars, vars) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// 1st route --------------------------------------------------------------
|
||||
|
||||
// Everything match.
|
||||
reset() |
||||
match(true) |
||||
|
||||
// Scheme doesn't match.
|
||||
reset() |
||||
scheme = "http" |
||||
match(false) |
||||
|
||||
// Host doesn't match.
|
||||
reset() |
||||
host = "www.mygoogle.com" |
||||
match(false) |
||||
|
||||
// Path doesn't match.
|
||||
reset() |
||||
path = "/product/notdigits" |
||||
match(false) |
||||
|
||||
// Query doesn't match.
|
||||
reset() |
||||
query = "?foo=baz" |
||||
match(false) |
||||
|
||||
// Method doesn't match.
|
||||
reset() |
||||
method = "POST" |
||||
match(false) |
||||
|
||||
// Header doesn't match.
|
||||
reset() |
||||
headers = map[string]string{} |
||||
match(false) |
||||
|
||||
// Everything match, again.
|
||||
reset() |
||||
match(true) |
||||
|
||||
// 2nd route --------------------------------------------------------------
|
||||
|
||||
// Everything match.
|
||||
reset2() |
||||
match(true) |
||||
|
||||
// Scheme doesn't match.
|
||||
reset2() |
||||
scheme = "https" |
||||
match(false) |
||||
|
||||
// Host doesn't match.
|
||||
reset2() |
||||
host = "sub.google.com" |
||||
match(false) |
||||
|
||||
// Path doesn't match.
|
||||
reset2() |
||||
path = "/bar/product/42" |
||||
match(false) |
||||
|
||||
// Query doesn't match.
|
||||
reset2() |
||||
query = "?foo=baz" |
||||
match(false) |
||||
|
||||
// Method doesn't match.
|
||||
reset2() |
||||
method = "GET" |
||||
match(false) |
||||
|
||||
// Header doesn't match.
|
||||
reset2() |
||||
headers = map[string]string{} |
||||
match(false) |
||||
|
||||
// Everything match, again.
|
||||
reset2() |
||||
match(true) |
||||
} |
||||
|
||||
type headerMatcherTest struct { |
||||
matcher headerMatcher |
||||
headers map[string]string |
||||
result bool |
||||
} |
||||
|
||||
var headerMatcherTests = []headerMatcherTest{ |
||||
{ |
||||
matcher: headerMatcher(map[string]string{"x-requested-with": "XMLHttpRequest"}), |
||||
headers: map[string]string{"X-Requested-With": "XMLHttpRequest"}, |
||||
result: true, |
||||
}, |
||||
{ |
||||
matcher: headerMatcher(map[string]string{"x-requested-with": ""}), |
||||
headers: map[string]string{"X-Requested-With": "anything"}, |
||||
result: true, |
||||
}, |
||||
{ |
||||
matcher: headerMatcher(map[string]string{"x-requested-with": "XMLHttpRequest"}), |
||||
headers: map[string]string{}, |
||||
result: false, |
||||
}, |
||||
} |
||||
|
||||
type hostMatcherTest struct { |
||||
matcher *Route |
||||
url string |
||||
vars map[string]string |
||||
result bool |
||||
} |
||||
|
||||
var hostMatcherTests = []hostMatcherTest{ |
||||
{ |
||||
matcher: NewRouter().NewRoute().Host("{foo:[a-z][a-z][a-z]}.{bar:[a-z][a-z][a-z]}.{baz:[a-z][a-z][a-z]}"), |
||||
url: "http://abc.def.ghi/", |
||||
vars: map[string]string{"foo": "abc", "bar": "def", "baz": "ghi"}, |
||||
result: true, |
||||
}, |
||||
{ |
||||
matcher: NewRouter().NewRoute().Host("{foo:[a-z][a-z][a-z]}.{bar:[a-z][a-z][a-z]}.{baz:[a-z][a-z][a-z]}"), |
||||
url: "http://a.b.c/", |
||||
vars: map[string]string{"foo": "abc", "bar": "def", "baz": "ghi"}, |
||||
result: false, |
||||
}, |
||||
} |
||||
|
||||
type methodMatcherTest struct { |
||||
matcher methodMatcher |
||||
method string |
||||
result bool |
||||
} |
||||
|
||||
var methodMatcherTests = []methodMatcherTest{ |
||||
{ |
||||
matcher: methodMatcher([]string{"GET", "POST", "PUT"}), |
||||
method: "GET", |
||||
result: true, |
||||
}, |
||||
{ |
||||
matcher: methodMatcher([]string{"GET", "POST", "PUT"}), |
||||
method: "POST", |
||||
result: true, |
||||
}, |
||||
{ |
||||
matcher: methodMatcher([]string{"GET", "POST", "PUT"}), |
||||
method: "PUT", |
||||
result: true, |
||||
}, |
||||
{ |
||||
matcher: methodMatcher([]string{"GET", "POST", "PUT"}), |
||||
method: "DELETE", |
||||
result: false, |
||||
}, |
||||
} |
||||
|
||||
type pathMatcherTest struct { |
||||
matcher *Route |
||||
url string |
||||
vars map[string]string |
||||
result bool |
||||
} |
||||
|
||||
var pathMatcherTests = []pathMatcherTest{ |
||||
{ |
||||
matcher: NewRouter().NewRoute().Path("/{foo:[0-9][0-9][0-9]}/{bar:[0-9][0-9][0-9]}/{baz:[0-9][0-9][0-9]}"), |
||||
url: "http://localhost:8080/123/456/789", |
||||
vars: map[string]string{"foo": "123", "bar": "456", "baz": "789"}, |
||||
result: true, |
||||
}, |
||||
{ |
||||
matcher: NewRouter().NewRoute().Path("/{foo:[0-9][0-9][0-9]}/{bar:[0-9][0-9][0-9]}/{baz:[0-9][0-9][0-9]}"), |
||||
url: "http://localhost:8080/1/2/3", |
||||
vars: map[string]string{"foo": "123", "bar": "456", "baz": "789"}, |
||||
result: false, |
||||
}, |
||||
} |
||||
|
||||
type schemeMatcherTest struct { |
||||
matcher schemeMatcher |
||||
url string |
||||
result bool |
||||
} |
||||
|
||||
var schemeMatcherTests = []schemeMatcherTest{ |
||||
{ |
||||
matcher: schemeMatcher([]string{"http", "https"}), |
||||
url: "http://localhost:8080/", |
||||
result: true, |
||||
}, |
||||
{ |
||||
matcher: schemeMatcher([]string{"http", "https"}), |
||||
url: "https://localhost:8080/", |
||||
result: true, |
||||
}, |
||||
{ |
||||
matcher: schemeMatcher([]string{"https"}), |
||||
url: "http://localhost:8080/", |
||||
result: false, |
||||
}, |
||||
{ |
||||
matcher: schemeMatcher([]string{"http"}), |
||||
url: "https://localhost:8080/", |
||||
result: false, |
||||
}, |
||||
} |
||||
|
||||
type urlBuildingTest struct { |
||||
route *Route |
||||
vars []string |
||||
url string |
||||
} |
||||
|
||||
var urlBuildingTests = []urlBuildingTest{ |
||||
{ |
||||
route: new(Route).Host("foo.domain.com"), |
||||
vars: []string{}, |
||||
url: "http://foo.domain.com", |
||||
}, |
||||
{ |
||||
route: new(Route).Host("{subdomain}.domain.com"), |
||||
vars: []string{"subdomain", "bar"}, |
||||
url: "http://bar.domain.com", |
||||
}, |
||||
{ |
||||
route: new(Route).Host("foo.domain.com").Path("/articles"), |
||||
vars: []string{}, |
||||
url: "http://foo.domain.com/articles", |
||||
}, |
||||
{ |
||||
route: new(Route).Path("/articles"), |
||||
vars: []string{}, |
||||
url: "/articles", |
||||
}, |
||||
{ |
||||
route: new(Route).Path("/articles/{category}/{id:[0-9]+}"), |
||||
vars: []string{"category", "technology", "id", "42"}, |
||||
url: "/articles/technology/42", |
||||
}, |
||||
{ |
||||
route: new(Route).Host("{subdomain}.domain.com").Path("/articles/{category}/{id:[0-9]+}"), |
||||
vars: []string{"subdomain", "foo", "category", "technology", "id", "42"}, |
||||
url: "http://foo.domain.com/articles/technology/42", |
||||
}, |
||||
} |
||||
|
||||
func TestHeaderMatcher(t *testing.T) { |
||||
for _, v := range headerMatcherTests { |
||||
request, _ := http.NewRequest("GET", "http://localhost:8080/", nil) |
||||
for key, value := range v.headers { |
||||
request.Header.Add(key, value) |
||||
} |
||||
var routeMatch RouteMatch |
||||
result := v.matcher.Match(request, &routeMatch) |
||||
if result != v.result { |
||||
if v.result { |
||||
t.Errorf("%#v: should match %v.", v.matcher, request.Header) |
||||
} else { |
||||
t.Errorf("%#v: should not match %v.", v.matcher, request.Header) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestHostMatcher(t *testing.T) { |
||||
for _, v := range hostMatcherTests { |
||||
request, _ := http.NewRequest("GET", v.url, nil) |
||||
var routeMatch RouteMatch |
||||
result := v.matcher.Match(request, &routeMatch) |
||||
vars := routeMatch.Vars |
||||
if result != v.result { |
||||
if v.result { |
||||
t.Errorf("%#v: should match %v.", v.matcher, v.url) |
||||
} else { |
||||
t.Errorf("%#v: should not match %v.", v.matcher, v.url) |
||||
} |
||||
} |
||||
if result { |
||||
if len(vars) != len(v.vars) { |
||||
t.Errorf("%#v: vars length should be %v, got %v.", v.matcher, len(v.vars), len(vars)) |
||||
} |
||||
for name, value := range vars { |
||||
if v.vars[name] != value { |
||||
t.Errorf("%#v: expected value %v for key %v, got %v.", v.matcher, v.vars[name], name, value) |
||||
} |
||||
} |
||||
} else { |
||||
if len(vars) != 0 { |
||||
t.Errorf("%#v: vars length should be 0, got %v.", v.matcher, len(vars)) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestMethodMatcher(t *testing.T) { |
||||
for _, v := range methodMatcherTests { |
||||
request, _ := http.NewRequest(v.method, "http://localhost:8080/", nil) |
||||
var routeMatch RouteMatch |
||||
result := v.matcher.Match(request, &routeMatch) |
||||
if result != v.result { |
||||
if v.result { |
||||
t.Errorf("%#v: should match %v.", v.matcher, v.method) |
||||
} else { |
||||
t.Errorf("%#v: should not match %v.", v.matcher, v.method) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestPathMatcher(t *testing.T) { |
||||
for _, v := range pathMatcherTests { |
||||
request, _ := http.NewRequest("GET", v.url, nil) |
||||
var routeMatch RouteMatch |
||||
result := v.matcher.Match(request, &routeMatch) |
||||
vars := routeMatch.Vars |
||||
if result != v.result { |
||||
if v.result { |
||||
t.Errorf("%#v: should match %v.", v.matcher, v.url) |
||||
} else { |
||||
t.Errorf("%#v: should not match %v.", v.matcher, v.url) |
||||
} |
||||
} |
||||
if result { |
||||
if len(vars) != len(v.vars) { |
||||
t.Errorf("%#v: vars length should be %v, got %v.", v.matcher, len(v.vars), len(vars)) |
||||
} |
||||
for name, value := range vars { |
||||
if v.vars[name] != value { |
||||
t.Errorf("%#v: expected value %v for key %v, got %v.", v.matcher, v.vars[name], name, value) |
||||
} |
||||
} |
||||
} else { |
||||
if len(vars) != 0 { |
||||
t.Errorf("%#v: vars length should be 0, got %v.", v.matcher, len(vars)) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestSchemeMatcher(t *testing.T) { |
||||
for _, v := range schemeMatcherTests { |
||||
request, _ := http.NewRequest("GET", v.url, nil) |
||||
var routeMatch RouteMatch |
||||
result := v.matcher.Match(request, &routeMatch) |
||||
if result != v.result { |
||||
if v.result { |
||||
t.Errorf("%#v: should match %v.", v.matcher, v.url) |
||||
} else { |
||||
t.Errorf("%#v: should not match %v.", v.matcher, v.url) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestUrlBuilding(t *testing.T) { |
||||
|
||||
for _, v := range urlBuildingTests { |
||||
u, _ := v.route.URL(v.vars...) |
||||
url := u.String() |
||||
if url != v.url { |
||||
t.Errorf("expected %v, got %v", v.url, url) |
||||
/* |
||||
reversePath := "" |
||||
reverseHost := "" |
||||
if v.route.pathTemplate != nil { |
||||
reversePath = v.route.pathTemplate.Reverse |
||||
} |
||||
if v.route.hostTemplate != nil { |
||||
reverseHost = v.route.hostTemplate.Reverse |
||||
} |
||||
|
||||
t.Errorf("%#v:\nexpected: %q\ngot: %q\nreverse path: %q\nreverse host: %q", v.route, v.url, url, reversePath, reverseHost) |
||||
*/ |
||||
} |
||||
} |
||||
|
||||
ArticleHandler := func(w http.ResponseWriter, r *http.Request) { |
||||
} |
||||
|
||||
router := NewRouter() |
||||
router.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).Name("article") |
||||
|
||||
url, _ := router.Get("article").URL("category", "technology", "id", "42") |
||||
expected := "/articles/technology/42" |
||||
if url.String() != expected { |
||||
t.Errorf("Expected %v, got %v", expected, url.String()) |
||||
} |
||||
} |
||||
|
||||
func TestMatchedRouteName(t *testing.T) { |
||||
routeName := "stock" |
||||
router := NewRouter() |
||||
route := router.NewRoute().Path("/products/").Name(routeName) |
||||
|
||||
url := "http://www.domain.com/products/" |
||||
request, _ := http.NewRequest("GET", url, nil) |
||||
var rv RouteMatch |
||||
ok := router.Match(request, &rv) |
||||
|
||||
if !ok || rv.Route != route { |
||||
t.Errorf("Expected same route, got %+v.", rv.Route) |
||||
} |
||||
|
||||
retName := rv.Route.GetName() |
||||
if retName != routeName { |
||||
t.Errorf("Expected %q, got %q.", routeName, retName) |
||||
} |
||||
} |
||||
|
||||
func TestSubRouting(t *testing.T) { |
||||
// Example from docs.
|
||||
router := NewRouter() |
||||
subrouter := router.NewRoute().Host("www.domain.com").Subrouter() |
||||
route := subrouter.NewRoute().Path("/products/").Name("products") |
||||
|
||||
url := "http://www.domain.com/products/" |
||||
request, _ := http.NewRequest("GET", url, nil) |
||||
var rv RouteMatch |
||||
ok := router.Match(request, &rv) |
||||
|
||||
if !ok || rv.Route != route { |
||||
t.Errorf("Expected same route, got %+v.", rv.Route) |
||||
} |
||||
|
||||
u, _ := router.Get("products").URL() |
||||
builtUrl := u.String() |
||||
// Yay, subroute aware of the domain when building!
|
||||
if builtUrl != url { |
||||
t.Errorf("Expected %q, got %q.", url, builtUrl) |
||||
} |
||||
} |
||||
|
||||
func TestVariableNames(t *testing.T) { |
||||
route := new(Route).Host("{arg1}.domain.com").Path("/{arg1}/{arg2:[0-9]+}") |
||||
if route.err == nil { |
||||
t.Errorf("Expected error for duplicated variable names") |
||||
} |
||||
} |
||||
|
||||
func TestRedirectSlash(t *testing.T) { |
||||
var route *Route |
||||
var routeMatch RouteMatch |
||||
r := NewRouter() |
||||
|
||||
r.StrictSlash(false) |
||||
route = r.NewRoute() |
||||
if route.strictSlash != false { |
||||
t.Errorf("Expected false redirectSlash.") |
||||
} |
||||
|
||||
r.StrictSlash(true) |
||||
route = r.NewRoute() |
||||
if route.strictSlash != true { |
||||
t.Errorf("Expected true redirectSlash.") |
||||
} |
||||
|
||||
route = new(Route) |
||||
route.strictSlash = true |
||||
route.Path("/{arg1}/{arg2:[0-9]+}/") |
||||
request, _ := http.NewRequest("GET", "http://localhost/foo/123", nil) |
||||
routeMatch = RouteMatch{} |
||||
_ = route.Match(request, &routeMatch) |
||||
vars := routeMatch.Vars |
||||
if vars["arg1"] != "foo" { |
||||
t.Errorf("Expected foo.") |
||||
} |
||||
if vars["arg2"] != "123" { |
||||
t.Errorf("Expected 123.") |
||||
} |
||||
rsp := NewRecorder() |
||||
routeMatch.Handler.ServeHTTP(rsp, request) |
||||
if rsp.HeaderMap.Get("Location") != "http://localhost/foo/123/" { |
||||
t.Errorf("Expected redirect header.") |
||||
} |
||||
|
||||
route = new(Route) |
||||
route.strictSlash = true |
||||
route.Path("/{arg1}/{arg2:[0-9]+}") |
||||
request, _ = http.NewRequest("GET", "http://localhost/foo/123/", nil) |
||||
routeMatch = RouteMatch{} |
||||
_ = route.Match(request, &routeMatch) |
||||
vars = routeMatch.Vars |
||||
if vars["arg1"] != "foo" { |
||||
t.Errorf("Expected foo.") |
||||
} |
||||
if vars["arg2"] != "123" { |
||||
t.Errorf("Expected 123.") |
||||
} |
||||
rsp = NewRecorder() |
||||
routeMatch.Handler.ServeHTTP(rsp, request) |
||||
if rsp.HeaderMap.Get("Location") != "http://localhost/foo/123" { |
||||
t.Errorf("Expected redirect header.") |
||||
} |
||||
} |
||||
|
||||
// Test for the new regexp library, still not available in stable Go.
|
||||
func TestNewRegexp(t *testing.T) { |
||||
var p *routeRegexp |
||||
var matches []string |
||||
|
||||
tests := map[string]map[string][]string{ |
||||
"/{foo:a{2}}": { |
||||
"/a": nil, |
||||
"/aa": {"aa"}, |
||||
"/aaa": nil, |
||||
"/aaaa": nil, |
||||
}, |
||||
"/{foo:a{2,}}": { |
||||
"/a": nil, |
||||
"/aa": {"aa"}, |
||||
"/aaa": {"aaa"}, |
||||
"/aaaa": {"aaaa"}, |
||||
}, |
||||
"/{foo:a{2,3}}": { |
||||
"/a": nil, |
||||
"/aa": {"aa"}, |
||||
"/aaa": {"aaa"}, |
||||
"/aaaa": nil, |
||||
}, |
||||
"/{foo:[a-z]{3}}/{bar:[a-z]{2}}": { |
||||
"/a": nil, |
||||
"/ab": nil, |
||||
"/abc": nil, |
||||
"/abcd": nil, |
||||
"/abc/ab": {"abc", "ab"}, |
||||
"/abc/abc": nil, |
||||
"/abcd/ab": nil, |
||||
}, |
||||
`/{foo:\w{3,}}/{bar:\d{2,}}`: { |
||||
"/a": nil, |
||||
"/ab": nil, |
||||
"/abc": nil, |
||||
"/abc/1": nil, |
||||
"/abc/12": {"abc", "12"}, |
||||
"/abcd/12": {"abcd", "12"}, |
||||
"/abcd/123": {"abcd", "123"}, |
||||
}, |
||||
} |
||||
|
||||
for pattern, paths := range tests { |
||||
p, _ = newRouteRegexp(pattern, false, false, false, false) |
||||
for path, result := range paths { |
||||
matches = p.regexp.FindStringSubmatch(path) |
||||
if result == nil { |
||||
if matches != nil { |
||||
t.Errorf("%v should not match %v.", pattern, path) |
||||
} |
||||
} else { |
||||
if len(matches) != len(result)+1 { |
||||
t.Errorf("Expected %v matches, got %v.", len(result)+1, len(matches)) |
||||
} else { |
||||
for k, v := range result { |
||||
if matches[k+1] != v { |
||||
t.Errorf("Expected %v, got %v.", v, matches[k+1]) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,276 @@ |
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mux |
||||
|
||||
import ( |
||||
"bytes" |
||||
"fmt" |
||||
"net/http" |
||||
"net/url" |
||||
"regexp" |
||||
"strings" |
||||
) |
||||
|
||||
// newRouteRegexp parses a route template and returns a routeRegexp,
|
||||
// used to match a host, a path or a query string.
|
||||
//
|
||||
// It will extract named variables, assemble a regexp to be matched, create
|
||||
// a "reverse" template to build URLs and compile regexps to validate variable
|
||||
// values used in URL building.
|
||||
//
|
||||
// Previously we accepted only Python-like identifiers for variable
|
||||
// names ([a-zA-Z_][a-zA-Z0-9_]*), but currently the only restriction is that
|
||||
// name and pattern can't be empty, and names can't contain a colon.
|
||||
func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash bool) (*routeRegexp, error) { |
||||
// Check if it is well-formed.
|
||||
idxs, errBraces := braceIndices(tpl) |
||||
if errBraces != nil { |
||||
return nil, errBraces |
||||
} |
||||
// Backup the original.
|
||||
template := tpl |
||||
// Now let's parse it.
|
||||
defaultPattern := "[^/]+" |
||||
if matchQuery { |
||||
defaultPattern = "[^?&]+" |
||||
matchPrefix = true |
||||
} else if matchHost { |
||||
defaultPattern = "[^.]+" |
||||
matchPrefix = false |
||||
} |
||||
// Only match strict slash if not matching
|
||||
if matchPrefix || matchHost || matchQuery { |
||||
strictSlash = false |
||||
} |
||||
// Set a flag for strictSlash.
|
||||
endSlash := false |
||||
if strictSlash && strings.HasSuffix(tpl, "/") { |
||||
tpl = tpl[:len(tpl)-1] |
||||
endSlash = true |
||||
} |
||||
varsN := make([]string, len(idxs)/2) |
||||
varsR := make([]*regexp.Regexp, len(idxs)/2) |
||||
pattern := bytes.NewBufferString("") |
||||
if !matchQuery { |
||||
pattern.WriteByte('^') |
||||
} |
||||
reverse := bytes.NewBufferString("") |
||||
var end int |
||||
var err error |
||||
for i := 0; i < len(idxs); i += 2 { |
||||
// Set all values we are interested in.
|
||||
raw := tpl[end:idxs[i]] |
||||
end = idxs[i+1] |
||||
parts := strings.SplitN(tpl[idxs[i]+1:end-1], ":", 2) |
||||
name := parts[0] |
||||
patt := defaultPattern |
||||
if len(parts) == 2 { |
||||
patt = parts[1] |
||||
} |
||||
// Name or pattern can't be empty.
|
||||
if name == "" || patt == "" { |
||||
return nil, fmt.Errorf("mux: missing name or pattern in %q", |
||||
tpl[idxs[i]:end]) |
||||
} |
||||
// Build the regexp pattern.
|
||||
fmt.Fprintf(pattern, "%s(%s)", regexp.QuoteMeta(raw), patt) |
||||
// Build the reverse template.
|
||||
fmt.Fprintf(reverse, "%s%%s", raw) |
||||
// Append variable name and compiled pattern.
|
||||
varsN[i/2] = name |
||||
varsR[i/2], err = regexp.Compile(fmt.Sprintf("^%s$", patt)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
// Add the remaining.
|
||||
raw := tpl[end:] |
||||
pattern.WriteString(regexp.QuoteMeta(raw)) |
||||
if strictSlash { |
||||
pattern.WriteString("[/]?") |
||||
} |
||||
if !matchPrefix { |
||||
pattern.WriteByte('$') |
||||
} |
||||
reverse.WriteString(raw) |
||||
if endSlash { |
||||
reverse.WriteByte('/') |
||||
} |
||||
// Compile full regexp.
|
||||
reg, errCompile := regexp.Compile(pattern.String()) |
||||
if errCompile != nil { |
||||
return nil, errCompile |
||||
} |
||||
// Done!
|
||||
return &routeRegexp{ |
||||
template: template, |
||||
matchHost: matchHost, |
||||
matchQuery: matchQuery, |
||||
strictSlash: strictSlash, |
||||
regexp: reg, |
||||
reverse: reverse.String(), |
||||
varsN: varsN, |
||||
varsR: varsR, |
||||
}, nil |
||||
} |
||||
|
||||
// routeRegexp stores a regexp to match a host or path and information to
|
||||
// collect and validate route variables.
|
||||
type routeRegexp struct { |
||||
// The unmodified template.
|
||||
template string |
||||
// True for host match, false for path or query string match.
|
||||
matchHost bool |
||||
// True for query string match, false for path and host match.
|
||||
matchQuery bool |
||||
// The strictSlash value defined on the route, but disabled if PathPrefix was used.
|
||||
strictSlash bool |
||||
// Expanded regexp.
|
||||
regexp *regexp.Regexp |
||||
// Reverse template.
|
||||
reverse string |
||||
// Variable names.
|
||||
varsN []string |
||||
// Variable regexps (validators).
|
||||
varsR []*regexp.Regexp |
||||
} |
||||
|
||||
// Match matches the regexp against the URL host or path.
|
||||
func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool { |
||||
if !r.matchHost { |
||||
if r.matchQuery { |
||||
return r.regexp.MatchString(req.URL.RawQuery) |
||||
} else { |
||||
return r.regexp.MatchString(req.URL.Path) |
||||
} |
||||
} |
||||
return r.regexp.MatchString(getHost(req)) |
||||
} |
||||
|
||||
// url builds a URL part using the given values.
|
||||
func (r *routeRegexp) url(pairs ...string) (string, error) { |
||||
values, err := mapFromPairs(pairs...) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
urlValues := make([]interface{}, len(r.varsN)) |
||||
for k, v := range r.varsN { |
||||
value, ok := values[v] |
||||
if !ok { |
||||
return "", fmt.Errorf("mux: missing route variable %q", v) |
||||
} |
||||
urlValues[k] = value |
||||
} |
||||
rv := fmt.Sprintf(r.reverse, urlValues...) |
||||
if !r.regexp.MatchString(rv) { |
||||
// The URL is checked against the full regexp, instead of checking
|
||||
// individual variables. This is faster but to provide a good error
|
||||
// message, we check individual regexps if the URL doesn't match.
|
||||
for k, v := range r.varsN { |
||||
if !r.varsR[k].MatchString(values[v]) { |
||||
return "", fmt.Errorf( |
||||
"mux: variable %q doesn't match, expected %q", values[v], |
||||
r.varsR[k].String()) |
||||
} |
||||
} |
||||
} |
||||
return rv, nil |
||||
} |
||||
|
||||
// braceIndices returns the first level curly brace indices from a string.
|
||||
// It returns an error in case of unbalanced braces.
|
||||
func braceIndices(s string) ([]int, error) { |
||||
var level, idx int |
||||
idxs := make([]int, 0) |
||||
for i := 0; i < len(s); i++ { |
||||
switch s[i] { |
||||
case '{': |
||||
if level++; level == 1 { |
||||
idx = i |
||||
} |
||||
case '}': |
||||
if level--; level == 0 { |
||||
idxs = append(idxs, idx, i+1) |
||||
} else if level < 0 { |
||||
return nil, fmt.Errorf("mux: unbalanced braces in %q", s) |
||||
} |
||||
} |
||||
} |
||||
if level != 0 { |
||||
return nil, fmt.Errorf("mux: unbalanced braces in %q", s) |
||||
} |
||||
return idxs, nil |
||||
} |
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// routeRegexpGroup
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// routeRegexpGroup groups the route matchers that carry variables.
|
||||
type routeRegexpGroup struct { |
||||
host *routeRegexp |
||||
path *routeRegexp |
||||
queries []*routeRegexp |
||||
} |
||||
|
||||
// setMatch extracts the variables from the URL once a route matches.
|
||||
func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) { |
||||
// Store host variables.
|
||||
if v.host != nil { |
||||
hostVars := v.host.regexp.FindStringSubmatch(getHost(req)) |
||||
if hostVars != nil { |
||||
for k, v := range v.host.varsN { |
||||
m.Vars[v] = hostVars[k+1] |
||||
} |
||||
} |
||||
} |
||||
// Store path variables.
|
||||
if v.path != nil { |
||||
pathVars := v.path.regexp.FindStringSubmatch(req.URL.Path) |
||||
if pathVars != nil { |
||||
for k, v := range v.path.varsN { |
||||
m.Vars[v] = pathVars[k+1] |
||||
} |
||||
// Check if we should redirect.
|
||||
if v.path.strictSlash { |
||||
p1 := strings.HasSuffix(req.URL.Path, "/") |
||||
p2 := strings.HasSuffix(v.path.template, "/") |
||||
if p1 != p2 { |
||||
u, _ := url.Parse(req.URL.String()) |
||||
if p1 { |
||||
u.Path = u.Path[:len(u.Path)-1] |
||||
} else { |
||||
u.Path += "/" |
||||
} |
||||
m.Handler = http.RedirectHandler(u.String(), 301) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
// Store query string variables.
|
||||
rawQuery := req.URL.RawQuery |
||||
for _, q := range v.queries { |
||||
queryVars := q.regexp.FindStringSubmatch(rawQuery) |
||||
if queryVars != nil { |
||||
for k, v := range q.varsN { |
||||
m.Vars[v] = queryVars[k+1] |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// getHost tries its best to return the request host.
|
||||
func getHost(r *http.Request) string { |
||||
if r.URL.IsAbs() { |
||||
return r.URL.Host |
||||
} |
||||
host := r.Host |
||||
// Slice off any port information.
|
||||
if i := strings.Index(host, ":"); i != -1 { |
||||
host = host[:i] |
||||
} |
||||
return host |
||||
|
||||
} |
@ -0,0 +1,524 @@ |
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mux |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"net/http" |
||||
"net/url" |
||||
"strings" |
||||
) |
||||
|
||||
// Route stores information to match a request and build URLs.
|
||||
type Route struct { |
||||
// Parent where the route was registered (a Router).
|
||||
parent parentRoute |
||||
// Request handler for the route.
|
||||
handler http.Handler |
||||
// List of matchers.
|
||||
matchers []matcher |
||||
// Manager for the variables from host and path.
|
||||
regexp *routeRegexpGroup |
||||
// If true, when the path pattern is "/path/", accessing "/path" will
|
||||
// redirect to the former and vice versa.
|
||||
strictSlash bool |
||||
// If true, this route never matches: it is only used to build URLs.
|
||||
buildOnly bool |
||||
// The name used to build URLs.
|
||||
name string |
||||
// Error resulted from building a route.
|
||||
err error |
||||
} |
||||
|
||||
// Match matches the route against the request.
|
||||
func (r *Route) Match(req *http.Request, match *RouteMatch) bool { |
||||
if r.buildOnly || r.err != nil { |
||||
return false |
||||
} |
||||
// Match everything.
|
||||
for _, m := range r.matchers { |
||||
if matched := m.Match(req, match); !matched { |
||||
return false |
||||
} |
||||
} |
||||
// Yay, we have a match. Let's collect some info about it.
|
||||
if match.Route == nil { |
||||
match.Route = r |
||||
} |
||||
if match.Handler == nil { |
||||
match.Handler = r.handler |
||||
} |
||||
if match.Vars == nil { |
||||
match.Vars = make(map[string]string) |
||||
} |
||||
// Set variables.
|
||||
if r.regexp != nil { |
||||
r.regexp.setMatch(req, match, r) |
||||
} |
||||
return true |
||||
} |
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Route attributes
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// GetError returns an error resulted from building the route, if any.
|
||||
func (r *Route) GetError() error { |
||||
return r.err |
||||
} |
||||
|
||||
// BuildOnly sets the route to never match: it is only used to build URLs.
|
||||
func (r *Route) BuildOnly() *Route { |
||||
r.buildOnly = true |
||||
return r |
||||
} |
||||
|
||||
// Handler --------------------------------------------------------------------
|
||||
|
||||
// Handler sets a handler for the route.
|
||||
func (r *Route) Handler(handler http.Handler) *Route { |
||||
if r.err == nil { |
||||
r.handler = handler |
||||
} |
||||
return r |
||||
} |
||||
|
||||
// HandlerFunc sets a handler function for the route.
|
||||
func (r *Route) HandlerFunc(f func(http.ResponseWriter, *http.Request)) *Route { |
||||
return r.Handler(http.HandlerFunc(f)) |
||||
} |
||||
|
||||
// GetHandler returns the handler for the route, if any.
|
||||
func (r *Route) GetHandler() http.Handler { |
||||
return r.handler |
||||
} |
||||
|
||||
// Name -----------------------------------------------------------------------
|
||||
|
||||
// Name sets the name for the route, used to build URLs.
|
||||
// If the name was registered already it will be overwritten.
|
||||
func (r *Route) Name(name string) *Route { |
||||
if r.name != "" { |
||||
r.err = fmt.Errorf("mux: route already has name %q, can't set %q", |
||||
r.name, name) |
||||
} |
||||
if r.err == nil { |
||||
r.name = name |
||||
r.getNamedRoutes()[name] = r |
||||
} |
||||
return r |
||||
} |
||||
|
||||
// GetName returns the name for the route, if any.
|
||||
func (r *Route) GetName() string { |
||||
return r.name |
||||
} |
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Matchers
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// matcher types try to match a request.
|
||||
type matcher interface { |
||||
Match(*http.Request, *RouteMatch) bool |
||||
} |
||||
|
||||
// addMatcher adds a matcher to the route.
|
||||
func (r *Route) addMatcher(m matcher) *Route { |
||||
if r.err == nil { |
||||
r.matchers = append(r.matchers, m) |
||||
} |
||||
return r |
||||
} |
||||
|
||||
// addRegexpMatcher adds a host or path matcher and builder to a route.
|
||||
func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix, matchQuery bool) error { |
||||
if r.err != nil { |
||||
return r.err |
||||
} |
||||
r.regexp = r.getRegexpGroup() |
||||
if !matchHost && !matchQuery { |
||||
if len(tpl) == 0 || tpl[0] != '/' { |
||||
return fmt.Errorf("mux: path must start with a slash, got %q", tpl) |
||||
} |
||||
if r.regexp.path != nil { |
||||
tpl = strings.TrimRight(r.regexp.path.template, "/") + tpl |
||||
} |
||||
} |
||||
rr, err := newRouteRegexp(tpl, matchHost, matchPrefix, matchQuery, r.strictSlash) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
for _, q := range r.regexp.queries { |
||||
if err = uniqueVars(rr.varsN, q.varsN); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
if matchHost { |
||||
if r.regexp.path != nil { |
||||
if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
r.regexp.host = rr |
||||
} else { |
||||
if r.regexp.host != nil { |
||||
if err = uniqueVars(rr.varsN, r.regexp.host.varsN); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
if matchQuery { |
||||
r.regexp.queries = append(r.regexp.queries, rr) |
||||
} else { |
||||
r.regexp.path = rr |
||||
} |
||||
} |
||||
r.addMatcher(rr) |
||||
return nil |
||||
} |
||||
|
||||
// Headers --------------------------------------------------------------------
|
||||
|
||||
// headerMatcher matches the request against header values.
|
||||
type headerMatcher map[string]string |
||||
|
||||
func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool { |
||||
return matchMap(m, r.Header, true) |
||||
} |
||||
|
||||
// Headers adds a matcher for request header values.
|
||||
// It accepts a sequence of key/value pairs to be matched. For example:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.Headers("Content-Type", "application/json",
|
||||
// "X-Requested-With", "XMLHttpRequest")
|
||||
//
|
||||
// The above route will only match if both request header values match.
|
||||
//
|
||||
// It the value is an empty string, it will match any value if the key is set.
|
||||
func (r *Route) Headers(pairs ...string) *Route { |
||||
if r.err == nil { |
||||
var headers map[string]string |
||||
headers, r.err = mapFromPairs(pairs...) |
||||
return r.addMatcher(headerMatcher(headers)) |
||||
} |
||||
return r |
||||
} |
||||
|
||||
// Host -----------------------------------------------------------------------
|
||||
|
||||
// Host adds a matcher for the URL host.
|
||||
// It accepts a template with zero or more URL variables enclosed by {}.
|
||||
// Variables can define an optional regexp pattern to me matched:
|
||||
//
|
||||
// - {name} matches anything until the next dot.
|
||||
//
|
||||
// - {name:pattern} matches the given regexp pattern.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.Host("www.domain.com")
|
||||
// r.Host("{subdomain}.domain.com")
|
||||
// r.Host("{subdomain:[a-z]+}.domain.com")
|
||||
//
|
||||
// Variable names must be unique in a given route. They can be retrieved
|
||||
// calling mux.Vars(request).
|
||||
func (r *Route) Host(tpl string) *Route { |
||||
r.err = r.addRegexpMatcher(tpl, true, false, false) |
||||
return r |
||||
} |
||||
|
||||
// MatcherFunc ----------------------------------------------------------------
|
||||
|
||||
// MatcherFunc is the function signature used by custom matchers.
|
||||
type MatcherFunc func(*http.Request, *RouteMatch) bool |
||||
|
||||
func (m MatcherFunc) Match(r *http.Request, match *RouteMatch) bool { |
||||
return m(r, match) |
||||
} |
||||
|
||||
// MatcherFunc adds a custom function to be used as request matcher.
|
||||
func (r *Route) MatcherFunc(f MatcherFunc) *Route { |
||||
return r.addMatcher(f) |
||||
} |
||||
|
||||
// Methods --------------------------------------------------------------------
|
||||
|
||||
// methodMatcher matches the request against HTTP methods.
|
||||
type methodMatcher []string |
||||
|
||||
func (m methodMatcher) Match(r *http.Request, match *RouteMatch) bool { |
||||
return matchInArray(m, r.Method) |
||||
} |
||||
|
||||
// Methods adds a matcher for HTTP methods.
|
||||
// It accepts a sequence of one or more methods to be matched, e.g.:
|
||||
// "GET", "POST", "PUT".
|
||||
func (r *Route) Methods(methods ...string) *Route { |
||||
for k, v := range methods { |
||||
methods[k] = strings.ToUpper(v) |
||||
} |
||||
return r.addMatcher(methodMatcher(methods)) |
||||
} |
||||
|
||||
// Path -----------------------------------------------------------------------
|
||||
|
||||
// Path adds a matcher for the URL path.
|
||||
// It accepts a template with zero or more URL variables enclosed by {}. The
|
||||
// template must start with a "/".
|
||||
// Variables can define an optional regexp pattern to me matched:
|
||||
//
|
||||
// - {name} matches anything until the next slash.
|
||||
//
|
||||
// - {name:pattern} matches the given regexp pattern.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.Path("/products/").Handler(ProductsHandler)
|
||||
// r.Path("/products/{key}").Handler(ProductsHandler)
|
||||
// r.Path("/articles/{category}/{id:[0-9]+}").
|
||||
// Handler(ArticleHandler)
|
||||
//
|
||||
// Variable names must be unique in a given route. They can be retrieved
|
||||
// calling mux.Vars(request).
|
||||
func (r *Route) Path(tpl string) *Route { |
||||
r.err = r.addRegexpMatcher(tpl, false, false, false) |
||||
return r |
||||
} |
||||
|
||||
// PathPrefix -----------------------------------------------------------------
|
||||
|
||||
// PathPrefix adds a matcher for the URL path prefix. This matches if the given
|
||||
// template is a prefix of the full URL path. See Route.Path() for details on
|
||||
// the tpl argument.
|
||||
//
|
||||
// Note that it does not treat slashes specially ("/foobar/" will be matched by
|
||||
// the prefix "/foo") so you may want to use a trailing slash here.
|
||||
//
|
||||
// Also note that the setting of Router.StrictSlash() has no effect on routes
|
||||
// with a PathPrefix matcher.
|
||||
func (r *Route) PathPrefix(tpl string) *Route { |
||||
r.err = r.addRegexpMatcher(tpl, false, true, false) |
||||
return r |
||||
} |
||||
|
||||
// Query ----------------------------------------------------------------------
|
||||
|
||||
// Queries adds a matcher for URL query values.
|
||||
// It accepts a sequence of key/value pairs. Values may define variables.
|
||||
// For example:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.Queries("foo", "bar", "id", "{id:[0-9]+}")
|
||||
//
|
||||
// The above route will only match if the URL contains the defined queries
|
||||
// values, e.g.: ?foo=bar&id=42.
|
||||
//
|
||||
// It the value is an empty string, it will match any value if the key is set.
|
||||
//
|
||||
// Variables can define an optional regexp pattern to me matched:
|
||||
//
|
||||
// - {name} matches anything until the next slash.
|
||||
//
|
||||
// - {name:pattern} matches the given regexp pattern.
|
||||
func (r *Route) Queries(pairs ...string) *Route { |
||||
length := len(pairs) |
||||
if length%2 != 0 { |
||||
r.err = fmt.Errorf( |
||||
"mux: number of parameters must be multiple of 2, got %v", pairs) |
||||
return nil |
||||
} |
||||
for i := 0; i < length; i += 2 { |
||||
if r.err = r.addRegexpMatcher(pairs[i]+"="+pairs[i+1], false, true, true); r.err != nil { |
||||
return r |
||||
} |
||||
} |
||||
|
||||
return r |
||||
} |
||||
|
||||
// Schemes --------------------------------------------------------------------
|
||||
|
||||
// schemeMatcher matches the request against URL schemes.
|
||||
type schemeMatcher []string |
||||
|
||||
func (m schemeMatcher) Match(r *http.Request, match *RouteMatch) bool { |
||||
return matchInArray(m, r.URL.Scheme) |
||||
} |
||||
|
||||
// Schemes adds a matcher for URL schemes.
|
||||
// It accepts a sequence of schemes to be matched, e.g.: "http", "https".
|
||||
func (r *Route) Schemes(schemes ...string) *Route { |
||||
for k, v := range schemes { |
||||
schemes[k] = strings.ToLower(v) |
||||
} |
||||
return r.addMatcher(schemeMatcher(schemes)) |
||||
} |
||||
|
||||
// Subrouter ------------------------------------------------------------------
|
||||
|
||||
// Subrouter creates a subrouter for the route.
|
||||
//
|
||||
// It will test the inner routes only if the parent route matched. For example:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// s := r.Host("www.domain.com").Subrouter()
|
||||
// s.HandleFunc("/products/", ProductsHandler)
|
||||
// s.HandleFunc("/products/{key}", ProductHandler)
|
||||
// s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler)
|
||||
//
|
||||
// Here, the routes registered in the subrouter won't be tested if the host
|
||||
// doesn't match.
|
||||
func (r *Route) Subrouter() *Router { |
||||
router := &Router{parent: r, strictSlash: r.strictSlash} |
||||
r.addMatcher(router) |
||||
return router |
||||
} |
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// URL building
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// URL builds a URL for the route.
|
||||
//
|
||||
// It accepts a sequence of key/value pairs for the route variables. For
|
||||
// example, given this route:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
|
||||
// Name("article")
|
||||
//
|
||||
// ...a URL for it can be built using:
|
||||
//
|
||||
// url, err := r.Get("article").URL("category", "technology", "id", "42")
|
||||
//
|
||||
// ...which will return an url.URL with the following path:
|
||||
//
|
||||
// "/articles/technology/42"
|
||||
//
|
||||
// This also works for host variables:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.Host("{subdomain}.domain.com").
|
||||
// HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
|
||||
// Name("article")
|
||||
//
|
||||
// // url.String() will be "http://news.domain.com/articles/technology/42"
|
||||
// url, err := r.Get("article").URL("subdomain", "news",
|
||||
// "category", "technology",
|
||||
// "id", "42")
|
||||
//
|
||||
// All variables defined in the route are required, and their values must
|
||||
// conform to the corresponding patterns.
|
||||
func (r *Route) URL(pairs ...string) (*url.URL, error) { |
||||
if r.err != nil { |
||||
return nil, r.err |
||||
} |
||||
if r.regexp == nil { |
||||
return nil, errors.New("mux: route doesn't have a host or path") |
||||
} |
||||
var scheme, host, path string |
||||
var err error |
||||
if r.regexp.host != nil { |
||||
// Set a default scheme.
|
||||
scheme = "http" |
||||
if host, err = r.regexp.host.url(pairs...); err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
if r.regexp.path != nil { |
||||
if path, err = r.regexp.path.url(pairs...); err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
return &url.URL{ |
||||
Scheme: scheme, |
||||
Host: host, |
||||
Path: path, |
||||
}, nil |
||||
} |
||||
|
||||
// URLHost builds the host part of the URL for a route. See Route.URL().
|
||||
//
|
||||
// The route must have a host defined.
|
||||
func (r *Route) URLHost(pairs ...string) (*url.URL, error) { |
||||
if r.err != nil { |
||||
return nil, r.err |
||||
} |
||||
if r.regexp == nil || r.regexp.host == nil { |
||||
return nil, errors.New("mux: route doesn't have a host") |
||||
} |
||||
host, err := r.regexp.host.url(pairs...) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return &url.URL{ |
||||
Scheme: "http", |
||||
Host: host, |
||||
}, nil |
||||
} |
||||
|
||||
// URLPath builds the path part of the URL for a route. See Route.URL().
|
||||
//
|
||||
// The route must have a path defined.
|
||||
func (r *Route) URLPath(pairs ...string) (*url.URL, error) { |
||||
if r.err != nil { |
||||
return nil, r.err |
||||
} |
||||
if r.regexp == nil || r.regexp.path == nil { |
||||
return nil, errors.New("mux: route doesn't have a path") |
||||
} |
||||
path, err := r.regexp.path.url(pairs...) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return &url.URL{ |
||||
Path: path, |
||||
}, nil |
||||
} |
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// parentRoute
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// parentRoute allows routes to know about parent host and path definitions.
|
||||
type parentRoute interface { |
||||
getNamedRoutes() map[string]*Route |
||||
getRegexpGroup() *routeRegexpGroup |
||||
} |
||||
|
||||
// getNamedRoutes returns the map where named routes are registered.
|
||||
func (r *Route) getNamedRoutes() map[string]*Route { |
||||
if r.parent == nil { |
||||
// During tests router is not always set.
|
||||
r.parent = NewRouter() |
||||
} |
||||
return r.parent.getNamedRoutes() |
||||
} |
||||
|
||||
// getRegexpGroup returns regexp definitions from this route.
|
||||
func (r *Route) getRegexpGroup() *routeRegexpGroup { |
||||
if r.regexp == nil { |
||||
if r.parent == nil { |
||||
// During tests router is not always set.
|
||||
r.parent = NewRouter() |
||||
} |
||||
regexp := r.parent.getRegexpGroup() |
||||
if regexp == nil { |
||||
r.regexp = new(routeRegexpGroup) |
||||
} else { |
||||
// Copy.
|
||||
r.regexp = &routeRegexpGroup{ |
||||
host: regexp.host, |
||||
path: regexp.path, |
||||
queries: regexp.queries, |
||||
} |
||||
} |
||||
} |
||||
return r.regexp |
||||
} |
Loading…
Reference in new issue