From a215b4d876a4806875a53df358a0f804cab98622 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 13 Jul 2014 04:35:04 +0000 Subject: [PATCH] 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. --- rest/route.go | 8 ++++--- rest/route_test.go | 11 +++++++++ rest/router.go | 55 ++++++++++++++++++++++++++++++--------------- rest/router_test.go | 47 +++++++++++++++++++++++++++++++++++--- 4 files changed, 97 insertions(+), 24 deletions(-) diff --git a/rest/route.go b/rest/route.go index 37c8423..d91fd5f 100644 --- a/rest/route.go +++ b/rest/route.go @@ -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 @@ -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 diff --git a/rest/route_test.go b/rest/route_test.go index d0005b9..434c4f0 100644 --- a/rest/route_test.go +++ b/rest/route_test.go @@ -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) + } } diff --git a/rest/router.go b/rest/router.go index 763484b..b110861 100644 --- a/rest/router.go +++ b/rest/router.go @@ -7,9 +7,6 @@ import ( "strings" ) -// TODO -// support for #param placeholder ? - type router struct { routes []*Route disableTrieCompression bool @@ -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. @@ -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 diff --git a/rest/router_test.go b/rest/router_test.go index 348ca23..7831d9c 100644 --- a/rest/router_test.go +++ b/rest/router_test.go @@ -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) } @@ -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{