From d4c1fa8384279353fa033bcf5483562dcb20a135 Mon Sep 17 00:00:00 2001 From: eugene Date: Thu, 22 Feb 2024 13:38:40 -0600 Subject: [PATCH] Implement derived groups in routegroup package The package now allows creation of derived groups with the same middleware stack as the original, using newly implemented Group and Mount methods. Additionally, the method Set is renamed to Route for better clarity and the documentation is also updated accordingly. --- README.md | 61 +++++++++++++++++++++++++++++++++++++++------------ group.go | 33 ++++++++++++++++++++++++++-- group_test.go | 59 +++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 133 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index ff9271a..fe17ca5 100644 --- a/README.md +++ b/README.md @@ -44,13 +44,14 @@ Add routes to your group, optionally with middleware: ``` **Creating a Nested Route Group** -For routes under a specific path prefix: +For routes under a specific path prefix `Mount` method can be used to create a nested group: ```go apiGroup := routegroup.Mount(mux, "/api") apiGroup.Use(loggingMiddleware, corsMiddleware) apiGroup.Handle("/v1", apiV1Handler) apiGroup.Handle("/v2", apiV2Handler) + ``` **Complete Example** @@ -69,20 +70,22 @@ import ( func main() { mux := http.NewServeMux() - apiGroup := routegroup.WithBasePath(mux, "/api") + apiGroup := routegroup.Mount(mux, "/api") // add middleware - apiGroup.Use(func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Println("Request received") - next.ServeHTTP(w, r) - }) - }) + apiGroup.Use(loggingMiddleware, corsMiddleware) - // Route handling + // route handling apiGroup.Handle("GET /hello", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello, API!")) }) + + // add another group with its own set of middlewares + protectedGroup := apiGroup.Group() + protectedGroup.Use(authMiddleware) + protectedGroup.Handle("GET /protected", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Protected API!")) + }) http.ListenAndServe(":8080", mux) } @@ -90,20 +93,20 @@ func main() { **Applying Middleware to Specific Routes** -You can also apply middleware to specific routes: +You can also apply middleware to specific routes inside the group without modifying the group's middleware stack: ```go - apiGroup.With(corsMiddleware, helloHandler).Handle("GET /hello", helloHandler) + apiGroup.With(corsMiddleware, helloHandler).Handle("GET /hello",helloHandler) ``` -*Alternative Usage with `Set`* +**Alternative Usage with `Route`** -You can also use the `Set` method to add routes and middleware: +You can also use the `Route` method to add routes and middleware in a single function call: ```go mux := http.NewServeMux() group := routegroup.New(mux) - group.Set(b func(*routegroup.Bundle) { + group.Route(b func(*routegroup.Bundle) { b.Use(loggingMiddleware, corsMiddleware) b.Handle("GET /hello", helloHandler) b.Handle("GET /bye", byeHandler) @@ -111,6 +114,36 @@ You can also use the `Set` method to add routes and middleware: http.ListenAndServe(":8080", mux) ``` +### Using derived groups + +In some instances, it's practical to create an initial group that includes a set of middlewares, and then derive all other groups from it. This approach guarantees that every group incorporates a common set of middlewares as a foundation, allowing each to add its specific middlewares. To facilitate this scenario, `routegrou`p offers both `Bundle.Group` and `Bundle.Mount` methods, and it also implements the `http.Handler` interface. The following example illustrates how to utilize derived groups: + +```go + // create a new bundle with a base set of middlewares + // note: the bundle is also http.Handler and can be passed to http.ListenAndServe + mux := routegroup.New(http.NewServeMux()) + mux.Use(loggingMiddleware, corsMiddleware) + + // add a new group with its own set of middlewares + // this group will inherit the middlewares from the base group + apiGroup := mux.Group() + apiGroup.Use(apiMiddleware) + apiGroup.Handle("GET /hello", helloHandler) + apiGroup.Handle("GET /bye", byeHandler) + + + // mount another group for the /admin path with its own set of middlewares, + // using `Set` method to show the alternative usage. + // this group will inherit the middlewares from the base group as well + mux.Mount("/admin").Route(func(b *routegroup.Bundle) { + b.Use(adminMiddleware) + b.Handle("POST /do", doHandler) + }) + + // start the server, passing the wrapped mux as the handler + http.ListenAndServe(":8080", mux) +``` + ## Contributing Contributions to `routegroup` are welcome! Please submit a pull request or open an issue for any bugs or feature requests. diff --git a/group.go b/group.go index 268186f..3cd5eb0 100644 --- a/group.go +++ b/group.go @@ -29,6 +29,35 @@ func Mount(mux *http.ServeMux, basePath string) *Bundle { } } +// ServeHTTP implements the http.Handler interface +func (b *Bundle) ServeHTTP(w http.ResponseWriter, r *http.Request) { + b.mux.ServeHTTP(w, r) +} + +// Group creates a new group with the same middleware stack as the original on top of the existing bundle. +func (b *Bundle) Group() *Bundle { + // copy the middlewares to avoid modifying the original + middlewares := make([]func(http.Handler) http.Handler, len(b.middlewares)) + copy(middlewares, b.middlewares) + return &Bundle{ + mux: b.mux, + basePath: b.basePath, + middlewares: middlewares, + } +} + +// Mount creates a new group with a specified base path on top of the existing bundle. +func (b *Bundle) Mount(basePath string) *Bundle { + // copy the middlewares to avoid modifying the original + middlewares := make([]func(http.Handler) http.Handler, len(b.middlewares)) + copy(middlewares, b.middlewares) + return &Bundle{ + mux: b.mux, + basePath: basePath, + middlewares: middlewares, + } +} + // Use adds middleware(s) to the Group. func (b *Bundle) Use(middleware func(http.Handler) http.Handler, more ...func(http.Handler) http.Handler) { b.middlewares = append(b.middlewares, middleware) @@ -77,7 +106,7 @@ func (b *Bundle) Handle(path string, handler http.HandlerFunc) { b.mux.HandleFunc(path, wrap(handler, b.middlewares...).ServeHTTP) } -// Set allows for configuring the Group. -func (b *Bundle) Set(configureFn func(*Bundle)) { +// Route allows for configuring the Group inside the configureFn function. +func (b *Bundle) Route(configureFn func(*Bundle)) { configureFn(b) } diff --git a/group_test.go b/group_test.go index cca58b5..c9c4ca6 100644 --- a/group_test.go +++ b/group_test.go @@ -62,7 +62,7 @@ func TestGroupSet(t *testing.T) { group := routegroup.New(mux) // configure the group using Set - group.Set(func(g *routegroup.Bundle) { + group.Route(func(g *routegroup.Bundle) { g.Use(testMiddleware) g.Handle("/test", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) @@ -206,6 +206,57 @@ func TestHTTPServerMethodAndPathHandling(t *testing.T) { }) } +func TestHTTPServerWithDerived(t *testing.T) { + // create a new bundle with default middleware + bundle := routegroup.New(http.NewServeMux()) + bundle.Use(testMiddleware) + + // mount a group with additional middleware on /api + group1 := bundle.Mount("/api") + group1.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("X-API-Middleware", "applied") + next.ServeHTTP(w, r) + }) + }) + + group1.Handle("GET /test", func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte("GET test method handler")) + }) + + // add another group with middleware + bundle.Group().Route(func(g *routegroup.Bundle) { + g.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("X-Blah-Middleware", "true") + next.ServeHTTP(w, r) + }) + }) + g.Handle("GET /blah/blah", func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte("GET blah method handler")) + }) + }) + + testServer := httptest.NewServer(bundle) + defer testServer.Close() + + resp, err := http.Get(testServer.URL + "/api/test") + assert.NoError(t, err) + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.Equal(t, "GET test method handler", string(body)) + assert.Equal(t, "true", resp.Header.Get("X-Test-Middleware")) + + resp, err = http.Get(testServer.URL + "/blah/blah") + assert.NoError(t, err) + defer resp.Body.Close() + body, err = io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.Equal(t, "GET blah method handler", string(body)) + assert.Equal(t, "true", resp.Header.Get("X-Blah-Middleware")) +} + func ExampleNew() { mux := http.NewServeMux() group := routegroup.New(mux) @@ -213,7 +264,7 @@ func ExampleNew() { // apply middleware to the group group.Use(func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("X-Test-Middleware", "true") + w.Header().Add("X-Mounted-Middleware", "true") next.ServeHTTP(w, r) }) }) @@ -258,12 +309,12 @@ func ExampleMount() { } } -func ExampleBundle_Set() { +func ExampleBundle_Route() { mux := http.NewServeMux() group := routegroup.New(mux) // configure the group using Set - group.Set(func(g *routegroup.Bundle) { + group.Route(func(g *routegroup.Bundle) { // apply middleware to the group g.Use(func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {