diff --git a/README.md b/README.md index 012152a0..f8bc4239 100644 --- a/README.md +++ b/README.md @@ -2096,3 +2096,4 @@ Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framewor * [krakend](https://github.com/devopsfaith/krakend): Ultra performant API Gateway with middlewares. * [picfit](https://github.com/thoas/picfit): An image resizing server written in Go. * [brigade](https://github.com/brigadecore/brigade): Event-based Scripting for Kubernetes. +* [dkron](https://github.com/distribworks/dkron): Distributed, fault tolerant job scheduling system. diff --git a/tree.go b/tree.go index 1187f3f6..b09d3f67 100644 --- a/tree.go +++ b/tree.go @@ -107,16 +107,15 @@ type node struct { // increments priority of the given child and reorders if necessary. func (n *node) incrementChildPrio(pos int) int { - n.children[pos].priority++ - prio := n.children[pos].priority + cs := n.children + cs[pos].priority++ + prio := cs[pos].priority - // adjust position (move to front) + // Adjust position (move to front) newPos := pos - for newPos > 0 && n.children[newPos-1].priority < prio { - // swap node positions - n.children[newPos-1], n.children[newPos] = n.children[newPos], n.children[newPos-1] - - newPos-- + for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- { + // Swap node positions + cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1] } // build new index char string @@ -231,7 +230,7 @@ walk: } // Check if a child with the next path byte exists - for i := 0; i < len(n.indices); i++ { + for i, max := 0, len(n.indices); i < max; i++ { if c == n.indices[i] { parentFullPathIndex += len(n.path) i = n.incrementChildPrio(i) @@ -254,75 +253,91 @@ walk: } n.insertChild(numParams, path, fullPath, handlers) return - - } else if i == len(path) { // Make node a (in-path) leaf - if n.handlers != nil { - panic("handlers are already registered for path '" + fullPath + "'") - } - n.handlers = handlers } + + // Otherwise and handle to current node + if n.handlers != nil { + panic("handlers are already registered for path '" + fullPath + "'") + } + n.handlers = handlers return } } -func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers HandlersChain) { - var offset int // already handled bytes of the path - - // find prefix until first wildcard (beginning with ':' or '*') - for i, max := 0, len(path); numParams > 0; i++ { - c := path[i] +// Search for a wildcard segment and check the name for invalid characters. +// Returns -1 as index, if no wildcard war found. +func findWildcard(path string) (wildcard string, i int, valid bool) { + // Find start + for start, c := range []byte(path) { + // A wildcard starts with ':' (param) or '*' (catch-all) if c != ':' && c != '*' { continue } - // find wildcard end (either '/' or path end) - end := i + 1 - for end < max && path[end] != '/' { - switch path[end] { - // the wildcard name must not contain ':' and '*' + // Find end and check for invalid characters + valid = true + for end, c := range []byte(path[start+1:]) { + switch c { + case '/': + return path[start : start+1+end], start, valid case ':', '*': - panic("only one wildcard per path segment is allowed, has: '" + - path[i:] + "' in path '" + fullPath + "'") - default: - end++ + valid = false } } + return path[start:], start, valid + } + return "", -1, false +} - // check if this Node existing children which would be - // unreachable if we insert the wildcard here - if len(n.children) > 0 { - panic("wildcard route '" + path[i:end] + - "' conflicts with existing children in path '" + fullPath + "'") +func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers HandlersChain) { + for numParams > 0 { + // Find prefix until first wildcard + wildcard, i, valid := findWildcard(path) + if i < 0 { // No wildcard found + break + } + + // The wildcard name must not contain ':' and '*' + if !valid { + panic("only one wildcard per path segment is allowed, has: '" + + wildcard + "' in path '" + fullPath + "'") } // check if the wildcard has a name - if end-i < 2 { + if len(wildcard) < 2 { panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") } - if c == ':' { // param - // split path at the beginning of the wildcard + // Check if this node has existing children which would be + // unreachable if we insert the wildcard here + if len(n.children) > 0 { + panic("wildcard segment '" + wildcard + + "' conflicts with existing children in path '" + fullPath + "'") + } + + if wildcard[0] == ':' { // param if i > 0 { - n.path = path[offset:i] - offset = i + // Insert prefix before the current wildcard + n.path = path[:i] + path = path[i:] } + n.wildChild = true child := &node{ nType: param, + path: wildcard, maxParams: numParams, fullPath: fullPath, } n.children = []*node{child} - n.wildChild = true n = child n.priority++ numParams-- // if the path doesn't end with the wildcard, then there // will be another non-wildcard subpath starting with '/' - if end < max { - n.path = path[offset:end] - offset = end + if len(wildcard) < len(path) { + path = path[len(wildcard):] child := &node{ maxParams: numParams, @@ -331,54 +346,63 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle } n.children = []*node{child} n = child + continue } - } else { // catchAll - if end != max || numParams > 1 { - panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") - } - - if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { - panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'") - } - - // currently fixed width 1 for '/' - i-- - if path[i] != '/' { - panic("no / before catch-all in path '" + fullPath + "'") - } - - n.path = path[offset:i] - - // first node: catchAll node with empty path - child := &node{ - wildChild: true, - nType: catchAll, - maxParams: 1, - fullPath: fullPath, - } - n.children = []*node{child} - n.indices = string(path[i]) - n = child - n.priority++ - - // second node: node holding the variable - child = &node{ - path: path[i:], - nType: catchAll, - maxParams: 1, - handlers: handlers, - priority: 1, - fullPath: fullPath, - } - n.children = []*node{child} - + // Otherwise we're done. Insert the handle in the new leaf + n.handlers = handlers return } + + // catchAll + if i+len(wildcard) != len(path) || numParams > 1 { + panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") + } + + if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { + panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'") + } + + // currently fixed width 1 for '/' + i-- + if path[i] != '/' { + panic("no / before catch-all in path '" + fullPath + "'") + } + + n.path = path[:i] + + // First node: catchAll node with empty path + child := &node{ + wildChild: true, + nType: catchAll, + maxParams: 1, + fullPath: fullPath, + } + // update maxParams of the parent node + if n.maxParams < 1 { + n.maxParams = 1 + } + n.children = []*node{child} + n.indices = string('/') + n = child + n.priority++ + + // second node: node holding the variable + child = &node{ + path: path[i:], + nType: catchAll, + maxParams: 1, + handlers: handlers, + priority: 1, + fullPath: fullPath, + } + n.children = []*node{child} + + return } - // insert remaining path part and handle to the leaf - n.path = path[offset:] + // If no wildcard was found, simple insert the path and handle + n.path = path n.handlers = handlers n.fullPath = fullPath } @@ -400,17 +424,20 @@ func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue) value.params = po walk: // Outer loop for walking the tree for { - if len(path) > len(n.path) { - if path[:len(n.path)] == n.path { - path = path[len(n.path):] + prefix := n.path + if len(path) > len(prefix) { + if path[:len(prefix)] == prefix { + path = path[len(prefix):] // If this node does not have a wildcard (param or catchAll) // child, we can just look up the next child node and continue // to walk down the tree if !n.wildChild { c := path[0] - for i := 0; i < len(n.indices); i++ { - if c == n.indices[i] { + indices := n.indices + for i, max := 0, len(indices); i < max; i++ { + if c == indices[i] { n = n.children[i] + prefix = n.path continue walk } } @@ -454,6 +481,7 @@ walk: // Outer loop for walking the tree if len(n.children) > 0 { path = path[end:] n = n.children[0] + prefix = n.path continue walk } @@ -500,7 +528,7 @@ walk: // Outer loop for walking the tree panic("invalid node type") } } - } else if path == n.path { + } else if path == prefix { // We should have reached the node containing the handle. // Check if this node has a handle registered. if value.handlers = n.handlers; value.handlers != nil { @@ -515,8 +543,9 @@ walk: // Outer loop for walking the tree // No handle found. Check if a handle for this path + a // trailing slash exists for trailing slash recommendation - for i := 0; i < len(n.indices); i++ { - if n.indices[i] == '/' { + indices := n.indices + for i, max := 0, len(indices); i < max; i++ { + if indices[i] == '/' { n = n.children[i] value.tsr = (len(n.path) == 1 && n.handlers != nil) || (n.nType == catchAll && n.children[0].handlers != nil) @@ -530,8 +559,8 @@ walk: // Outer loop for walking the tree // Nothing found. We can recommend to redirect to the same URL with an // extra trailing slash if a leaf exists for that path value.tsr = (path == "/") || - (len(n.path) == len(path)+1 && n.path[len(path)] == '/' && - path == n.path[:len(n.path)-1] && n.handlers != nil) + (len(prefix) == len(path)+1 && prefix[len(path)] == '/' && + path == prefix[:len(prefix)-1] && n.handlers != nil) return } } @@ -597,25 +626,25 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa n = n.children[0] switch n.nType { case param: - // find param end (either '/' or path end) - k := 0 - for k < len(path) && path[k] != '/' { - k++ + // Find param end (either '/' or path end) + end := 0 + for end < len(path) && path[end] != '/' { + end++ } // add param value to case insensitive path - ciPath = append(ciPath, path[:k]...) + ciPath = append(ciPath, path[:end]...) // we need to go deeper! - if k < len(path) { + if end < len(path) { if len(n.children) > 0 { - path = path[k:] + path = path[end:] n = n.children[0] continue } // ... but we can't - if fixTrailingSlash && len(path) == k+1 { + if fixTrailingSlash && len(path) == end+1 { return ciPath, true } return @@ -623,7 +652,8 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa if n.handlers != nil { return ciPath, true - } else if fixTrailingSlash && len(n.children) == 1 { + } + if fixTrailingSlash && len(n.children) == 1 { // No handle found. Check if a handle for this path + a // trailing slash exists n = n.children[0] diff --git a/tree_test.go b/tree_test.go index e6e28865..0fe2fe00 100644 --- a/tree_test.go +++ b/tree_test.go @@ -368,6 +368,13 @@ func TestTreeCatchAllConflictRoot(t *testing.T) { testRoutes(t, routes) } +func TestTreeCatchMaxParams(t *testing.T) { + tree := &node{} + var route = "/cmd/*filepath" + tree.addRoute(route, fakeHandler(route)) + checkMaxParams(t, tree) +} + func TestTreeDoubleWildcard(t *testing.T) { const panicMsg = "only one wildcard per path segment is allowed"