From 27f0c4aaa94798795948cdc26ece94f65a9fa53d Mon Sep 17 00:00:00 2001 From: Bergutov Ruslan Date: Sun, 20 Sep 2020 19:38:41 +0500 Subject: [PATCH 1/6] pattern aliases --- mux.go | 10 ++++++++++ mux_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ regexp.go | 8 ++++++-- route.go | 13 +++++++++++-- 4 files changed, 67 insertions(+), 4 deletions(-) diff --git a/mux.go b/mux.go index 782a34b2..855b60aa 100644 --- a/mux.go +++ b/mux.go @@ -92,6 +92,8 @@ type routeConf struct { buildScheme string buildVarsFunc BuildVarsFunc + + registeredPatterns map[string]string } // returns an effective deep copy of `routeConf` @@ -122,6 +124,14 @@ func copyRouteRegexp(r *routeRegexp) *routeRegexp { return &c } +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..c4a5f495 100644 --- a/mux_test.go +++ b/mux_test.go @@ -216,6 +216,16 @@ 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, + }, } for _, test := range tests { t.Run(test.title, func(t *testing.T) { @@ -449,6 +459,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 regexp alias patterns passed through router", + route: NewRouter().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, + }, } 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 From cddaa9690db65a07dddceec3ee7ec80a860e6670 Mon Sep 17 00:00:00 2001 From: Bergutov Ruslan Date: Sun, 20 Sep 2020 20:27:37 +0500 Subject: [PATCH 2/6] docs and examples --- README.md | 27 +++++++++++++++++++++++++++ example_router_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 example_router_test.go diff --git a/README.md b/README.md index 35eea9f1..856e6711 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,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) +* [Alias Pattern Registration](#pattern-registration) * [Graceful Shutdown](#graceful-shutdown) * [Middleware](#middleware) * [Handling CORS Requests](#handling-cors-requests) @@ -435,6 +436,32 @@ func main() { } ``` +### Alias Pattern Registration + +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_router_test.go b/example_router_test.go new file mode 100644 index 00000000..9ce03756 --- /dev/null +++ b/example_router_test.go @@ -0,0 +1,25 @@ +package mux_test + +import ( + "fmt" + "github.com/gorilla/mux" + "net/http" +) + +// This example demonstrates alias pattern registration on router +func ExampleRouter_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 +} \ No newline at end of file From 446cd977e7a3dff913b6588894725ef4ca4058b2 Mon Sep 17 00:00:00 2001 From: Bergutov Ruslan Date: Sun, 20 Sep 2020 20:30:27 +0500 Subject: [PATCH 3/6] lint fix --- example_router_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example_router_test.go b/example_router_test.go index 9ce03756..c019430e 100644 --- a/example_router_test.go +++ b/example_router_test.go @@ -22,4 +22,4 @@ func ExampleRouter_RegisterPattern() { // Output // Match: true /category/abe193ed-e0bc-4e1b-8e3c-736d5b381b60 // Match: false /category/42 -} \ No newline at end of file +} From 95b356a939d7b05a3dd5c5c5f8acd565282caa75 Mon Sep 17 00:00:00 2001 From: Bergutov Ruslan Date: Mon, 12 Oct 2020 19:18:06 +0500 Subject: [PATCH 4/6] renamed --- README.md | 4 ++-- example_router_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 856e6711..84b4adb1 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,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) -* [Alias Pattern Registration](#pattern-registration) +* [Re-using Regular Expressions](#re-using-regular-expressions) * [Graceful Shutdown](#graceful-shutdown) * [Middleware](#middleware) * [Handling CORS Requests](#handling-cors-requests) @@ -436,7 +436,7 @@ func main() { } ``` -### Alias Pattern Registration +### 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: diff --git a/example_router_test.go b/example_router_test.go index c019430e..a66ae5ea 100644 --- a/example_router_test.go +++ b/example_router_test.go @@ -7,7 +7,7 @@ import ( ) // This example demonstrates alias pattern registration on router -func ExampleRouter_RegisterPattern() { +func ExampleRegisterPattern() { 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}") From 54dd42ca722b60367d5b4285085e48738324f3f1 Mon Sep 17 00:00:00 2001 From: Bergutov Ruslan Date: Mon, 12 Oct 2020 19:24:28 +0500 Subject: [PATCH 5/6] fix --- example_router_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example_router_test.go b/example_router_test.go index a66ae5ea..c019430e 100644 --- a/example_router_test.go +++ b/example_router_test.go @@ -7,7 +7,7 @@ import ( ) // This example demonstrates alias pattern registration on router -func ExampleRegisterPattern() { +func ExampleRouter_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}") From 306e54f076faa0862d3aa12864a71a0232307dbd Mon Sep 17 00:00:00 2001 From: Bergutov Ruslan Date: Mon, 6 Jun 2022 20:50:41 +0500 Subject: [PATCH 6/6] Negative test cases, comments for new code --- example_route_test.go | 18 ++++++++++++++++++ example_router_test.go | 25 ------------------------- mux.go | 4 ++++ mux_test.go | 20 +++++++++++++++----- 4 files changed, 37 insertions(+), 30 deletions(-) delete mode 100644 example_router_test.go 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/example_router_test.go b/example_router_test.go deleted file mode 100644 index c019430e..00000000 --- a/example_router_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package mux_test - -import ( - "fmt" - "github.com/gorilla/mux" - "net/http" -) - -// This example demonstrates alias pattern registration on router -func ExampleRouter_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 580f9ba8..bcd73608 100644 --- a/mux.go +++ b/mux.go @@ -95,6 +95,7 @@ type routeConf struct { buildVarsFunc BuildVarsFunc + // Map of registered pattern aliases registeredPatterns map[string]string } @@ -126,6 +127,9 @@ 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{} diff --git a/mux_test.go b/mux_test.go index c4a5f495..29889861 100644 --- a/mux_test.go +++ b/mux_test.go @@ -226,6 +226,16 @@ func TestHost(t *testing.T) { 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) { @@ -480,14 +490,14 @@ func TestPath(t *testing.T) { shouldMatch: true, }, { - title: "Path route with regexp alias patterns passed through router", - route: NewRouter().RegisterPattern("digits", "[0-9]+").Path("/{id:digits}"), - request: newRequest("GET", "http://localhost/1"), + 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: "/1", + path: "/letters", pathTemplate: `/{id:digits}`, - shouldMatch: true, + shouldMatch: false, }, }