diff --git a/_examples/routing/dynamic-path/main.go b/_examples/routing/dynamic-path/main.go index 6f6fedae..63be863f 100644 --- a/_examples/routing/dynamic-path/main.go +++ b/_examples/routing/dynamic-path/main.go @@ -369,5 +369,12 @@ func main() { // should differ e.g. // /path/{name:string} // /path/{id:uint} + // + // Note: + // If * path part is declared at the end of the route path, then + // it's considered a wildcard (same as {p:path}). In order to declare + // literal * and over pass this limitation use the string's path parameter 'eq' function + // as shown below: + // app.Get("/*/*/{p:string eq(*)}", handler) <- This will match only: /*/*/* and not /*/*/anything. app.Listen(":8080") } diff --git a/core/router/path.go b/core/router/path.go index d4f560fc..1936057a 100644 --- a/core/router/path.go +++ b/core/router/path.go @@ -98,19 +98,22 @@ func joinPath(path1 string, path2 string) string { // 6. Remove trailing '/'. // // The returned path ends in a slash only if it is the root "/". -func cleanPath(s string) string { +// The function does not modify the dynamic path parts. +func cleanPath(path string) string { // note that we don't care about the performance here, it's before the server ran. - if s == "" || s == "." { + if path == "" || path == "." { return "/" } // remove suffix "/", if it's root "/" then it will add it as a prefix below. - if lidx := len(s) - 1; s[lidx] == '/' { - s = s[:lidx] + if lidx := len(path) - 1; path[lidx] == '/' { + path = path[:lidx] } // prefix with "/". - s = prefix(s, "/") + path = prefix(path, "/") + + s := []rune(path) // If you're learning go through Iris I will ask you to ignore the // following part, it's not the recommending way to do that, @@ -138,46 +141,34 @@ func cleanPath(s string) string { // when inside {} then don't try to clean it. if !insideMacro { - if s[i] == '/' { - if len(s)-1 >= i+1 && s[i+1] == '/' { // we have "//". - bckp := s - s = bckp[:i] + "/" - // forward two, we ignore the second "/" in the raw. - i = i + 2 - if len(bckp)-1 >= i { - s += bckp[i:] - } + if s[i] == '\\' { + s[i] = '/' + + if len(s)-1 > i+1 && s[i+1] == '\\' { + s = deleteCharacter(s, i+1) + } else { + i-- // set to minus in order for the next check to be applied for prev tokens too. } - // if we have just a single slash then continue. - continue } - if s[i] == '\\' { // this will catch "\\" and "\". - bckp := s - s = bckp[:i] + "/" - - if len(bckp)-1 >= i+1 { - s += bckp[i+1:] - i++ - } - - if len(s)-1 > i && s[i] == '\\' { - bckp := s - s = bckp[:i] - if len(bckp)-1 >= i+2 { - s = bckp[:i-1] + bckp[i+1:] - i++ - } - } - + if s[i] == '/' && len(s)-1 > i+1 && s[i+1] == '/' { + s[i] = '/' + s = deleteCharacter(s, i+1) + i-- continue } - } - } - return s + if len(s) > 1 && s[len(s)-1] == '/' { // remove any last //. + s = s[:len(s)-1] + } + + return string(s) +} + +func deleteCharacter(s []rune, index int) []rune { + return append(s[0:index], s[index+1:]...) } const ( diff --git a/core/router/path_test.go b/core/router/path_test.go index 5e5c6abc..aafb9a7c 100644 --- a/core/router/path_test.go +++ b/core/router/path_test.go @@ -47,6 +47,18 @@ func TestCleanPath(t *testing.T) { "/single/{id:uint64}", "/single/{id:uint64}", }, + { + "0\\\\\\0", + "/0/0", + }, + { + "*\\*\\*", + "/*/*/*", + }, + { + "\\", + "/", + }, } for i, tt := range tests { diff --git a/core/router/route.go b/core/router/route.go index e55a4419..457a28cd 100644 --- a/core/router/route.go +++ b/core/router/route.go @@ -90,12 +90,14 @@ type Route struct { // handlers are being changed to validate the macros at serve time, if needed. func NewRoute(p Party, statusErrorCode int, method, subdomain, unparsedPath string, handlers context.Handlers, macros macro.Macros) (*Route, error) { - tmpl, err := macro.Parse(unparsedPath, macros) + path := cleanPath(unparsedPath) // required. Before macro template parse as the cleanPath does not modify the dynamic path route parts. + + tmpl, err := macro.Parse(path, macros) if err != nil { return nil, err } - path := convertMacroTmplToNodePath(tmpl) + path = convertMacroTmplToNodePath(tmpl) // prepend the macro handler to the route, now, // right before the register to the tree, so APIBuilder#UseGlobal will work as expected. if handler.CanMakeHandler(tmpl) { @@ -103,7 +105,6 @@ func NewRoute(p Party, statusErrorCode int, method, subdomain, unparsedPath stri handlers = append(context.Handlers{macroEvaluatorHandler}, handlers...) } - path = cleanPath(path) // maybe unnecessary here. defaultName := method + subdomain + tmpl.Src if statusErrorCode > 0 { defaultName = fmt.Sprintf("%d_%s", statusErrorCode, defaultName) @@ -506,7 +507,7 @@ func (r *Route) Trace(w io.Writer, stoppedIndex int) { // s := fmt.Sprintf("%s: %s", pio.Rich(title, color), path) pio.WriteRich(w, title, color) - path := r.Tmpl().Src + path := r.tmpl.Src if path == "" { path = "/" } diff --git a/core/router/trie.go b/core/router/trie.go index a22dc769..af17daec 100644 --- a/core/router/trie.go +++ b/core/router/trie.go @@ -9,9 +9,13 @@ import ( const ( // ParamStart the character in string representation where the underline router starts its dynamic named parameter. ParamStart = ":" + // paramStartCharacter is the character as rune of ParamStart. + paramStartCharacter = ':' // WildcardParamStart the character in string representation where the underline router starts its dynamic wildcard // path parameter. WildcardParamStart = "*" + // wildcardParamStartCharacter is the character as rune of WildcardParamStart. + wildcardParamStartCharacter = '*' ) // An iris-specific identical version of the https://github.com/kataras/muxie version 1.0.0 released at 15 Oct 2018 @@ -112,6 +116,9 @@ func slowPathSplit(path string) []string { func (tr *trie) insert(path string, route context.RouteReadOnly, handlers context.Handlers) { input := slowPathSplit(path) + if len(input) == 0 { + return + } n := tr.root if path == pathSep { @@ -121,9 +128,13 @@ func (tr *trie) insert(path string, route context.RouteReadOnly, handlers contex var paramKeys []string for _, s := range input { + if len(s) == 0 { + continue + } + c := s[0] - if isParam, isWildcard := c == ParamStart[0], c == WildcardParamStart[0]; isParam || isWildcard { + if isParam, isWildcard := c == paramStartCharacter, c == wildcardParamStartCharacter; isParam || isWildcard { n.hasDynamicChild = true paramKeys = append(paramKeys, s[1:]) // without : or *. @@ -166,7 +177,6 @@ func (tr *trie) insert(path string, route context.RouteReadOnly, handlers contex } n.staticKey = path[:i] - // fmt.Printf("trie.insert: (whole path=%v) Path: %s, Route name: %s, Handlers len: %d\n", n.end, n.key, route.Name(), len(handlers)) } @@ -192,15 +202,26 @@ func (tr *trie) search(q string, params *context.RequestParams) *trieNode { for { if i == end || q[i] == pathSepB { - if child := n.getChild(q[start:i]); child != nil { + segment := q[start:i] + if child := n.getChild(segment); child != nil { n = child + // Possible reserved param character, should catch it as + // dynamic node instead of static-path based. + if segment == ParamStart { // len(n.paramKeys) > 0 && (segment == ParamStart || segment == WildcardParamStart) + if ln := len(paramValues); cap(paramValues) > ln { + paramValues = paramValues[:ln+1] + paramValues[ln] = segment + } else { + paramValues = append(paramValues, segment) + } + } } else if n.childNamedParameter { n = n.getChild(ParamStart) if ln := len(paramValues); cap(paramValues) > ln { paramValues = paramValues[:ln+1] - paramValues[ln] = q[start:i] + paramValues[ln] = segment } else { - paramValues = append(paramValues, q[start:i]) + paramValues = append(paramValues, segment) } } else if n.childWildcardParameter { n = n.getChild(WildcardParamStart) @@ -213,7 +234,7 @@ func (tr *trie) search(q string, params *context.RequestParams) *trieNode { break } else { n = n.findClosestParentWildcardNode() - if n != nil { + if n != nil && len(n.paramKeys) > 0 { // means that it has :param/static and *wildcard, we go trhough the :param // but the next path segment is not the /static, so go back to *wildcard // instead of not found. @@ -248,7 +269,7 @@ func (tr *trie) search(q string, params *context.RequestParams) *trieNode { if n == nil || !n.end { if n != nil { // we need it on both places, on last segment (below) or on the first unnknown (above). - if n = n.findClosestParentWildcardNode(); n != nil { + if n = n.findClosestParentWildcardNode(); n != nil && len(n.paramKeys) > 0 { params.Set(n.paramKeys[0], q[len(n.staticKey):]) return n } @@ -263,6 +284,10 @@ func (tr *trie) search(q string, params *context.RequestParams) *trieNode { // the /other2/*myparam and not the root wildcard, which is what we want. // n = tr.root.getChild(WildcardParamStart) + if len(n.paramKeys) == 0 { // fix crashes on /*/*/*. + return nil + } + params.Set(n.paramKeys[0], q[1:]) return n } diff --git a/macro/handler/handler.go b/macro/handler/handler.go index 948a8c0b..6fd1d771 100644 --- a/macro/handler/handler.go +++ b/macro/handler/handler.go @@ -107,6 +107,7 @@ func MakeFilter(tmpl macro.Template) context.Filter { entry, found := ctx.Params().Store.GetEntryAt(p.Index) if !found { // should never happen. + ctx.StatusCode(p.ErrCode) // status code can change from an error handler, set it here. return false } diff --git a/macro/interpreter/parser/parser.go b/macro/interpreter/parser/parser.go index a489298e..c50d9696 100644 --- a/macro/interpreter/parser/parser.go +++ b/macro/interpreter/parser/parser.go @@ -39,7 +39,7 @@ func Parse(fullpath string, paramTypes []ast.ParamType) ([]*ast.ParamStatement, } // if we have param type path but it's not the last path part if ast.IsTrailing(stmt.Type) && i < len(pathParts)-1 { - return nil, fmt.Errorf("%s: parameter type \"%s\" should be registered to the very end of a path", s, stmt.Type.Indent()) + return nil, fmt.Errorf("%s: parameter type \"%s\" should be registered to the very end of a path once", s, stmt.Type.Indent()) } statements = append(statements, stmt) diff --git a/macro/macros.go b/macro/macros.go index 48264654..7b7c19d0 100644 --- a/macro/macros.go +++ b/macro/macros.go @@ -50,6 +50,31 @@ var ( return func(paramValue string) bool { return max >= len(paramValue) } + }). + // checks if param value's matches the given input + RegisterFunc("eq", func(s string) func(string) bool { + return func(paramValue string) bool { + return paramValue == s + } + }). + // checks if param value's matches at least one of the inputs + RegisterFunc("eqor", func(texts []string) func(string) bool { + if len(texts) == 1 { + text := texts[0] + return func(paramValue string) bool { + return paramValue == text + } + } + + return func(paramValue string) bool { + for _, s := range texts { + if paramValue == s { + return true + } + } + + return false + } }) // Int or number type