Skip to content

Commit 28cbc1b

Browse files
committed
Add support for relaxed route placeholder
Notation is #param (used by other frameworks, and inspired by Mojilicious) It catches all chars until the first '/' or the end of the string.
1 parent 1c5b504 commit 28cbc1b

File tree

2 files changed

+107
-9
lines changed

2 files changed

+107
-9
lines changed

rest/trie/impl.go

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,28 @@ func splitParam(remaining string) (string, string) {
2222
return remaining[:i], remaining[i:]
2323
}
2424

25+
func splitRelaxed(remaining string) (string, string) {
26+
i := 0
27+
for len(remaining) > i && remaining[i] != '/' {
28+
i++
29+
}
30+
return remaining[:i], remaining[i:]
31+
}
32+
2533
type node struct {
2634
HttpMethodToRoute map[string]interface{}
27-
Children map[string]*node
28-
ChildrenKeyLen int
29-
ParamChild *node
30-
ParamName string
31-
SplatChild *node
32-
SplatName string
35+
36+
Children map[string]*node
37+
ChildrenKeyLen int
38+
39+
ParamChild *node
40+
ParamName string
41+
42+
RelaxedChild *node
43+
RelaxedName string
44+
45+
SplatChild *node
46+
SplatName string
3347
}
3448

