From f3eb928f995aff1af5ce71d453696dcca5d684d3 Mon Sep 17 00:00:00 2001 From: Louie Liu Date: Thu, 12 Sep 2024 11:08:41 -0400 Subject: [PATCH 1/3] Added CurrentRoute and PathTemplate functions to store and retrieve the path template --- go.mod | 7 +++++++ route.go | 23 +++++++++++++++++++---- router.go | 24 ++++++++++++++++++------ router_test.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index 3768a0e..fa1ea24 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,10 @@ module github.com/shellfu/muxer go 1.18 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.9.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/route.go b/route.go index 38c5522..eee74c3 100644 --- a/route.go +++ b/route.go @@ -1,6 +1,7 @@ package muxer import ( + "errors" "net/http" "regexp" ) @@ -11,10 +12,11 @@ It contains the regular expression that matches the request path, the HTTP metho the handler to be executed for that request, and the parameter names extracted from the path. */ type Route struct { - path *regexp.Regexp - method string - handler http.Handler - params []string + path *regexp.Regexp + method string + handler http.Handler + params []string + template string } func (r *Route) match(path string) map[string]string { @@ -30,3 +32,16 @@ func (r *Route) match(path string) map[string]string { return params } + +// PathTemplate retrieves the path template of the current route +func (r *Route) PathTemplate() (string, error) { + if r == nil { + return "", errors.New("route is nil, no template") + } + + if r.template == "" { + return r.template, errors.New("template is empty") + } + + return r.template, nil +} diff --git a/router.go b/router.go index d9c3a9a..5dfe32d 100644 --- a/router.go +++ b/router.go @@ -12,6 +12,8 @@ type contextKey string const ( // ParamsKey is the key used to store the extracted parameters in the request context. ParamsKey contextKey = "params" + // routeContextKey is the key used to store the matched route in the request context + routeContextKey contextKey = "matched_route" ) /* @@ -131,19 +133,20 @@ func (r *Router) HandleRoute(method, path string, handler http.HandlerFunc) { // Parse path to extract parameter names paramNames := make([]string, 0) re := regexp.MustCompile(`:([\w-]+)`) - path = re.ReplaceAllStringFunc(path, func(m string) string { + pathRegex := re.ReplaceAllStringFunc(path, func(m string) string { paramName := m[1:] paramNames = append(paramNames, paramName) return `([-\w.]+)` }) - exactPath := regexp.MustCompile("^" + path + "$") + exactPath := regexp.MustCompile("^" + pathRegex + "$") r.routes = append(r.routes, Route{ - method: method, - path: exactPath, - handler: handler, - params: paramNames, + method: method, + path: exactPath, + handler: handler, + params: paramNames, + template: path, // Save the original template }) } @@ -208,6 +211,7 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { ctx := req.Context() ctx = context.WithValue(ctx, ParamsKey, params) + ctx = context.WithValue(ctx, routeContextKey, &route) handler := route.handler for i := len(r.middleware) - 1; i >= 0; i-- { @@ -256,3 +260,11 @@ the given order before executing the main handler. func (r *Router) Use(middleware ...func(http.Handler) http.Handler) { r.middleware = append(r.middleware, middleware...) } + +// GetPathTemplate retrieves the path template of the current route from the current request context. +func GetPathTemplate(req *http.Request) string { + if route, ok := req.Context().Value(routeContextKey).(*Route); ok { + return route.template + } + return "not_found" +} diff --git a/router_test.go b/router_test.go index 354fe03..12a2984 100644 --- a/router_test.go +++ b/router_test.go @@ -2,6 +2,7 @@ package muxer import ( "context" + "errors" "fmt" "io" "io/ioutil" @@ -13,6 +14,7 @@ import ( "testing" . "github.com/shellfu/muxer/middleware" + "github.com/stretchr/testify/assert" ) func TestRouter(t *testing.T) { @@ -519,3 +521,50 @@ func TestEnableCORSOption(t *testing.T) { }) } } + +func TestPathTemplate(t *testing.T) { + tests := []struct { + name string + route *Route + expectedOutput string + expectedError error + }{ + { + name: "Error with nil Route", + route: nil, + expectedOutput: "", + expectedError: errors.New("route is nil, no template"), + }, + { + name: "Error with empty template", + route: &Route{template: ""}, + expectedOutput: "", + expectedError: errors.New("template is empty"), + }, + { + name: "Valid Route with Template and path param", + route: &Route{template: "/users/:id"}, + expectedOutput: "/users/:id", + expectedError: nil, + }, + { + name: "Valid Route with simple Template", + route: &Route{template: "/metrics"}, + expectedOutput: "/metrics", + expectedError: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + output, err := tt.route.PathTemplate() + + assert.Equal(t, tt.expectedOutput, output) + if tt.expectedError != nil { + assert.EqualError(t, err, tt.expectedError.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} From ed0868d654a18290f0269e10de4efe8c31e6526f Mon Sep 17 00:00:00 2001 From: Louie Liu Date: Thu, 12 Sep 2024 13:17:56 -0400 Subject: [PATCH 2/3] Removed testify dependency. Added tests for CurrentRoute. Made contextKey public --- go.mod | 7 ------ router.go | 19 +++++++++------- router_test.go | 59 ++++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 66 insertions(+), 19 deletions(-) diff --git a/go.mod b/go.mod index fa1ea24..3768a0e 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,3 @@ module github.com/shellfu/muxer go 1.18 - -require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/testify v1.9.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/router.go b/router.go index 5dfe32d..ec8fca0 100644 --- a/router.go +++ b/router.go @@ -12,8 +12,8 @@ type contextKey string const ( // ParamsKey is the key used to store the extracted parameters in the request context. ParamsKey contextKey = "params" - // routeContextKey is the key used to store the matched route in the request context - routeContextKey contextKey = "matched_route" + // RouteContextKey is the key used to store the matched route in the request context + RouteContextKey contextKey = "matched_route" ) /* @@ -211,7 +211,7 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { ctx := req.Context() ctx = context.WithValue(ctx, ParamsKey, params) - ctx = context.WithValue(ctx, routeContextKey, &route) + ctx = context.WithValue(ctx, RouteContextKey, &route) handler := route.handler for i := len(r.middleware) - 1; i >= 0; i-- { @@ -261,10 +261,13 @@ func (r *Router) Use(middleware ...func(http.Handler) http.Handler) { r.middleware = append(r.middleware, middleware...) } -// GetPathTemplate retrieves the path template of the current route from the current request context. -func GetPathTemplate(req *http.Request) string { - if route, ok := req.Context().Value(routeContextKey).(*Route); ok { - return route.template +// CurrentRoute returns the matched route for the current request, if any. +// This only works when called inside the handler of the matched route +// because the matched route is stored inside the request's context, +// which is wiped after the handler returns. +func CurrentRoute(r *http.Request) *Route { + if rv := r.Context().Value(RouteContextKey); rv != nil { + return rv.(*Route) } - return "not_found" + return nil } diff --git a/router_test.go b/router_test.go index 12a2984..cf2672f 100644 --- a/router_test.go +++ b/router_test.go @@ -14,7 +14,6 @@ import ( "testing" . "github.com/shellfu/muxer/middleware" - "github.com/stretchr/testify/assert" ) func TestRouter(t *testing.T) { @@ -559,11 +558,63 @@ func TestPathTemplate(t *testing.T) { t.Run(tt.name, func(t *testing.T) { output, err := tt.route.PathTemplate() - assert.Equal(t, tt.expectedOutput, output) + if tt.expectedOutput != output { + t.Errorf("expected output %v, got %v", tt.expectedOutput, output) + } if tt.expectedError != nil { - assert.EqualError(t, err, tt.expectedError.Error()) + if tt.expectedError.Error() != err.Error() { + t.Errorf("expected error %v, got %v", tt.expectedError, err) + } } else { - assert.NoError(t, err) + if err != nil { + t.Errorf("expected error to be nil, got %v", err) + } + } + }) + } +} + +func TestCurrentRoute(t *testing.T) { + route := &Route{template: "/users/:id"} + + tests := []struct { + name string + contextKey interface{} + contextValue interface{} + expectedRoute *Route + }{ + { + name: "Route in context", + contextKey: RouteContextKey, + contextValue: route, + expectedRoute: route, + }, + { + name: "No route in context", + contextKey: "some_other_key", + contextValue: "some_value", + expectedRoute: nil, + }, + { + name: "Empty context", + contextKey: nil, + contextValue: nil, + expectedRoute: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, _ := http.NewRequest(http.MethodGet, "/users/123", nil) + + if tt.contextKey != nil { + req = req.WithContext(context.WithValue(req.Context(), tt.contextKey, tt.contextValue)) + } + + result := CurrentRoute(req) + + if tt.expectedRoute != result { + t.Errorf("expected route %v got %v", tt.expectedRoute, result) } }) } From f97fd1143822bb11c8978b1c8c35efd51d1552c5 Mon Sep 17 00:00:00 2001 From: Louie Liu Date: Thu, 12 Sep 2024 13:41:01 -0400 Subject: [PATCH 3/3] Remove extraneous comment --- router.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/router.go b/router.go index ec8fca0..fc1da0d 100644 --- a/router.go +++ b/router.go @@ -146,7 +146,7 @@ func (r *Router) HandleRoute(method, path string, handler http.HandlerFunc) { path: exactPath, handler: handler, params: paramNames, - template: path, // Save the original template + template: path, }) }