Skip to content

Commit 82148b5

Browse files
authored
feat: add route groups (#39)
1 parent 171d83c commit 82148b5

File tree

4 files changed

+369
-0
lines changed

4 files changed

+369
-0
lines changed

group.go

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package kid
2+
3+
import "net/http"
4+
5+
// Group is for creating groups of routes.
6+
//
7+
// It doesn't actually group them but
8+
// it's kind of an abstraction to make it easier to make a group of routes.
9+
type Group struct {
10+
kid *Kid
11+
prefix string
12+
middlewares []MiddlewareFunc
13+
}
14+
15+
// newGroup returns a new group.
16+
func newGroup(k *Kid, prefix string, middlewares ...MiddlewareFunc) Group {
17+
return Group{kid: k, prefix: prefix, middlewares: middlewares}
18+
}
19+
20+
// Get registers a new handler for the given path for GET method.
21+
//
22+
// Specifying middlewares is optional. Middlewares will only be applied to this route.
23+
func (g *Group) Get(path string, handler HandlerFunc, middlewares ...MiddlewareFunc) {
24+
g.Add(path, handler, []string{http.MethodGet}, middlewares...)
25+
}
26+
27+
// Post registers a new handler for the given path for POST method.
28+
//
29+
// Specifying middlewares is optional. Middlewares will only be applied to this route.
30+
func (g *Group) Post(path string, handler HandlerFunc, middlewares ...MiddlewareFunc) {
31+
g.Add(path, handler, []string{http.MethodPost}, middlewares...)
32+
}
33+
34+
// Put registers a new handler for the given path for PUT method.
35+
//
36+
// Specifying middlewares is optional. Middlewares will only be applied to this route.
37+
func (g *Group) Put(path string, handler HandlerFunc, middlewares ...MiddlewareFunc) {
38+
g.Add(path, handler, []string{http.MethodPut}, middlewares...)
39+
}
40+
41+
// Patch registers a new handler for the given path for PATCH method.
42+
//
43+
// Specifying middlewares is optional. Middlewares will only be applied to this route.
44+
func (g *Group) Patch(path string, handler HandlerFunc, middlewares ...MiddlewareFunc) {
45+
g.Add(path, handler, []string{http.MethodPatch}, middlewares...)
46+
}
47+
48+
// Delete registers a new handler for the given path for DELETE method.
49+
//
50+
// Specifying middlewares is optional. Middlewares will only be applied to this route.
51+
func (g *Group) Delete(path string, handler HandlerFunc, middlewares ...MiddlewareFunc) {
52+
g.Add(path, handler, []string{http.MethodDelete}, middlewares...)
53+
}
54+
55+
// Head registers a new handler for the given path for HEAD method.
56+
//
57+
// Specifying middlewares is optional. Middlewares will only be applied to this route.
58+
func (g *Group) Head(path string, handler HandlerFunc, middlewares ...MiddlewareFunc) {
59+
g.Add(path, handler, []string{http.MethodHead}, middlewares...)
60+
}
61+
62+
// Options registers a new handler for the given path for OPTIONS method.
63+
//
64+
// Specifying middlewares is optional. Middlewares will only be applied to this route.
65+
func (g *Group) Options(path string, handler HandlerFunc, middlewares ...MiddlewareFunc) {
66+
g.Add(path, handler, []string{http.MethodOptions}, middlewares...)
67+
}
68+
69+
// Connect registers a new handler for the given path for CONNECT method.
70+
//
71+
// Specifying middlewares is optional. Middlewares will only be applied to this route.
72+
func (g *Group) Connect(path string, handler HandlerFunc, middlewares ...MiddlewareFunc) {
73+
g.Add(path, handler, []string{http.MethodConnect}, middlewares...)
74+
}
75+
76+
// Trace registers a new handler for the given path for TRACE method.
77+
//
78+
// Specifying middlewares is optional. Middlewares will only be applied to this route.
79+
func (g *Group) Trace(path string, handler HandlerFunc, middlewares ...MiddlewareFunc) {
80+
g.Add(path, handler, []string{http.MethodTrace}, middlewares...)
81+
}
82+
83+
// Any registers a new handler for the given path for all of the HTTP methods.
84+
//
85+
// Specifying middlewares is optional. Middlewares will only be applied to this route.
86+
func (g *Group) Any(path string, handler HandlerFunc, middlewares ...MiddlewareFunc) {
87+
methods := []string{
88+
http.MethodGet, http.MethodPost, http.MethodPut,
89+
http.MethodPatch, http.MethodDelete, http.MethodHead,
90+
http.MethodOptions, http.MethodConnect, http.MethodTrace,
91+
}
92+
g.Add(path, handler, methods, middlewares...)
93+
}
94+
95+
// Add adds a route to the group routes.
96+
func (g *Group) Add(path string, handler HandlerFunc, methods []string, middlewares ...MiddlewareFunc) {
97+
path = g.prefix + path
98+
middlewares = g.combineMiddlewares(middlewares)
99+
100+
g.kid.Add(path, handler, methods, middlewares...)
101+
}
102+
103+
// Group creates a sub-group for that group.
104+
func (g *Group) Group(prefix string, middlewares ...MiddlewareFunc) Group {
105+
prefix = g.prefix + prefix
106+
gMiddlewares := g.combineMiddlewares(middlewares)
107+
return newGroup(g.kid, prefix, gMiddlewares...)
108+
}
109+
110+
// combineMiddlewares combines the given middlewares with the group middlewares and returns the combined middlewares.
111+
func (g *Group) combineMiddlewares(middlewares []MiddlewareFunc) []MiddlewareFunc {
112+
gMiddlewares := make([]MiddlewareFunc, 0, len(g.middlewares)+len(middlewares))
113+
if cap(gMiddlewares) == 0 {
114+
return nil
115+
}
116+
117+
gMiddlewares = append(gMiddlewares, g.middlewares...)
118+
gMiddlewares = append(gMiddlewares, middlewares...)
119+
120+
return gMiddlewares
121+
}

group_test.go

+223
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
package kid
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func registerHandlers(g Group) {
13+
g.Get("/path", func(c *Context) error {
14+
return c.JSON(http.StatusOK, Map{"method": c.Request().Method})
15+
})
16+
17+
g.Post("/path", func(c *Context) error {
18+
return c.JSON(http.StatusOK, Map{"method": c.Request().Method})
19+
})
20+
21+
g.Patch("/path", func(c *Context) error {
22+
return c.JSON(http.StatusOK, Map{"method": c.Request().Method})
23+
})
24+
25+
g.Put("/path", func(c *Context) error {
26+
return c.JSON(http.StatusOK, Map{"method": c.Request().Method})
27+
})
28+
29+
g.Delete("/path", func(c *Context) error {
30+
return c.JSON(http.StatusOK, Map{"method": c.Request().Method})
31+
})
32+
33+
g.Connect("/path", func(c *Context) error {
34+
return c.JSON(http.StatusOK, Map{"method": c.Request().Method})
35+
})
36+
37+
g.Trace("/path", func(c *Context) error {
38+
return c.JSON(http.StatusOK, Map{"method": c.Request().Method})
39+
})
40+
41+
g.Options("/path", func(c *Context) error {
42+
return c.JSON(http.StatusOK, Map{"method": c.Request().Method})
43+
})
44+
45+
g.Head("/path", func(c *Context) error {
46+
return c.JSON(http.StatusOK, Map{"method": c.Request().Method})
47+
})
48+
49+
g.Any("/any", func(c *Context) error {
50+
return c.JSON(http.StatusOK, Map{"method": c.Request().Method})
51+
})
52+
}
53+
54+
func TestNewGroup(t *testing.T) {
55+
k := New()
56+
57+
g := newGroup(k, "/v1")
58+
assert.Equal(t, k, g.kid)
59+
assert.Equal(t, "/v1", g.prefix)
60+
assert.Nil(t, g.middlewares)
61+
62+
g = newGroup(k, "/v1", nil)
63+
assert.NotNil(t, g.middlewares)
64+
assert.Len(t, g.middlewares, 1)
65+
}
66+
67+
func TestGroup_combineMiddlewares(t *testing.T) {
68+
k := New()
69+
70+
g := newGroup(k, "/v1")
71+
72+
middlewares := g.combineMiddlewares(nil)
73+
assert.Nil(t, middlewares)
74+
assert.Len(t, middlewares, 0)
75+
76+
middlewares = g.combineMiddlewares([]MiddlewareFunc{nil})
77+
assert.NotNil(t, middlewares)
78+
assert.Len(t, middlewares, 1)
79+
80+
g.middlewares = []MiddlewareFunc{nil, nil}
81+
82+
middlewares = g.combineMiddlewares(nil)
83+
assert.NotNil(t, middlewares)
84+
assert.Len(t, middlewares, 2)
85+
86+
middlewares = g.combineMiddlewares([]MiddlewareFunc{nil})
87+
assert.NotNil(t, middlewares)
88+
assert.Len(t, middlewares, 3)
89+
}
90+
91+
func TestGroup_Add(t *testing.T) {
92+
k := New()
93+
g := newGroup(k, "/v1")
94+
95+
assert.PanicsWithValue(t, "handler cannot be nil", func() {
96+
g.Add("/", nil, []string{http.MethodGet, http.MethodPost})
97+
})
98+
99+
g.Add("/test", func(c *Context) error {
100+
return c.JSON(http.StatusCreated, Map{"message": c.Request().Method})
101+
}, []string{http.MethodGet, http.MethodPost})
102+
103+
assert.Equal(t, 1, len(k.router.routes))
104+
assert.Equal(t, 2, len(k.router.routes[0].methods))
105+
assert.Equal(t, 0, len(k.router.routes[0].middlewares))
106+
assert.Equal(t, []string{http.MethodGet, http.MethodPost}, k.router.routes[0].methods)
107+
108+
testCases := []struct {
109+
req *http.Request
110+
res *httptest.ResponseRecorder
111+
expectedMethod string
112+
}{
113+
{req: httptest.NewRequest(http.MethodPost, "/v1/test", nil), res: httptest.NewRecorder(), expectedMethod: http.MethodPost},
114+
{req: httptest.NewRequest(http.MethodGet, "/v1/test", nil), res: httptest.NewRecorder(), expectedMethod: http.MethodGet},
115+
}
116+
117+
for _, testCase := range testCases {
118+
t.Run(testCase.expectedMethod, func(t *testing.T) {
119+
k.ServeHTTP(testCase.res, testCase.req)
120+
121+
assert.Equal(t, http.StatusCreated, testCase.res.Code)
122+
assert.Equal(t, "application/json", testCase.res.Header().Get("Content-Type"))
123+
assert.Equal(t, fmt.Sprintf("{\"message\":%q}\n", testCase.expectedMethod), testCase.res.Body.String())
124+
})
125+
}
126+
}
127+
128+
func TestGroup_Methods(t *testing.T) {
129+
k := New()
130+
g := newGroup(k, "/v1")
131+
132+
registerHandlers(g)
133+
134+
target := "/v1/path"
135+
any := "/v1/any"
136+
137+
testCases := []struct {
138+
req *http.Request
139+
res *httptest.ResponseRecorder
140+
expectedMethod string
141+
}{
142+
{req: httptest.NewRequest(http.MethodGet, target, nil), res: httptest.NewRecorder(), expectedMethod: http.MethodGet},
143+
{req: httptest.NewRequest(http.MethodPost, target, nil), res: httptest.NewRecorder(), expectedMethod: http.MethodPost},
144+
{req: httptest.NewRequest(http.MethodPut, target, nil), res: httptest.NewRecorder(), expectedMethod: http.MethodPut},
145+
{req: httptest.NewRequest(http.MethodPatch, target, nil), res: httptest.NewRecorder(), expectedMethod: http.MethodPatch},
146+
{req: httptest.NewRequest(http.MethodDelete, target, nil), res: httptest.NewRecorder(), expectedMethod: http.MethodDelete},
147+
{req: httptest.NewRequest(http.MethodOptions, target, nil), res: httptest.NewRecorder(), expectedMethod: http.MethodOptions},
148+
{req: httptest.NewRequest(http.MethodConnect, target, nil), res: httptest.NewRecorder(), expectedMethod: http.MethodConnect},
149+
{req: httptest.NewRequest(http.MethodTrace, target, nil), res: httptest.NewRecorder(), expectedMethod: http.MethodTrace},
150+
{req: httptest.NewRequest(http.MethodHead, target, nil), res: httptest.NewRecorder(), expectedMethod: http.MethodHead},
151+
152+
{req: httptest.NewRequest(http.MethodGet, any, nil), res: httptest.NewRecorder(), expectedMethod: http.MethodGet},
153+
{req: httptest.NewRequest(http.MethodDelete, any, nil), res: httptest.NewRecorder(), expectedMethod: http.MethodDelete},
154+
}
155+
156+
for _, testCase := range testCases {
157+
t.Run(testCase.expectedMethod, func(t *testing.T) {
158+
k.ServeHTTP(testCase.res, testCase.req)
159+
160+
assert.Equal(t, http.StatusOK, testCase.res.Code)
161+
assert.Equal(t, "application/json", testCase.res.Header().Get("Content-Type"))
162+
assert.Equal(t, fmt.Sprintf("{\"method\":%q}\n", testCase.expectedMethod), testCase.res.Body.String())
163+
})
164+
}
165+
}
166+
167+
func TestGroup_Group(t *testing.T) {
168+
k := New()
169+
170+
g := newGroup(k, "/v1")
171+
172+
nestedG := g.Group("/api")
173+
assert.Equal(t, k, nestedG.kid)
174+
assert.Equal(t, "/v1/api", nestedG.prefix)
175+
assert.Nil(t, nestedG.middlewares)
176+
177+
nestedG = g.Group("/api", nil)
178+
assert.NotNil(t, nestedG.middlewares)
179+
assert.Len(t, nestedG.middlewares, 1)
180+
}
181+
182+
func TestGroup_Add_NestedGroups(t *testing.T) {
183+
k := New()
184+
g := newGroup(k, "/v1")
185+
nestedG := g.Group("/api")
186+
187+
g.Add("/test", func(c *Context) error {
188+
return c.JSON(http.StatusCreated, Map{"message": c.Request().Method})
189+
}, []string{http.MethodPost})
190+
191+
nestedG.Add("/{var}", func(c *Context) error {
192+
return c.JSON(http.StatusCreated, Map{"message": c.Param("var")})
193+
}, []string{http.MethodPost})
194+
195+
testCases := []struct {
196+
req *http.Request
197+
res *httptest.ResponseRecorder
198+
name, expectedMessage string
199+
}{
200+
{
201+
name: "group",
202+
req: httptest.NewRequest(http.MethodPost, "/v1/test", nil),
203+
res: httptest.NewRecorder(),
204+
expectedMessage: "{\"message\":\"POST\"}\n",
205+
},
206+
{
207+
name: "nested_group",
208+
req: httptest.NewRequest(http.MethodPost, "/v1/api/test", nil),
209+
res: httptest.NewRecorder(),
210+
expectedMessage: "{\"message\":\"test\"}\n",
211+
},
212+
}
213+
214+
for _, testCase := range testCases {
215+
t.Run(testCase.name, func(t *testing.T) {
216+
k.ServeHTTP(testCase.res, testCase.req)
217+
218+
assert.Equal(t, http.StatusCreated, testCase.res.Code)
219+
assert.Equal(t, "application/json", testCase.res.Header().Get("Content-Type"))
220+
assert.Equal(t, testCase.expectedMessage, testCase.res.Body.String())
221+
})
222+
}
223+
}

kid.go

+7
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,13 @@ func (k *Kid) Any(path string, handler HandlerFunc, middlewares ...MiddlewareFun
163163
k.router.add(path, handler, methods, middlewares)
164164
}
165165

166+
// Group creates a new router group.
167+
//
168+
// Specifying middlewares is optional. Middlewares will be applied to all of the group routes.
169+
func (k *Kid) Group(prefix string, middlewares ...MiddlewareFunc) Group {
170+
return newGroup(k, prefix, middlewares...)
171+
}
172+
166173
// Add registers a new handler for the given path for the given methods.
167174
// Specifying at least one method is required.
168175
//

kid_test.go

+18
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,24 @@ func TestKid_Any(t *testing.T) {
376376
}
377377
}
378378

379+
func TestKid_Group(t *testing.T) {
380+
k := New()
381+
g := k.Group("/v1")
382+
383+
g.Get("/path", func(c *Context) error {
384+
return c.JSON(http.StatusOK, Map{"message": "group"})
385+
})
386+
387+
req := httptest.NewRequest(http.MethodGet, "/v1/path", nil)
388+
res := httptest.NewRecorder()
389+
390+
k.ServeHTTP(res, req)
391+
392+
assert.Equal(t, http.StatusOK, res.Code)
393+
assert.Equal(t, "application/json", res.Header().Get("Content-Type"))
394+
assert.Equal(t, "{\"message\":\"group\"}\n", res.Body.String())
395+
}
396+
379397
func TestKid_applyMiddlewaresToHandler(t *testing.T) {
380398
k := New()
381399

0 commit comments

Comments
 (0)