3549
func (n *node) addRoute(httpMethod, pathExp string, route interface{}, usedParams []string) error {
@@ -84,6 +98,36 @@ func (n *node) addRoute(httpMethod, pathExp string, route interface{}, usedParam
8498
}
8599
}
86100
nextNode = n.ParamChild
101+
} else if token[0] == '#' {
102+
// #param case
103+
var name string
104+
name, remaining = splitRelaxed(remaining)
105+
106+
// Check param name is unique
107+
for _, e := range usedParams {
108+
if e == name {
109+
return errors.New(
110+
fmt.Sprintf("A route can't have two placeholders with the same name: %s", name),
111+
)
112+
}
113+
}
114+
usedParams = append(usedParams, name)
115+
116+
if n.RelaxedChild == nil {
117+
n.RelaxedChild = &node{}
118+
n.RelaxedName = name
119+
} else {
120+
if n.RelaxedName != name {
121+
return errors.New(
122+
fmt.Sprintf(
123+
"Routes sharing a common placeholder MUST name it consistently: %s != %s",
124+
n.RelaxedName,
125+
name,
126+
),
127+
)
128+
}
129+
}
130+
nextNode = n.RelaxedChild
87131
} else if token[0] == '*' {
88132
// *splat case
89133
name := remaining
@@ -194,6 +238,14 @@ func (n *node) find(httpMethod, path string, context *findContext) {
194238
context.popParams()
195239
}
196240

241+
// #param branch
242+
if n.RelaxedChild != nil {
243+
value, remaining := splitRelaxed(path)
244+
context.pushParams(n.RelaxedName, value)
245+
n.RelaxedChild.find(httpMethod, remaining, context)
246+
context.popParams()
247+
}
248+
197249
// main branch
198250
length := n.ChildrenKeyLen
199251
if len(path) < length {
@@ -215,14 +267,18 @@ func (n *node) compress() {
215267
if n.ParamChild != nil {
216268
n.ParamChild.compress()
217269
}
270+
// #param branch
271+
if n.RelaxedChild != nil {
272+
n.RelaxedChild.compress()
273+
}
218274
// main branch
219275
if len(n.Children) == 0 {
220276
return
221277
}
222278
// compressable ?
223279
canCompress := true
224280
for _, node := range n.Children {
225-
if node.HttpMethodToRoute != nil || node.SplatChild != nil || node.ParamChild != nil {
281+
if node.HttpMethodToRoute != nil || node.SplatChild != nil || node.ParamChild != nil || node.RelaxedChild != nil {
226282
canCompress = false
227283
}
228284
}

rest/trie/impl_test.go

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ func TestTrieCompression(t *testing.T) {
5050
if trie.root.Children["/adc"] == nil {
5151
t.Errorf("%+v", trie.root)
5252
}
53-
5453
}
54+
5555
func TestParamInsert(t *testing.T) {
5656
trie := New()
5757

@@ -78,6 +78,18 @@ func TestParamInsert(t *testing.T) {
7878
}
7979
}
8080

81+
func TestRelaxedInsert(t *testing.T) {
82+
trie := New()
83+
84+
trie.AddRoute("GET", "/#id/", "")
85+
if trie.root.Children["/"].RelaxedChild.Children["/"] == nil {
86+
t.Error()
87+
}
88+
if trie.root.Children["/"].RelaxedName != "id" {
89+
t.Error()
90+
}
91+
}
92+
8193
func TestSplatInsert(t *testing.T) {
8294
trie := New()
8395
trie.AddRoute("GET", "/*splat", "")
@@ -115,6 +127,7 @@ func TestFindRoute(t *testing.T) {
115127
trie.AddRoute("GET", "/r/:id", "resource")
116128
trie.AddRoute("GET", "/r/:id/property", "property")
117129
trie.AddRoute("GET", "/r/:id/property.*format", "property_format")
130+
trie.AddRoute("GET", "/user/#username/property", "user_property")
118131

119132
trie.Compress()
120133

@@ -166,6 +179,17 @@ func TestFindRoute(t *testing.T) {
166179
if matches[0].Params["format"] != "json" {
167180
t.Error()
168181
}
182+
183+
matches = trie.FindRoutes("GET", "/user/antoine.imbert/property")
184+
if len(matches) != 1 {
185+
t.Errorf("expected one route, got %d", len(matches))
186+
}
187+
if !isInMatches("user_property", matches) {
188+
t.Error("expected 'user_property'")
189+
}
190+
if matches[0].Params["username"] != "antoine.imbert" {
191+
t.Error()
192+
}
169193
}
170194

171195
func TestFindRouteMultipleMatches(t *testing.T) {
@@ -177,6 +201,7 @@ func TestFindRouteMultipleMatches(t *testing.T) {
177201
trie.AddRoute("GET", "/r/:id", "resource_generic")
178202
trie.AddRoute("GET", "/s/*rest", "special_all")
179203
trie.AddRoute("GET", "/s/:param", "special_generic")
204+
trie.AddRoute("GET", "/s/#param", "special_relaxed")
180205
trie.AddRoute("GET", "/", "root")
181206

182207
trie.Compress()
@@ -193,7 +218,7 @@ func TestFindRouteMultipleMatches(t *testing.T) {
193218
}
194219

195220
matches = trie.FindRoutes("GET", "/s/1")
196-
if len(matches) != 2 {
221+
if len(matches) != 3 {
197222
t.Errorf("expected two matches, got %d", len(matches))
198223
}
199224
if !isInMatches("special_all", matches) {
@@ -202,16 +227,28 @@ func TestFindRouteMultipleMatches(t *testing.T) {
202227
if !isInMatches("special_generic", matches) {
203228
t.Error()
204229
}
230+
if !isInMatches("special_relaxed", matches) {
231+
t.Error()
232+
}
205233
}
206234

207235
func TestConsistentPlaceholderName(t *testing.T) {
208236

209237
trie := New()
238+
210239
trie.AddRoute("GET", "/r/:id", "oneph")
211240
err := trie.AddRoute("GET", "/r/:rid/other", "twoph")
212241
if err == nil {
213242
t.Error("Should have died on adding second route")
214243
}
244+
245+
trie.AddRoute("GET", "/r/#id", "oneph")
246+
err = trie.AddRoute("GET", "/r/#rid/other", "twoph")
247+
if err == nil {
248+
t.Error("Should have died on adding second route")
249+
}
250+
251+
// TODO *param
215252
}
216253

217254
func TestDuplicateName(t *testing.T) {
@@ -227,4 +264,9 @@ func TestDuplicateName(t *testing.T) {
227264
if err == nil {
228265
t.Error("Should have died, this route has two placeholder named `id`")
229266
}
267+
268+
err = trie.AddRoute("GET", "/r/:id/o/#id", "two")
269+
if err == nil {
270+
t.Error("Should have died, this route has two placeholder named `id`")
271+
}
230272
}

0 commit comments

Comments
 (0)