diff --git a/README.md b/README.md index c7e6872b..d7658e61 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ The name mux stands for "HTTP request multiplexer". Like the standard `http.Serv * [Serving Single Page Applications](#serving-single-page-applications) (e.g. React, Vue, Ember.js, etc.) * [Registered URLs](#registered-urls) * [Walking Routes](#walking-routes) +* [Re-using Regular Expressions](#re-using-regular-expressions) * [Graceful Shutdown](#graceful-shutdown) * [Middleware](#middleware) * [Handling CORS Requests](#handling-cors-requests) @@ -439,6 +440,32 @@ func main() { } ``` +### Re-using Regular Expressions + +There can be a situation when you often need to specify some complex regular expressions inside your paths, e.g. uuid. This can be easily shorthanded: + +```go + +package main + +import ( + "net/http" + "github.com/gorilla/mux" +) + +func handler(w http.ResponseWriter, r *http.Request) { + return +} + +func main() { + r := mux.NewRouter().RegisterPattern("uuid", "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}") + r.HandleFunc("/products/{id:uuid}", handler) + r.HandleFunc("/articles/{id:uuid}", handler) + r.HandleFunc("/authors/{id:uuid}", handler) +} +``` + + ### Graceful Shutdown Go 1.8 introduced the ability to [gracefully shutdown](https://golang.org/doc/go1.8#http_shutdown) a `*http.Server`. Here's how to do that alongside `mux`: diff --git a/example_route_test.go b/example_route_test.go index 11255707..963291b6 100644 --- a/example_route_test.go +++ b/example_route_test.go @@ -49,3 +49,21 @@ func ExampleRoute_HeadersRegexp_exactMatch() { // Match: true ["https://example.co"] // Match: false ["https://example.co.uk"] } + +// This example demonstrates alias pattern registration and usage on router +func ExampleRoute_RegisterPattern() { + + r := mux.NewRouter().RegisterPattern("uuid", "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}") + route := r.Path("/category/{id:uuid}") + + yes, _ := http.NewRequest("GET", "example.co/category/abe193ed-e0bc-4e1b-8e3c-736d5b381b60", nil) + no, _ := http.NewRequest("GET", "example.co/category/42", nil) + + mathInfo := &mux.RouteMatch{} + fmt.Printf("Match: %v %q\n", route.Match(yes, mathInfo), yes.URL.Path) + fmt.Printf("Match: %v %q\n", route.Match(no, mathInfo), no.URL.Path) + + // Output + // Match: true /category/abe193ed-e0bc-4e1b-8e3c-736d5b381b60 + // Match: false /category/42 +} diff --git a/mux.go b/mux.go index f126a602..bcd73608 100644 --- a/mux.go +++ b/mux.go @@ -94,6 +94,9 @@ type routeConf struct { buildScheme string buildVarsFunc BuildVarsFunc + + // Map of registered pattern aliases + registeredPatterns map[string]string } // returns an effective deep copy of `routeConf` @@ -124,6 +127,17 @@ func copyRouteRegexp(r *routeRegexp) *routeRegexp { return &c } +// RegisterPattern registers an alias for a frequently repeated regular expression. +// +// It can be used for some popular regular expressions, e.g. uuid, number and etc. +func (r *Router) RegisterPattern(alias string, pattern string) *Router { + if r.registeredPatterns == nil { + r.registeredPatterns = map[string]string{} + } + r.registeredPatterns[alias] = pattern + return r +} + // Match attempts to match the given request against the router's registered routes. // // If the request matches a route of this router or one of its subrouters the Route, diff --git a/mux_test.go b/mux_test.go index 2d8d2b3e..29889861 100644 --- a/mux_test.go +++ b/mux_test.go @@ -216,6 +216,26 @@ func TestHost(t *testing.T) { hostTemplate: `{v-1:[a-z]{3}}.{v-2:[a-z]{3}}.{v-3:[a-z]{3}}`, shouldMatch: true, }, + { + title: "Host route with alias patterns", + route: new(Route).RegisterPattern("version", "[a-z]{3}").Host("{v-1:version}.{v-2:version}.{v-3:version}"), + request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), + vars: map[string]string{"v-1": "aaa", "v-2": "bbb", "v-3": "ccc"}, + host: "aaa.bbb.ccc", + path: "", + hostTemplate: `{v-1:version}.{v-2:version}.{v-3:version}`, + shouldMatch: true, + }, + { + title: "Host route with not matched alias patterns", + route: new(Route).RegisterPattern("pin", "[1-9]{4}").Host("{v-1:pin}.{v-2:pin}.{v-3:pin}"), + request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), + vars: map[string]string{"v-1": "aaa", "v-2": "bbb", "v-3": "ccc"}, + host: "aaa.bbb.ccc", + path: "", + hostTemplate: `{v-1:pin}.{v-2:pin}.{v-3:pin}`, + shouldMatch: false, + }, } for _, test := range tests { t.Run(test.title, func(t *testing.T) { @@ -449,6 +469,36 @@ func TestPath(t *testing.T) { pathTemplate: `/{category:a|b/c}/{product}/{id:[0-9]+}`, shouldMatch: true, }, + { + title: "Path route with regexp alias patterns", + route: new(Route).RegisterPattern("digits", "[0-9]+").Path("/{id:digits}"), + request: newRequest("GET", "http://localhost/1"), + vars: map[string]string{"id": "1"}, + host: "", + path: "/1", + pathTemplate: `/{id:digits}`, + shouldMatch: true, + }, + { + title: "Path route with regexp alias patterns", + route: new(Route).RegisterPattern("uuid", "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}").Path("/{category:uuid}/{product:uuid}"), + request: newRequest("GET", "http://localhost/dce51145-5cc3-4b54-bfb0-7bdb64a67e4d/a385ddcb-278e-4234-93dd-4d7b0fcb95c1"), + vars: map[string]string{"category": "dce51145-5cc3-4b54-bfb0-7bdb64a67e4d", "product": "a385ddcb-278e-4234-93dd-4d7b0fcb95c1"}, + host: "", + path: "/dce51145-5cc3-4b54-bfb0-7bdb64a67e4d/a385ddcb-278e-4234-93dd-4d7b0fcb95c1", + pathTemplate: `/{category:uuid}/{product:uuid}`, + shouldMatch: true, + }, + { + title: "Path route with not matched regexp alias patterns", + route: new(Route).RegisterPattern("digits", "[0-9]+").Path("/{id:digits}"), + request: newRequest("GET", "http://localhost/letters"), + vars: map[string]string{"id": "1"}, + host: "", + path: "/letters", + pathTemplate: `/{id:digits}`, + shouldMatch: false, + }, } for _, test := range tests { diff --git a/regexp.go b/regexp.go index 0144842b..7d400152 100644 --- a/regexp.go +++ b/regexp.go @@ -15,8 +15,9 @@ import ( ) type routeRegexpOptions struct { - strictSlash bool - useEncodedPath bool + strictSlash bool + useEncodedPath bool + registeredPatterns map[string]string } type regexpType int @@ -85,6 +86,9 @@ func newRouteRegexp(tpl string, typ regexpType, options routeRegexpOptions) (*ro return nil, fmt.Errorf("mux: missing name or pattern in %q", tpl[idxs[i]:end]) } + if registeredPattern, ok := options.registeredPatterns[patt]; ok { + patt = registeredPattern + } // Build the regexp pattern. fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), varGroupName(i/2), patt) diff --git a/route.go b/route.go index 750afe57..b27375d1 100644 --- a/route.go +++ b/route.go @@ -31,6 +31,14 @@ type Route struct { routeConf } +func (r *Route) RegisterPattern(alias string, pattern string) *Route { + if r.registeredPatterns == nil { + r.registeredPatterns = map[string]string{} + } + r.registeredPatterns[alias] = pattern + return r +} + // SkipClean reports whether path cleaning is enabled for this route via // Router.SkipClean. func (r *Route) SkipClean() bool { @@ -184,8 +192,9 @@ func (r *Route) addRegexpMatcher(tpl string, typ regexpType) error { } } rr, err := newRouteRegexp(tpl, typ, routeRegexpOptions{ - strictSlash: r.strictSlash, - useEncodedPath: r.useEncodedPath, + strictSlash: r.strictSlash, + useEncodedPath: r.useEncodedPath, + registeredPatterns: r.registeredPatterns, }) if err != nil { return err