Skip to content

Commit

Permalink
Add public support for relaxed route placeholder
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ant0ine committed Jul 13, 2014
1 parent 28cbc1b commit a215b4d
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 24 deletions.
8 changes: 5 additions & 3 deletions rest/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ type Route struct {

// A string like "/resource/:id.json".
// Placeholders supported are:
// :param that matches any char to the first '/' or '.'
// *splat that matches everything to the end of the string
// :paramName that matches any char to the first '/' or '.'
// #paramName that matches any char to the first '/'
// *paramName that matches everything to the end of the string
// (placeholder names must be unique per PathExp)
PathExp string

Expand Down Expand Up @@ -57,8 +58,9 @@ func (route *Route) MakePath(pathParams map[string]string) string {
path := route.PathExp
for paramName, paramValue := range pathParams {
paramPlaceholder := ":" + paramName
relaxedPlaceholder := "#" + paramName
splatPlaceholder := "*" + paramName
r := strings.NewReplacer(paramPlaceholder, paramValue, splatPlaceholder, paramValue)
r := strings.NewReplacer(paramPlaceholder, paramValue, splatPlaceholder, paramValue, relaxedPlaceholder, paramValue)
path = r.Replace(path)
}
return path
Expand Down
11 changes: 11 additions & 0 deletions rest/route_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,15 @@ func TestReverseRouteResolution(t *testing.T) {
if got != expected {
t.Errorf("expected %s, got %s", expected, got)
}

relaxedParam := &Route{"GET", "/#file", nil}
got = relaxedParam.MakePath(
map[string]string{
"file": "a.txt",
},
)
expected = "/a.txt"
if got != expected {
t.Errorf("expected %s, got %s", expected, got)
}
}
55 changes: 37 additions & 18 deletions rest/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ import (
"strings"
)

// TODO
// support for #param placeholder ?

type router struct {
routes []*Route
disableTrieCompression bool
Expand All @@ -24,6 +21,41 @@ func escapedPath(urlObj *url.URL) string {
return parts[0]
}

var preEscape = strings.NewReplacer("*", "__SPLAT_PLACEHOLDER__", "#", "__RELAXED_PLACEHOLDER__")

var postEscape = strings.NewReplacer("__SPLAT_PLACEHOLDER__", "*", "__RELAXED_PLACEHOLDER__", "#")

func escapedPathExp(pathExp string) (string, error) {

// PathExp validation
if pathExp == "" {
return "", errors.New("empty PathExp")
}
if pathExp[0] != '/' {
return "", errors.New("PathExp must start with /")
}
if strings.Contains(pathExp, "?") {
return "", errors.New("PathExp must not contain the query string")
}

// Get the right escaping
// XXX a bit hacky

pathExp = preEscape.Replace(pathExp)

urlObj, err := url.Parse(pathExp)
if err != nil {
return "", err
}

// get the same escaping as find requests
pathExp = urlObj.RequestURI()

pathExp = postEscape.Replace(pathExp)

return pathExp, nil
}

// This validates the Routes and prepares the Trie data structure.
// It must be called once the Routes are defined and before trying to find Routes.
// The order matters, if multiple Routes match, the first defined will be used.
Expand All @@ -34,25 +66,12 @@ func (rt *router) start() error {

for i, route := range rt.routes {

// PathExp validation
if route.PathExp == "" {
return errors.New("empty PathExp")
}
if route.PathExp[0] != '/' {
return errors.New("PathExp must start with /")
}
urlObj, err := url.Parse(route.PathExp)
// work with the PathExp urlencoded.
pathExp, err := escapedPathExp(route.PathExp)
if err != nil {
return err
}

// work with the PathExp urlencoded.
pathExp := escapedPath(urlObj)

// make an exception for '*' used by the *splat notation
// (at the trie insert only)
pathExp = strings.Replace(pathExp, "%2A", "*", -1)

// insert in the Trie
err = rt.trie.AddRoute(
strings.ToUpper(route.HttpMethod), // work with the HttpMethod in uppercase
Expand Down
47 changes: 44 additions & 3 deletions rest/router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,15 +293,17 @@ func TestRouteOrder(t *testing.T) {

err := r.start()
if err != nil {
t.Fatal()
t.Fatal(err)
}

input := "http://example.org/r/123"
route, params, pathMatched, err := r.findRoute("GET", input)
if err != nil {
t.Fatal()
t.Fatal(err)
}
if route == nil {
t.Fatal("Expected one route to be matched")
}

if route.PathExp != "/r/:id" {
t.Errorf("both match, expected the first defined, got %s", route.PathExp)
}
Expand All @@ -313,6 +315,45 @@ func TestRouteOrder(t *testing.T) {
}
}

func TestRelaxedPlaceholder(t *testing.T) {

r := router{
routes: []*Route{
&Route{
HttpMethod: "GET",
PathExp: "/r/:id",
},
&Route{
HttpMethod: "GET",
PathExp: "/r/#filename",
},
},
}

err := r.start()
if err != nil {
t.Fatal()
}

input := "http://example.org/r/a.txt"
route, params, pathMatched, err := r.findRoute("GET", input)
if err != nil {
t.Fatal(err)
}
if route == nil {
t.Fatal("Expected one route to be matched")
}
if route.PathExp != "/r/#filename" {
t.Errorf("expected the second route, got %s", route.PathExp)
}
if params["filename"] != "a.txt" {
t.Error()
}
if pathMatched != true {
t.Error()
}
}

func TestSimpleExample(t *testing.T) {

r := router{
Expand Down

0 comments on commit a215b4d

Please sign in to comment.