Skip to content

Commit cce6be6

Browse files
authored
feat: add star routes (#28)
1 parent 460e5fb commit cce6be6

File tree

2 files changed

+91
-15
lines changed

2 files changed

+91
-15
lines changed

mux.go

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ var (
1212
errMethodNotAllowed = errors.New("method is not allowed")
1313
)
1414

15+
// Path parameters prefix and suffix.
16+
const (
17+
paramPrefix = "{"
18+
paramSuffix = "}"
19+
plusParamPrefix = paramPrefix + "+"
20+
starParamPrefix = paramPrefix + "*"
21+
)
22+
1523
type (
1624
// Router is the struct which holds all of the routes.
1725
Router struct {
@@ -30,6 +38,7 @@ type (
3038
Segment struct {
3139
isParam bool
3240
isPlus bool
41+
isStar bool
3342
tpl string
3443
}
3544

@@ -59,17 +68,26 @@ func (router *Router) add(path string, handler HandlerFunc, methods []string, mi
5968
routeSegments := make([]Segment, 0, len(segments))
6069

6170
for i, segment := range segments {
62-
if strings.HasPrefix(segment, "{+") && strings.HasSuffix(segment, "}") {
71+
if (strings.HasPrefix(segment, plusParamPrefix) || strings.HasPrefix(segment, starParamPrefix)) && strings.HasSuffix(segment, paramSuffix) {
72+
isPlus := router.isPlus(segment)
73+
isStar := !isPlus
74+
6375
if i == len(segments)-1 {
64-
routeSegments = append(routeSegments, Segment{isParam: true, isPlus: true, tpl: segment[2 : len(segment)-1]})
76+
routeSegments = append(
77+
routeSegments,
78+
Segment{isParam: true, isPlus: isPlus, isStar: isStar, tpl: segment[2 : len(segment)-1]},
79+
)
6580
} else if i == len(segments)-2 {
6681
if segments[i+1] != "" {
67-
panic("plus path parameters can only be the last part of a path")
82+
panic("plus/star path parameters can only be the last part of a path")
6883
}
69-
routeSegments = append(routeSegments, Segment{isParam: true, isPlus: true, tpl: segment[2 : len(segment)-1]})
84+
routeSegments = append(
85+
routeSegments,
86+
Segment{isParam: true, isPlus: isPlus, isStar: isStar, tpl: segment[2 : len(segment)-1]},
87+
)
7088
break
7189
} else {
72-
panic("plus path parameters can only be the last part of a path")
90+
panic("plus/star path parameters can only be the last part of a path")
7391
}
7492
} else if strings.HasPrefix(segment, "{") && strings.HasSuffix(segment, "}") {
7593
routeSegments = append(routeSegments, Segment{isParam: true, tpl: segment[1 : len(segment)-1]})
@@ -84,6 +102,7 @@ func (router *Router) add(path string, handler HandlerFunc, methods []string, mi
84102
// match determines if the given path and method matches the route.
85103
func (route *Route) match(path, method string) (Params, error) {
86104
params := make(Params)
105+
totalSegments := len(route.segments)
87106
var end bool
88107

89108
for segmentIndex, segment := range route.segments {
@@ -96,14 +115,21 @@ func (route *Route) match(path, method string) (Params, error) {
96115
end = true
97116

98117
// No slashes are left but there are still more segments.
99-
if segmentIndex != len(route.segments)-1 {
100-
return nil, errNotFound
118+
if segmentIndex != totalSegments-1 {
119+
// It means /api/v1 will be matched to /api/v1/{*param}
120+
lastSegment := route.segments[totalSegments-1]
121+
if segmentIndex == totalSegments-2 && lastSegment.isStar {
122+
end = true
123+
params[lastSegment.tpl] = ""
124+
} else {
125+
return nil, errNotFound
126+
}
101127
}
102128
}
103129

104130
if segment.isParam {
105-
if segment.isPlus {
106-
if len(path) == 0 {
131+
if segment.isPlus || segment.isStar {
132+
if len(path) == 0 && segment.isPlus {
107133
return nil, errNotFound
108134
}
109135

@@ -169,6 +195,15 @@ func (router *Router) find(path string, method string) (Route, Params, error) {
169195

170196
}
171197

198+
// isPlus returns true if path parameter is plus path parameter.
199+
func (router *Router) isPlus(segment string) bool {
200+
var isPlus bool
201+
if strings.HasPrefix(segment, plusParamPrefix) {
202+
isPlus = true
203+
}
204+
return isPlus
205+
}
206+
172207
// cleanPath normalizes the path.
173208
//
174209
// If soft is false it also removes duplicate slashes.

mux_test.go

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,11 @@ func TestAdd(t *testing.T) {
6262
router.add("/", nil, []string{http.MethodGet}, nil)
6363
})
6464

65-
assert.PanicsWithValue(t, "plus path parameters can only be the last part of a path", func() {
65+
assert.PanicsWithValue(t, "plus/star path parameters can only be the last part of a path", func() {
6666
router.add("/path/{+extraPath}/asd", testHandlerFunc, []string{http.MethodGet}, nil)
6767
})
6868

69-
assert.PanicsWithValue(t, "plus path parameters can only be the last part of a path", func() {
69+
assert.PanicsWithValue(t, "plus/star path parameters can only be the last part of a path", func() {
7070
router.add("/path/{+extraPath}/test/test2", testHandlerFunc, []string{http.MethodGet}, nil)
7171
})
7272

@@ -147,11 +147,13 @@ func TestMatch(t *testing.T) {
147147

148148
router.add("/", testHandlerFunc, []string{http.MethodGet}, nil)
149149
router.add("/test/{var}/get", testHandlerFunc, []string{http.MethodGet, http.MethodPost}, nil)
150-
router.add("/test/{var}/get/{+extraPath}", testHandlerFunc, []string{http.MethodPut}, nil)
150+
router.add("/test/{var}/get/{+plusPath}", testHandlerFunc, []string{http.MethodPut}, nil)
151+
router.add("/test/{var}/path/{*starPath}", testHandlerFunc, []string{http.MethodGet}, nil)
151152

152153
firstRoute := router.routes[0]
153154
secondRoute := router.routes[1]
154155
plusRoute := router.routes[2]
156+
starRoute := router.routes[3]
155157

156158
// Don't need to add starting slash in route's match method as they are skipped in router's find method.
157159
// For matching we should match relative paths, not abosulute paths.
@@ -218,18 +220,47 @@ func TestMatch(t *testing.T) {
218220
// Testing plus route.
219221
params, err = plusRoute.match("test/123/get/extra/path", http.MethodPut)
220222
assert.NoError(t, err)
221-
assert.Equal(t, Params{"var": "123", "extraPath": "extra/path"}, params)
223+
assert.Equal(t, Params{"var": "123", "plusPath": "extra/path"}, params)
224+
225+
params, err = plusRoute.match("test/123/get/extra", http.MethodPut)
226+
assert.NoError(t, err)
227+
assert.Equal(t, Params{"var": "123", "plusPath": "extra"}, params)
222228

223229
params, err = plusRoute.match("test/123/get/extra/path", http.MethodGet)
224230
assert.ErrorIs(t, err, errMethodNotAllowed)
225231
assert.Nil(t, params)
226232

227-
params, err = plusRoute.match("test//get/extra/path", http.MethodGet)
233+
params, err = plusRoute.match("test//get/extra/path", http.MethodPut)
228234
assert.ErrorIs(t, err, errNotFound)
229235
assert.Nil(t, params)
230236

231237
// At least one extra path is required
232-
params, err = plusRoute.match("test/123/get/", http.MethodGet)
238+
params, err = plusRoute.match("test/123/get/", http.MethodPut)
239+
assert.ErrorIs(t, err, errNotFound)
240+
assert.Nil(t, params)
241+
242+
// Testing star route.
243+
params, err = starRoute.match("test/123/path/star/path", http.MethodGet)
244+
assert.NoError(t, err)
245+
assert.Equal(t, Params{"var": "123", "starPath": "star/path"}, params)
246+
247+
params, err = starRoute.match("test/123/path/star", http.MethodGet)
248+
assert.NoError(t, err)
249+
assert.Equal(t, Params{"var": "123", "starPath": "star"}, params)
250+
251+
params, err = starRoute.match("test/123/path/", http.MethodGet)
252+
assert.NoError(t, err)
253+
assert.Equal(t, Params{"var": "123", "starPath": ""}, params)
254+
255+
params, err = starRoute.match("test/123/path", http.MethodGet)
256+
assert.NoError(t, err)
257+
assert.Equal(t, Params{"var": "123", "starPath": ""}, params)
258+
259+
params, err = starRoute.match("test/123/path/star/path", http.MethodPost)
260+
assert.ErrorIs(t, err, errMethodNotAllowed)
261+
assert.Nil(t, params)
262+
263+
params, err = starRoute.match("test//path/star/path", http.MethodGet)
233264
assert.ErrorIs(t, err, errNotFound)
234265
assert.Nil(t, params)
235266
}
@@ -317,3 +348,13 @@ func TestFind(t *testing.T) {
317348
assert.True(t, funcsAreEqual(testHandlerFunc, route.handler))
318349
assert.True(t, funcsAreEqual(testMiddlewareFunc, route.middlewares[0]))
319350
}
351+
352+
func TestRouterIsPlus(t *testing.T) {
353+
router := newRouter()
354+
355+
isPlus := router.isPlus("{+param}")
356+
assert.True(t, isPlus)
357+
358+
isPlus = router.isPlus("{*param}")
359+
assert.False(t, isPlus)
360+
}

0 commit comments

Comments
 (0)