Browse Source

Support building URLs with non-http schemes. (#260)

* Move misplaced tests and fix comments.

* Support building URLs with non-http schemes.

- Capture first scheme configured for a route for use when building
  URLs.
- Add new Route.URLScheme method similar to URLHost and URLPath.
- Update Route.URLHost and Route.URL to use the captured scheme if
  present.

* Remove Route.URLScheme method.

* Remove UTF-8 BOM.
pull/264/head v1.4.0
Chris Hines 9 years ago committed by Matt Silverlock
parent
commit
bcd8bc72b0
  1. 232
      mux_test.go
  2. 19
      route.go

232
mux_test.go

@ -10,6 +10,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"strings" "strings"
"testing" "testing"
) )
@ -31,10 +32,11 @@ type routeTest struct {
route *Route // the route being tested route *Route // the route being tested
request *http.Request // a request to test the route request *http.Request // a request to test the route
vars map[string]string // the expected vars of the match vars map[string]string // the expected vars of the match
host string // the expected host of the match scheme string // the expected scheme of the built URL
path string // the expected path of the match host string // the expected host of the built URL
pathTemplate string // the expected path template to match path string // the expected path of the built URL
hostTemplate string // the expected host template to match pathTemplate string // the expected path template of the route
hostTemplate string // the expected host template of the route
methods []string // the expected route methods methods []string // the expected route methods
pathRegexp string // the expected path regexp pathRegexp string // the expected path regexp
shouldMatch bool // whether the request is expected to match the route at all shouldMatch bool // whether the request is expected to match the route at all
@ -197,46 +199,6 @@ func TestHost(t *testing.T) {
hostTemplate: `{v-1:[a-z]{3}}.{v-2:[a-z]{3}}.{v-3:[a-z]{3}}`, hostTemplate: `{v-1:[a-z]{3}}.{v-2:[a-z]{3}}.{v-3:[a-z]{3}}`,
shouldMatch: true, shouldMatch: true,
}, },
{
title: "Path route with single pattern with pipe, match",
route: new(Route).Path("/{category:a|b/c}"),
request: newRequest("GET", "http://localhost/a"),
vars: map[string]string{"category": "a"},
host: "",
path: "/a",
pathTemplate: `/{category:a|b/c}`,
shouldMatch: true,
},
{
title: "Path route with single pattern with pipe, match",
route: new(Route).Path("/{category:a|b/c}"),
request: newRequest("GET", "http://localhost/b/c"),
vars: map[string]string{"category": "b/c"},
host: "",
path: "/b/c",
pathTemplate: `/{category:a|b/c}`,
shouldMatch: true,
},
{
title: "Path route with multiple patterns with pipe, match",
route: new(Route).Path("/{category:a|b/c}/{product}/{id:[0-9]+}"),
request: newRequest("GET", "http://localhost/a/product_name/1"),
vars: map[string]string{"category": "a", "product": "product_name", "id": "1"},
host: "",
path: "/a/product_name/1",
pathTemplate: `/{category:a|b/c}/{product}/{id:[0-9]+}`,
shouldMatch: true,
},
{
title: "Path route with multiple patterns with pipe, match",
route: new(Route).Path("/{category:a|b/c}/{product}/{id:[0-9]+}"),
request: newRequest("GET", "http://localhost/b/c/product_name/1"),
vars: map[string]string{"category": "b/c", "product": "product_name", "id": "1"},
host: "",
path: "/b/c/product_name/1",
pathTemplate: `/{category:a|b/c}/{product}/{id:[0-9]+}`,
shouldMatch: true,
},
} }
for _, test := range tests { for _, test := range tests {
testRoute(t, test) testRoute(t, test)
@ -428,6 +390,46 @@ func TestPath(t *testing.T) {
pathRegexp: `^/(?P<v0>[0-9]*)(?P<v1>[a-z]*)/(?P<v2>[0-9]*)$`, pathRegexp: `^/(?P<v0>[0-9]*)(?P<v1>[a-z]*)/(?P<v2>[0-9]*)$`,
shouldMatch: true, shouldMatch: true,
}, },
{
title: "Path route with single pattern with pipe, match",
route: new(Route).Path("/{category:a|b/c}"),
request: newRequest("GET", "http://localhost/a"),
vars: map[string]string{"category": "a"},
host: "",
path: "/a",
pathTemplate: `/{category:a|b/c}`,
shouldMatch: true,
},
{
title: "Path route with single pattern with pipe, match",
route: new(Route).Path("/{category:a|b/c}"),
request: newRequest("GET", "http://localhost/b/c"),
vars: map[string]string{"category": "b/c"},
host: "",
path: "/b/c",
pathTemplate: `/{category:a|b/c}`,
shouldMatch: true,
},
{
title: "Path route with multiple patterns with pipe, match",
route: new(Route).Path("/{category:a|b/c}/{product}/{id:[0-9]+}"),
request: newRequest("GET", "http://localhost/a/product_name/1"),
vars: map[string]string{"category": "a", "product": "product_name", "id": "1"},
host: "",
path: "/a/product_name/1",
pathTemplate: `/{category:a|b/c}/{product}/{id:[0-9]+}`,
shouldMatch: true,
},
{
title: "Path route with multiple patterns with pipe, match",
route: new(Route).Path("/{category:a|b/c}/{product}/{id:[0-9]+}"),
request: newRequest("GET", "http://localhost/b/c/product_name/1"),
vars: map[string]string{"category": "b/c", "product": "product_name", "id": "1"},
host: "",
path: "/b/c/product_name/1",
pathTemplate: `/{category:a|b/c}/{product}/{id:[0-9]+}`,
shouldMatch: true,
},
} }
for _, test := range tests { for _, test := range tests {
@ -516,15 +518,28 @@ func TestPathPrefix(t *testing.T) {
} }
} }
func TestHostPath(t *testing.T) { func TestSchemeHostPath(t *testing.T) {
tests := []routeTest{ tests := []routeTest{
{ {
title: "Host and Path route, match", title: "Host and Path route, match",
route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"), route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"),
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
vars: map[string]string{}, vars: map[string]string{},
host: "", scheme: "http",
path: "", host: "aaa.bbb.ccc",
path: "/111/222/333",
pathTemplate: `/111/222/333`,
hostTemplate: `aaa.bbb.ccc`,
shouldMatch: true,
},
{
title: "Scheme, Host, and Path route, match",
route: new(Route).Schemes("https").Host("aaa.bbb.ccc").Path("/111/222/333"),
request: newRequest("GET", "https://aaa.bbb.ccc/111/222/333"),
vars: map[string]string{},
scheme: "https",
host: "aaa.bbb.ccc",
path: "/111/222/333",
pathTemplate: `/111/222/333`, pathTemplate: `/111/222/333`,
hostTemplate: `aaa.bbb.ccc`, hostTemplate: `aaa.bbb.ccc`,
shouldMatch: true, shouldMatch: true,
@ -534,8 +549,9 @@ func TestHostPath(t *testing.T) {
route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"), route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"),
request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), request: newRequest("GET", "http://aaa.222.ccc/111/222/333"),
vars: map[string]string{}, vars: map[string]string{},
host: "", scheme: "http",
path: "", host: "aaa.bbb.ccc",
path: "/111/222/333",
pathTemplate: `/111/222/333`, pathTemplate: `/111/222/333`,
hostTemplate: `aaa.bbb.ccc`, hostTemplate: `aaa.bbb.ccc`,
shouldMatch: false, shouldMatch: false,
@ -545,6 +561,19 @@ func TestHostPath(t *testing.T) {
route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), 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"), request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
vars: map[string]string{"v1": "bbb", "v2": "222"}, vars: map[string]string{"v1": "bbb", "v2": "222"},
scheme: "http",
host: "aaa.bbb.ccc",
path: "/111/222/333",
pathTemplate: `/111/{v2:[0-9]{3}}/333`,
hostTemplate: `aaa.{v1:[a-z]{3}}.ccc`,
shouldMatch: true,
},
{
title: "Scheme, Host, and Path route with host and path patterns, match",
route: new(Route).Schemes("ftp", "ssss").Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"),
request: newRequest("GET", "ssss://aaa.bbb.ccc/111/222/333"),
vars: map[string]string{"v1": "bbb", "v2": "222"},
scheme: "ftp",
host: "aaa.bbb.ccc", host: "aaa.bbb.ccc",
path: "/111/222/333", path: "/111/222/333",
pathTemplate: `/111/{v2:[0-9]{3}}/333`, pathTemplate: `/111/{v2:[0-9]{3}}/333`,
@ -556,6 +585,7 @@ func TestHostPath(t *testing.T) {
route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), 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"), request: newRequest("GET", "http://aaa.222.ccc/111/222/333"),
vars: map[string]string{"v1": "bbb", "v2": "222"}, vars: map[string]string{"v1": "bbb", "v2": "222"},
scheme: "http",
host: "aaa.bbb.ccc", host: "aaa.bbb.ccc",
path: "/111/222/333", path: "/111/222/333",
pathTemplate: `/111/{v2:[0-9]{3}}/333`, pathTemplate: `/111/{v2:[0-9]{3}}/333`,
@ -567,6 +597,7 @@ func TestHostPath(t *testing.T) {
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}}"), 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"), 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"}, vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"},
scheme: "http",
host: "aaa.bbb.ccc", host: "aaa.bbb.ccc",
path: "/111/222/333", path: "/111/222/333",
pathTemplate: `/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}`, pathTemplate: `/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}`,
@ -578,6 +609,7 @@ func TestHostPath(t *testing.T) {
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}}"), 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"), 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"}, vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"},
scheme: "http",
host: "aaa.bbb.ccc", host: "aaa.bbb.ccc",
path: "/111/222/333", path: "/111/222/333",
pathTemplate: `/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}`, pathTemplate: `/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}`,
@ -649,7 +681,6 @@ func TestHeaders(t *testing.T) {
testRoute(t, test) testRoute(t, test)
testTemplate(t, test) testTemplate(t, test)
} }
} }
func TestMethods(t *testing.T) { func TestMethods(t *testing.T) {
@ -938,30 +969,43 @@ func TestSchemes(t *testing.T) {
tests := []routeTest{ tests := []routeTest{
// Schemes // Schemes
{ {
title: "Schemes route, match https", title: "Schemes route, default scheme, match http, build http",
route: new(Route).Schemes("https", "ftp"), route: new(Route).Host("localhost"),
request: newRequest("GET", "http://localhost"),
scheme: "http",
host: "localhost",
shouldMatch: true,
},
{
title: "Schemes route, match https, build https",
route: new(Route).Schemes("https", "ftp").Host("localhost"),
request: newRequest("GET", "https://localhost"), request: newRequest("GET", "https://localhost"),
vars: map[string]string{}, scheme: "https",
host: "", host: "localhost",
path: "",
shouldMatch: true, shouldMatch: true,
}, },
{ {
title: "Schemes route, match ftp", title: "Schemes route, match ftp, build https",
route: new(Route).Schemes("https", "ftp"), route: new(Route).Schemes("https", "ftp").Host("localhost"),
request: newRequest("GET", "ftp://localhost"), request: newRequest("GET", "ftp://localhost"),
vars: map[string]string{}, scheme: "https",
host: "", host: "localhost",
path: "", shouldMatch: true,
},
{
title: "Schemes route, match ftp, build ftp",
route: new(Route).Schemes("ftp", "https").Host("localhost"),
request: newRequest("GET", "ftp://localhost"),
scheme: "ftp",
host: "localhost",
shouldMatch: true, shouldMatch: true,
}, },
{ {
title: "Schemes route, bad scheme", title: "Schemes route, bad scheme",
route: new(Route).Schemes("https", "ftp"), route: new(Route).Schemes("https", "ftp").Host("localhost"),
request: newRequest("GET", "http://localhost"), request: newRequest("GET", "http://localhost"),
vars: map[string]string{}, scheme: "https",
host: "", host: "localhost",
path: "",
shouldMatch: false, shouldMatch: false,
}, },
} }
@ -1448,10 +1492,15 @@ func testRoute(t *testing.T, test routeTest) {
route := test.route route := test.route
vars := test.vars vars := test.vars
shouldMatch := test.shouldMatch shouldMatch := test.shouldMatch
host := test.host
path := test.path
url := test.host + test.path
shouldRedirect := test.shouldRedirect shouldRedirect := test.shouldRedirect
uri := url.URL{
Scheme: test.scheme,
Host: test.host,
Path: test.path,
}
if uri.Scheme == "" {
uri.Scheme = "http"
}
var match RouteMatch var match RouteMatch
ok := route.Match(request, &match) ok := route.Match(request, &match)
@ -1464,28 +1513,51 @@ func testRoute(t *testing.T, test routeTest) {
return return
} }
if shouldMatch { if shouldMatch {
if test.vars != nil && !stringMapEqual(test.vars, match.Vars) { if vars != nil && !stringMapEqual(vars, match.Vars) {
t.Errorf("(%v) Vars not equal: expected %v, got %v", test.title, vars, match.Vars) t.Errorf("(%v) Vars not equal: expected %v, got %v", test.title, vars, match.Vars)
return return
} }
if host != "" { if test.scheme != "" {
u, _ := test.route.URLHost(mapToPairs(match.Vars)...) u, err := route.URL(mapToPairs(match.Vars)...)
if host != u.Host { if err != nil {
t.Errorf("(%v) URLHost not equal: expected %v, got %v -- %v", test.title, host, u.Host, getRouteTemplate(route)) t.Fatalf("(%v) URL error: %v -- %v", test.title, err, getRouteTemplate(route))
}
if uri.Scheme != u.Scheme {
t.Errorf("(%v) URLScheme not equal: expected %v, got %v", test.title, uri.Scheme, u.Scheme)
return
}
}
if test.host != "" {
u, err := test.route.URLHost(mapToPairs(match.Vars)...)
if err != nil {
t.Fatalf("(%v) URLHost error: %v -- %v", test.title, err, getRouteTemplate(route))
}
if uri.Scheme != u.Scheme {
t.Errorf("(%v) URLHost scheme not equal: expected %v, got %v -- %v", test.title, uri.Scheme, u.Scheme, getRouteTemplate(route))
return
}
if uri.Host != u.Host {
t.Errorf("(%v) URLHost host not equal: expected %v, got %v -- %v", test.title, uri.Host, u.Host, getRouteTemplate(route))
return return
} }
} }
if path != "" { if test.path != "" {
u, _ := route.URLPath(mapToPairs(match.Vars)...) u, err := route.URLPath(mapToPairs(match.Vars)...)
if path != u.Path { if err != nil {
t.Errorf("(%v) URLPath not equal: expected %v, got %v -- %v", test.title, path, u.Path, getRouteTemplate(route)) t.Fatalf("(%v) URLPath error: %v -- %v", test.title, err, getRouteTemplate(route))
}
if uri.Path != u.Path {
t.Errorf("(%v) URLPath not equal: expected %v, got %v -- %v", test.title, uri.Path, u.Path, getRouteTemplate(route))
return return
} }
} }
if url != "" { if test.host != "" && test.path != "" {
u, _ := route.URL(mapToPairs(match.Vars)...) u, err := route.URL(mapToPairs(match.Vars)...)
if url != u.Host+u.Path { if err != nil {
t.Errorf("(%v) URL not equal: expected %v, got %v -- %v", test.title, url, u.Host+u.Path, getRouteTemplate(route)) t.Fatalf("(%v) URL error: %v -- %v", test.title, err, getRouteTemplate(route))
}
if expected, got := uri.String(), u.String(); expected != got {
t.Errorf("(%v) URL not equal: expected %v, got %v -- %v", test.title, expected, got, getRouteTemplate(route))
return return
} }
} }

19
route.go

@ -31,6 +31,8 @@ type Route struct {
skipClean bool skipClean bool
// If true, "/path/foo%2Fbar/to" will match the path "/path/{var}/to" // If true, "/path/foo%2Fbar/to" will match the path "/path/{var}/to"
useEncodedPath bool useEncodedPath bool
// The scheme used when building URLs.
buildScheme string
// If true, this route never matches: it is only used to build URLs. // If true, this route never matches: it is only used to build URLs.
buildOnly bool buildOnly bool
// The name used to build URLs. // The name used to build URLs.
@ -394,6 +396,9 @@ func (r *Route) Schemes(schemes ...string) *Route {
for k, v := range schemes { for k, v := range schemes {
schemes[k] = strings.ToLower(v) schemes[k] = strings.ToLower(v)
} }
if r.buildScheme == "" && len(schemes) > 0 {
r.buildScheme = schemes[0]
}
return r.addMatcher(schemeMatcher(schemes)) return r.addMatcher(schemeMatcher(schemes))
} }
@ -478,11 +483,13 @@ func (r *Route) URL(pairs ...string) (*url.URL, error) {
} }
var scheme, host, path string var scheme, host, path string
if r.regexp.host != nil { if r.regexp.host != nil {
// Set a default scheme.
scheme = "http"
if host, err = r.regexp.host.url(values); err != nil { if host, err = r.regexp.host.url(values); err != nil {
return nil, err return nil, err
} }
scheme = "http"
if r.buildScheme != "" {
scheme = r.buildScheme
}
} }
if r.regexp.path != nil { if r.regexp.path != nil {
if path, err = r.regexp.path.url(values); err != nil { if path, err = r.regexp.path.url(values); err != nil {
@ -514,10 +521,14 @@ func (r *Route) URLHost(pairs ...string) (*url.URL, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &url.URL{ u := &url.URL{
Scheme: "http", Scheme: "http",
Host: host, Host: host,
}, nil }
if r.buildScheme != "" {
u.Scheme = r.buildScheme
}
return u, nil
} }
// URLPath builds the path part of the URL for a route. See Route.URL(). // URLPath builds the path part of the URL for a route. See Route.URL().

Loading…
Cancel
Save