Skip to content

Commit

Permalink
Implement derived groups in routegroup package
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
eugene committed Feb 22, 2024
1 parent 5a94030 commit d4c1fa8
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 20 deletions.
61 changes: 47 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Expand All @@ -69,48 +70,80 @@ 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)
}
```

**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)
})
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.
Expand Down
33 changes: 31 additions & 2 deletions group.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
59 changes: 55 additions & 4 deletions group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -206,14 +206,65 @@ 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)

// 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)
})
})
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit d4c1fa8

Please sign in to comment.