Regex capturing group.

This commit is contained in:
Ludovic Fernandez 2017-10-23 10:20:02 +02:00 committed by Traefiker
parent afbad56012
commit 81f7aa9df2
6 changed files with 152 additions and 42 deletions

2
glide.lock generated
View file

@ -89,7 +89,7 @@ imports:
- name: github.com/containous/flaeg
version: b5d2dc5878df07c2d74413348186982e7b865871
- name: github.com/containous/mux
version: af6ea922f7683d9706834157e6b0610e22ccb2db
version: 06ccd3e75091eb659b1d720cda0e16bc7057954c
- name: github.com/containous/staert
version: 1e26a71803e428fd933f5f9c8e50a26878f53147
- name: github.com/coreos/etcd

View file

@ -136,6 +136,57 @@ func TestPriorites(t *testing.T) {
assert.NotEqual(t, foobarMatcher.Handler, fooHandler, "Error matching priority")
}
func TestHostRegexp(t *testing.T) {
testCases := []struct {
desc string
hostExp string
urls map[string]bool
}{
{
desc: "capturing group",
hostExp: "{subdomain:(foo\\.)?bar\\.com}",
urls: map[string]bool{
"http://foo.bar.com": true,
"http://bar.com": true,
"http://fooubar.com": false,
"http://barucom": false,
"http://barcom": false,
},
},
{
desc: "non capturing group",
hostExp: "{subdomain:(?:foo\\.)?bar\\.com}",
urls: map[string]bool{
"http://foo.bar.com": true,
"http://bar.com": true,
"http://fooubar.com": false,
"http://barucom": false,
"http://barcom": false,
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
rls := &Rules{
route: &serverRoute{
route: &mux.Route{},
},
}
rt := rls.hostRegexp(test.hostExp)
for testURL, match := range test.urls {
req := testhelpers.MustNewRequest(http.MethodGet, testURL, nil)
assert.Equal(t, match, rt.Match(req, &mux.RouteMatch{}))
}
})
}
}
type fakeHandler struct {
name string
}

View file

@ -57,11 +57,6 @@ calling mux.Vars():
vars := mux.Vars(request)
category := vars["category"]
Note that if any capturing groups are present, mux will panic() during parsing. To prevent
this, convert any capturing groups to non-capturing, e.g. change "/{sort:(asc|desc)}" to
"/{sort:(?:asc|desc)}". This is a change from prior versions which behaved unpredictably
when capturing groups were present.
And this is all you need to know about the basic usage. More advanced options
are explained below.

View file

