Skip to content

Commit a215b4d

Browse files
committed
Add public 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. This is the part to make trie implementation available from the rest package.
1 parent 28cbc1b commit a215b4d

File tree

4 files changed

+97
-24
lines changed

4 files changed

+97
-24
lines changed

rest/route.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ type Route struct {
1414

1515
// A string like "/resource/:id.json".
1616
// Placeholders supported are:
17-
// :param that matches any char to the first '/' or '.'
18-
// *splat that matches everything to the end of the string
17+
// :paramName that matches any char to the first '/' or '.'
18+
// #paramName that matches any char to the first '/'
19+
// *paramName that matches everything to the end of the string
1920
// (placeholder names must be unique per PathExp)
2021
PathExp string
2122

@@ -57,8 +58,9 @@ func (route *Route) MakePath(pathParams map[string]string) string {
5758
path := route.PathExp
5859
for paramName, paramValue := range pathParams {
5960
paramPlaceholder := ":" + paramName
61+
relaxedPlaceholder := "#" + paramName
6062
splatPlaceholder := "*" + paramName
61-
r := strings.NewReplacer(paramPlaceholder, paramValue, splatPlaceholder, paramValue)
63+
r := strings.NewReplacer(paramPlaceholder, paramValue, splatPlaceholder, paramValue, relaxedPlaceholder, paramValue)
6264
path = r.Replace(path)
6365
}
6466
return path

rest/route_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,15 @@ func TestReverseRouteResolution(t *testing.T) {
3636
if got != expected {
3737
t.Errorf("expected %s, got %s", expected, got)
3838
}
39+
40+
relaxedParam := &Route{"GET", "/#file", nil}
41+
got = relaxedParam.MakePath(
42+
map[string]string{
43+
"file": "a.txt",
44+
},
45+
)
46+
expected = "/a.txt"
47+
if got != expected {
48+
t.Errorf("expected %s, got %s", expected, got)
49+
}
3950
}

rest/router.go

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@ import (
77
"strings"
88
)
99

10-
// TODO
11-
// support for #param placeholder ?
12-
1310
type router struct {
1411
routes []*Route
1512
disableTrieCompression bool
@@ -24,6 +21,41 @@ func escapedPath(urlObj *url.URL) string {
2421
return parts[0]
2522
}
2623

24+
var preEscape = strings.NewReplacer("*", "__SPLAT_PLACEHOLDER__", "#", "__RELAXED_PLACEHOLDER__")
25+
26+
var postEscape = strings.NewReplacer("__SPLAT_PLACEHOLDER__", "*", "__RELAXED_PLACEHOLDER__", "#")
27+
28+
func escapedPathExp(pathExp string) (string, error) {
29+
30+
// PathExp validation
31+
if pathExp == "" {
32+
return "", errors.New("empty PathExp")
33+
}
34+
if pathExp[0] != '/' {
35+
return "", errors.New("PathExp must start with /")
36+
}
37+
if strings.Contains(pathExp, "?") {
38+
return "", errors.New("PathExp must not contain the query string")
39+
}
40+
41+
// Get the right escaping
42+
// XXX a bit hacky
43+
44+
pathExp = preEscape.Replace(pathExp)
45+
46+
urlObj, err := url.Parse(pathExp)
47+
if err != nil {
48+
return "", err
49+
}
50+
51+
// get the same escaping as find requests
52+
pathExp = urlObj.RequestURI()
53+
54+
pathExp = postEscape.Replace(pathExp)
55+
56+
return pathExp, nil
57+
}
58+
2759
// This validates the Routes and prepares the Trie data structure.
2860
// It must be called once the Routes are defined and before trying to find Routes.
2961
// The order matters, if multiple Routes match, the first defined will be used.
@@ -34,25 +66,12 @@ func (rt *router) start() error {
3466

3567
for i, route := range rt.routes {
3668

37-
// PathExp validation
38-
if route.PathExp == "" {
39-
return errors.New("empty PathExp")
40-
}
41-
if route.PathExp[0] != '/' {
42-
return errors.New("PathExp must start with /")
43-
}
44-
urlObj, err := url.Parse(route.PathExp)
69+
// work with the PathExp urlencoded.
70+
pathExp, err := escapedPathExp(route.PathExp)
4571
if err != nil {
4672
return err
4773
}
4874

49-
// work with the PathExp urlencoded.
50-
pathExp := escapedPath(urlObj)
51-
52-
// make an exception for '*' used by the *splat notation
53-
// (at the trie insert only)
54-
pathExp = strings.Replace(pathExp, "%2A", "*", -1)
55-
5675
// insert in the Trie
5776
err = rt.trie.AddRoute(
5877
strings.ToUpper(route.HttpMethod), // work with the HttpMethod in uppercase

rest/router_test.go

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -293,15 +293,17 @@ func TestRouteOrder(t *testing.T) {
293293

294294
err := r.start()
295295
if err != nil {
296-
t.Fatal()
296+
t.Fatal(err)
297297
}
298298

299299
input := "http://example.org/r/123"
300300
route, params, pathMatched, err := r.findRoute("GET", input)
301301
if err != nil {
302-
t.Fatal()
302+
t.Fatal(err)
303+
}
304+
if route == nil {
305+
t.Fatal("Expected one route to be matched")
303306
}
304-
305307
if route.PathExp != "/r/:id" {
306308
t.Errorf("both match, expected the first defined, got %s", route.PathExp)
307309
}
@@ -313,6 +315,45 @@ func TestRouteOrder(t *testing.T) {
313315
}
314316
}
315317

318+
func TestRelaxedPlaceholder(t *testing.T) {
319+
320+
r := router{
321+
routes: []*Route{
322+
&Route{
323+
HttpMethod: "GET",
324+
PathExp: "/r/:id",
325+
},
326+
&Route{
327+
HttpMethod: "GET",
328+
PathExp: "/r/#filename",
329+
},
330+
},
331+
}
332+
333+
err := r.start()
334+
if err != nil {
335+
t.Fatal()
336+
}
337+
338+
input := "http://example.org/r/a.txt"
339+
route, params, pathMatched, err := r.findRoute("GET", input)
340+
if err != nil {
341+
t.Fatal(err)
342+
}
343+
if route == nil {
344+
t.Fatal("Expected one route to be matched")
345+
}
346+
if route.PathExp != "/r/#filename" {
347+
t.Errorf("expected the second route, got %s", route.PathExp)
348+
}
349+
if params["filename"] != "a.txt" {
350+
t.Error()
351+
}
352+
if pathMatched != true {
353+
t.Error()
354+
}
355+
}
356+
316357
func TestSimpleExample(t *testing.T) {
317358

318359
r := router{

0 commit comments

Comments
 (0)