@ -11,7 +11,10 @@ import (
"path"
"regexp"
"sort"
"strings"
)
var (
ErrMethodMismatch = errors.New("method is not allowed")
)
// NewRouter returns a new router instance.
@ -40,6 +43,10 @@ func NewRouter() *Router {
type Router struct {
// Configurable Handler to be used when no route matches.
NotFoundHandler http.Handler
// Configurable Handler to be used when the request method does not match the route.
MethodNotAllowedHandler http.Handler
// Parent route, if this is a subrouter.
parent parentRoute
// Routes to be matched, in order.
@ -66,6 +73,11 @@ func (r *Router) Match(req *http.Request, match *RouteMatch) bool {
}
}
if match.MatchErr == ErrMethodMismatch && r.MethodNotAllowedHandler != nil {
match.Handler = r.MethodNotAllowedHandler
return true
}
// Closest match for a router (includes sub-routers)
if r.NotFoundHandler != nil {
match.Handler = r.NotFoundHandler
@ -82,7 +94,7 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if !r.skipClean {
path := req.URL.Path
if r.useEncodedPath {
path = getPath(req)
path = req.URL.EscapedPath()
}
// Clean path to canonical form and redirect.
if p := cleanPath(path); p != path {
@ -106,9 +118,15 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
req = setVars(req, match.Vars)
req = setCurrentRoute(req, match.Route)
}
if handler == nil && match.MatchErr == ErrMethodMismatch {
handler = methodNotAllowedHandler()
}
if handler == nil {
handler = http.NotFoundHandler()
}
if !r.KeepContext {
defer contextClear(req)
}
@ -356,6 +374,11 @@ type RouteMatch struct {
Route *Route
Handler http.Handler
Vars map[string]string
// MatchErr is set to appropriate matching error
// It is set to ErrMethodMismatch if there is a mismatch in
// the request method and route method
MatchErr error
}
type contextKey int
@ -397,28 +420,6 @@ func setCurrentRoute(r *http.Request, val interface{}) *http.Request {
// Helpers
// ----------------------------------------------------------------------------
// getPath returns the escaped path if possible; doing what URL.EscapedPath()
// which was added in go1.5 does
func getPath(req *http.Request) string {
if req.RequestURI != "" {
// Extract the path from RequestURI (which is escaped unlike URL.Path)
// as detailed here as detailed in https://golang.org/pkg/net/url/#URL
// for < 1.5 server side workaround
// http://localhost/path/here?v=1 -> /path/here
path := req.RequestURI
path = strings.TrimPrefix(path, req.URL.Scheme+`://`)
path = strings.TrimPrefix(path, req.URL.Host)
if i := strings.LastIndex(path, "?"); i > -1 {
path = path[:i]
}
if i := strings.LastIndex(path, "#"); i > -1 {
path = path[:i]
}
return path
}
return req.URL.Path
}
// cleanPath returns the canonical path for p, eliminating . and .. elements.
// Borrowed from the net/http package.
func cleanPath(p string) string {
@ -557,3 +558,12 @@ func matchMapWithRegex(toCheck map[string]*regexp.Regexp, toMatch map[string][]s
}
return true
}
// methodNotAllowed replies to the request with an HTTP status code 405.
func methodNotAllowed(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusMethodNotAllowed)
}
// methodNotAllowedHandler returns a simple request handler
// that replies to each request with a status code 405.
func methodNotAllowedHandler() http.Handler { return http.HandlerFunc(methodNotAllowed) }

View file

@ -109,13 +109,6 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash,
if errCompile != nil {
return nil, errCompile
}
// Check for capturing groups which used to work in older versions
if reg.NumSubexp() != len(idxs)/2 {
panic(fmt.Sprintf("route %s contains capture groups in its regexp. ", template) +
"Only non-capturing groups are accepted: e.g. (?:pattern) instead of (pattern)")
}
// Done!
return &routeRegexp{
template: template,
@ -141,7 +134,7 @@ type routeRegexp struct {
matchQuery bool
// The strictSlash value defined on the route, but disabled if PathPrefix was used.
strictSlash bool
// Determines whether to use encoded path from getPath function or unencoded
// Determines whether to use encoded req.URL.EnscapedPath() or unencoded
// req.URL.Path for path matching
useEncodedPath bool
// Expanded regexp.
@ -162,7 +155,7 @@ func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool {
}
path := req.URL.Path
if r.useEncodedPath {
path = getPath(req)
path = req.URL.EscapedPath()
}
return r.regexp.MatchString(path)
}
@ -272,7 +265,7 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route)
}
path := req.URL.Path
if r.useEncodedPath {
path = getPath(req)
path = req.URL.EscapedPath()
}
// Store path variables.
if v.path != nil {
@ -320,7 +313,14 @@ func getHost(r *http.Request) string {
}
func extractVars(input string, matches []int, names []string, output map[string]string) {
for i, name := range names {
output[name] = input[matches[2*i+2]:matches[2*i+3]]
matchesCount := 0
prevEnd := -1
for i := 2; i < len(matches) && matchesCount < len(names); i += 2 {
if prevEnd < matches[i+1] {
value := input[matches[i]:matches[i+1]]
output[names[matchesCount]] = value
prevEnd = matches[i+1]
matchesCount++
}
}
}

View file

@ -54,12 +54,27 @@ func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
if r.buildOnly || r.err != nil {
return false
}
var matchErr error
// Match everything.
for _, m := range r.matchers {
if matched := m.Match(req, match); !matched {
if _, ok := m.(methodMatcher); ok {
matchErr = ErrMethodMismatch
continue
}
matchErr = nil
return false
}
}
if matchErr != nil {
match.MatchErr = matchErr
return false
}
match.MatchErr = nil
// Yay, we have a match. Let's collect some info about it.
if match.Route == nil {
match.Route = r
@ -70,6 +85,7 @@ func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
if match.Vars == nil {
match.Vars = make(map[string]string)
}
// Set variables.
if r.regexp != nil {
r.regexp.setMatch(req, match, r)
@ -607,6 +623,44 @@ func (r *Route) GetPathRegexp() (string, error) {
return r.regexp.path.regexp.String(), nil
}
// GetQueriesRegexp returns the expanded regular expressions used to match the
// route queries.
// This is useful for building simple REST API documentation and for instrumentation
// against third-party services.
// An empty list will be returned if the route does not have queries.
func (r *Route) GetQueriesRegexp() ([]string, error) {
if r.err != nil {
return nil, r.err
}
if r.regexp == nil || r.regexp.queries == nil {
return nil, errors.New("mux: route doesn't have queries")
}
var queries []string
for _, query := range r.regexp.queries {
queries = append(queries, query.regexp.String())
}
return queries, nil
}
// GetQueriesTemplates returns the templates used to build the
// query matching.
// This is useful for building simple REST API documentation and for instrumentation
// against third-party services.
// An empty list will be returned if the route does not define queries.
func (r *Route) GetQueriesTemplates() ([]string, error) {
if r.err != nil {
return nil, r.err
}
if r.regexp == nil || r.regexp.queries == nil {
return nil, errors.New("mux: route doesn't have queries")
}
var queries []string
for _, query := range r.regexp.queries {
queries = append(queries, query.template)
}
return queries, nil
}
// GetMethods returns the methods the route matches against
// This is useful for building simple REST API documentation and for instrumentation
// against third-party services.