1
0
mirror of https://github.com/labstack/echo.git synced 2025-04-27 12:32:09 +02:00

Refactor router for readability (#1796)

* refactor router tests to table driven (this way it is easier to debug test cases with breakpoints)
* refactor router variables to be more readable
This commit is contained in:
Martti T 2021-03-06 01:43:59 +02:00 committed by GitHub
parent 6f9b71cd6f
commit 664cf8c106
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 1315 additions and 833 deletions

275
router.go
View File

@ -17,12 +17,12 @@ type (
label byte label byte
prefix string prefix string
parent *node parent *node
staticChildrens children staticChildren children
ppath string ppath string
pnames []string pnames []string
methodHandler *methodHandler methodHandler *methodHandler
paramChildren *node paramChild *node
anyChildren *node anyChild *node
} }
kind uint8 kind uint8
children []*node children []*node
@ -42,9 +42,9 @@ type (
) )
const ( const (
skind kind = iota staticKind kind = iota
pkind paramKind
akind anyKind
paramLabel = byte(':') paramLabel = byte(':')
anyLabel = byte('*') anyLabel = byte('*')
@ -73,137 +73,147 @@ func (r *Router) Add(method, path string, h HandlerFunc) {
pnames := []string{} // Param names pnames := []string{} // Param names
ppath := path // Pristine path ppath := path // Pristine path
for i, l := 0, len(path); i < l; i++ { for i, lcpIndex := 0, len(path); i < lcpIndex; i++ {
if path[i] == ':' { if path[i] == ':' {
j := i + 1 j := i + 1
r.insert(method, path[:i], nil, skind, "", nil) r.insert(method, path[:i], nil, staticKind, "", nil)
for ; i < l && path[i] != '/'; i++ { for ; i < lcpIndex && path[i] != '/'; i++ {
} }
pnames = append(pnames, path[j:i]) pnames = append(pnames, path[j:i])
path = path[:j] + path[i:] path = path[:j] + path[i:]
i, l = j, len(path) i, lcpIndex = j, len(path)
if i == l { if i == lcpIndex {
r.insert(method, path[:i], h, pkind, ppath, pnames) r.insert(method, path[:i], h, paramKind, ppath, pnames)
} else { } else {
r.insert(method, path[:i], nil, pkind, "", nil) r.insert(method, path[:i], nil, paramKind, "", nil)
} }
} else if path[i] == '*' { } else if path[i] == '*' {
r.insert(method, path[:i], nil, skind, "", nil) r.insert(method, path[:i], nil, staticKind, "", nil)
pnames = append(pnames, "*") pnames = append(pnames, "*")
r.insert(method, path[:i+1], h, akind, ppath, pnames) r.insert(method, path[:i+1], h, anyKind, ppath, pnames)
} }
} }
r.insert(method, path, h, skind, ppath, pnames) r.insert(method, path, h, staticKind, ppath, pnames)
} }
func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string, pnames []string) { func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string, pnames []string) {
// Adjust max param // Adjust max param
l := len(pnames) paramLen := len(pnames)
if *r.echo.maxParam < l { if *r.echo.maxParam < paramLen {
*r.echo.maxParam = l *r.echo.maxParam = paramLen
} }
cn := r.tree // Current node as root currentNode := r.tree // Current node as root
if cn == nil { if currentNode == nil {
panic("echo: invalid method") panic("echo: invalid method")
} }
search := path search := path
for { for {
sl := len(search) searchLen := len(search)
pl := len(cn.prefix) prefixLen := len(currentNode.prefix)
l := 0 lcpLen := 0
// LCP // LCP - Longest Common Prefix (https://en.wikipedia.org/wiki/LCP_array)
max := pl max := prefixLen
if sl < max { if searchLen < max {
max = sl max = searchLen
} }
for ; l < max && search[l] == cn.prefix[l]; l++ { for ; lcpLen < max && search[lcpLen] == currentNode.prefix[lcpLen]; lcpLen++ {
} }
if l == 0 { if lcpLen == 0 {
// At root node // At root node
cn.label = search[0] currentNode.label = search[0]
cn.prefix = search currentNode.prefix = search
if h != nil { if h != nil {
cn.kind = t currentNode.kind = t
cn.addHandler(method, h) currentNode.addHandler(method, h)
cn.ppath = ppath currentNode.ppath = ppath
cn.pnames = pnames currentNode.pnames = pnames
} }
} else if l < pl { } else if lcpLen < prefixLen {
// Split node // Split node
n := newNode(cn.kind, cn.prefix[l:], cn, cn.staticChildrens, cn.methodHandler, cn.ppath, cn.pnames, cn.paramChildren, cn.anyChildren) n := newNode(
currentNode.kind,
currentNode.prefix[lcpLen:],
currentNode,
currentNode.staticChildren,
currentNode.methodHandler,
currentNode.ppath,
currentNode.pnames,
currentNode.paramChild,
currentNode.anyChild,
)
// Update parent path for all children to new node // Update parent path for all children to new node
for _, child := range cn.staticChildrens { for _, child := range currentNode.staticChildren {
child.parent = n child.parent = n
} }
if cn.paramChildren != nil { if currentNode.paramChild != nil {
cn.paramChildren.parent = n currentNode.paramChild.parent = n
} }
if cn.anyChildren != nil { if currentNode.anyChild != nil {
cn.anyChildren.parent = n currentNode.anyChild.parent = n
} }
// Reset parent node // Reset parent node
cn.kind = skind currentNode.kind = staticKind
cn.label = cn.prefix[0] currentNode.label = currentNode.prefix[0]
cn.prefix = cn.prefix[:l] currentNode.prefix = currentNode.prefix[:lcpLen]
cn.staticChildrens = nil currentNode.staticChildren = nil
cn.methodHandler = new(methodHandler) currentNode.methodHandler = new(methodHandler)
cn.ppath = "" currentNode.ppath = ""
cn.pnames = nil currentNode.pnames = nil
cn.paramChildren = nil currentNode.paramChild = nil
cn.anyChildren = nil currentNode.anyChild = nil
// Only Static children could reach here // Only Static children could reach here
cn.addStaticChild(n) currentNode.addStaticChild(n)
if l == sl { if lcpLen == searchLen {
// At parent node // At parent node
cn.kind = t currentNode.kind = t
cn.addHandler(method, h) currentNode.addHandler(method, h)
cn.ppath = ppath currentNode.ppath = ppath
cn.pnames = pnames currentNode.pnames = pnames
} else { } else {
// Create child node // Create child node
n = newNode(t, search[l:], cn, nil, new(methodHandler), ppath, pnames, nil, nil) n = newNode(t, search[lcpLen:], currentNode, nil, new(methodHandler), ppath, pnames, nil, nil)
n.addHandler(method, h) n.addHandler(method, h)
// Only Static children could reach here // Only Static children could reach here
cn.addStaticChild(n) currentNode.addStaticChild(n)
} }
} else if l < sl { } else if lcpLen < searchLen {
search = search[l:] search = search[lcpLen:]
c := cn.findChildWithLabel(search[0]) c := currentNode.findChildWithLabel(search[0])
if c != nil { if c != nil {
// Go deeper // Go deeper
cn = c currentNode = c
continue continue
} }
// Create child node // Create child node
n := newNode(t, search, cn, nil, new(methodHandler), ppath, pnames, nil, nil) n := newNode(t, search, currentNode, nil, new(methodHandler), ppath, pnames, nil, nil)
n.addHandler(method, h) n.addHandler(method, h)
switch t { switch t {
case skind: case staticKind:
cn.addStaticChild(n) currentNode.addStaticChild(n)
case pkind: case paramKind:
cn.paramChildren = n currentNode.paramChild = n
case akind: case anyKind:
cn.anyChildren = n currentNode.anyChild = n
} }
} else { } else {
// Node already exists // Node already exists
if h != nil { if h != nil {
cn.addHandler(method, h) currentNode.addHandler(method, h)
cn.ppath = ppath currentNode.ppath = ppath
if len(cn.pnames) == 0 { // Issue #729 if len(currentNode.pnames) == 0 { // Issue #729
cn.pnames = pnames currentNode.pnames = pnames
} }
} }
} }
@ -217,21 +227,21 @@ func newNode(t kind, pre string, p *node, sc children, mh *methodHandler, ppath
label: pre[0], label: pre[0],
prefix: pre, prefix: pre,
parent: p, parent: p,
staticChildrens: sc, staticChildren: sc,
ppath: ppath, ppath: ppath,
pnames: pnames, pnames: pnames,
methodHandler: mh, methodHandler: mh,
paramChildren: paramChildren, paramChild: paramChildren,
anyChildren: anyChildren, anyChild: anyChildren,
} }
} }
func (n *node) addStaticChild(c *node) { func (n *node) addStaticChild(c *node) {
n.staticChildrens = append(n.staticChildrens, c) n.staticChildren = append(n.staticChildren, c)
} }
func (n *node) findStaticChild(l byte) *node { func (n *node) findStaticChild(l byte) *node {
for _, c := range n.staticChildrens { for _, c := range n.staticChildren {
if c.label == l { if c.label == l {
return c return c
} }
@ -240,16 +250,16 @@ func (n *node) findStaticChild(l byte) *node {
} }
func (n *node) findChildWithLabel(l byte) *node { func (n *node) findChildWithLabel(l byte) *node {
for _, c := range n.staticChildrens { for _, c := range n.staticChildren {
if c.label == l { if c.label == l {
return c return c
} }
} }
if l == paramLabel { if l == paramLabel {
return n.paramChildren return n.paramChild
} }
if l == anyLabel { if l == anyLabel {
return n.anyChildren return n.anyChild
} }
return nil return nil
} }
@ -330,13 +340,15 @@ func (n *node) checkMethodNotAllowed() HandlerFunc {
func (r *Router) Find(method, path string, c Context) { func (r *Router) Find(method, path string, c Context) {
ctx := c.(*context) ctx := c.(*context)
ctx.path = path ctx.path = path
cn := r.tree // Current node as root currentNode := r.tree // Current node as root
var ( var (
// search stores the remaining path to check for match. By each iteration we move from start of path to end of the path
// and search value gets shorter and shorter.
search = path search = path
searchIndex = 0 searchIndex = 0
n int // Param counter paramIndex int // Param counter
pvalues = ctx.pvalues // Use the internal slice so the interface can keep the illusion of a dynamic slice paramValues = ctx.pvalues // Use the internal slice so the interface can keep the illusion of a dynamic slice
) )
// Backtracking is needed when a dead end (leaf node) is reached in the router tree. // Backtracking is needed when a dead end (leaf node) is reached in the router tree.
@ -345,9 +357,9 @@ func (r *Router) Find(method, path string, c Context) {
// For example if there is no static node match we should check parent next sibling by kind (param). // For example if there is no static node match we should check parent next sibling by kind (param).
// Backtracking itself does not check if there is a next sibling, this is done by the router logic. // Backtracking itself does not check if there is a next sibling, this is done by the router logic.
backtrackToNextNodeKind := func(fromKind kind) (nextNodeKind kind, valid bool) { backtrackToNextNodeKind := func(fromKind kind) (nextNodeKind kind, valid bool) {
previous := cn previous := currentNode
cn = previous.parent currentNode = previous.parent
valid = cn != nil valid = currentNode != nil
// Next node type by priority // Next node type by priority
// NOTE: With the current implementation we never backtrack from an `any` route, so `previous.kind` is // NOTE: With the current implementation we never backtrack from an `any` route, so `previous.kind` is
@ -355,51 +367,57 @@ func (r *Router) Find(method, path string, c Context) {
// If this is changed then for any route next kind would be `static` and this statement should be changed // If this is changed then for any route next kind would be `static` and this statement should be changed
nextNodeKind = previous.kind + 1 nextNodeKind = previous.kind + 1
if fromKind == skind { if fromKind == staticKind {
// when backtracking is done from static kind block we did not change search so nothing to restore // when backtracking is done from static kind block we did not change search so nothing to restore
return return
} }
// restore search to value it was before we move to current node we are backtracking from. // restore search to value it was before we move to current node we are backtracking from.
if previous.kind == skind { if previous.kind == staticKind {
searchIndex -= len(previous.prefix) searchIndex -= len(previous.prefix)
} else { } else {
n-- paramIndex--
// for param/any node.prefix value is always `:` so we can not deduce searchIndex from that and must use pValue // for param/any node.prefix value is always `:` so we can not deduce searchIndex from that and must use pValue
// for that index as it would also contain part of path we cut off before moving into node we are backtracking from // for that index as it would also contain part of path we cut off before moving into node we are backtracking from
searchIndex -= len(pvalues[n]) searchIndex -= len(paramValues[paramIndex])
} }
search = path[searchIndex:] search = path[searchIndex:]
return return
} }
// Search order static > param > any // Router tree is implemented by longest common prefix array (LCP array) https://en.wikipedia.org/wiki/LCP_array
// Tree search is implemented as for loop where one loop iteration is divided into 3 separate blocks
// Each of these blocks checks specific kind of node (static/param/any). Order of blocks reflex their priority in routing.
// Search order/priority is: static > param > any.
//
// Note: backtracking in tree is implemented by replacing/switching currentNode to previous node
// and hoping to (goto statement) next block by priority to check if it is the match.
for { for {
pl := 0 // Prefix length prefixLen := 0 // Prefix length
l := 0 // LCP length lcpLen := 0 // LCP (longest common prefix) length
if cn.label != ':' { if currentNode.kind == staticKind {
sl := len(search) searchLen := len(search)
pl = len(cn.prefix) prefixLen = len(currentNode.prefix)
// LCP // LCP - Longest Common Prefix (https://en.wikipedia.org/wiki/LCP_array)
max := pl max := prefixLen
if sl < max { if searchLen < max {
max = sl max = searchLen
} }
for ; l < max && search[l] == cn.prefix[l]; l++ { for ; lcpLen < max && search[lcpLen] == currentNode.prefix[lcpLen]; lcpLen++ {
} }
} }
if l != pl { if lcpLen != prefixLen {
// No matching prefix, let's backtrack to the first possible alternative node of the decision path // No matching prefix, let's backtrack to the first possible alternative node of the decision path
nk, ok := backtrackToNextNodeKind(skind) nk, ok := backtrackToNextNodeKind(staticKind)
if !ok { if !ok {
return // No other possibilities on the decision path return // No other possibilities on the decision path
} else if nk == pkind { } else if nk == paramKind {
goto Param goto Param
// NOTE: this case (backtracking from static node to previous any node) can not happen by current any matching logic. Any node is end of search currently // NOTE: this case (backtracking from static node to previous any node) can not happen by current any matching logic. Any node is end of search currently
//} else if nk == akind { //} else if nk == anyKind {
// goto Any // goto Any
} else { } else {
// Not found (this should never be possible for static node we are looking currently) // Not found (this should never be possible for static node we are looking currently)
@ -408,31 +426,32 @@ func (r *Router) Find(method, path string, c Context) {
} }
// The full prefix has matched, remove the prefix from the remaining search // The full prefix has matched, remove the prefix from the remaining search
search = search[l:] search = search[lcpLen:]
searchIndex = searchIndex + l searchIndex = searchIndex + lcpLen
// Finish routing if no remaining search and we are on an leaf node // Finish routing if no remaining search and we are on an leaf node
if search == "" && cn.ppath != "" { if search == "" && currentNode.ppath != "" {
break break
} }
// Static node // Static node
if search != "" { if search != "" {
if child := cn.findStaticChild(search[0]); child != nil { if child := currentNode.findStaticChild(search[0]); child != nil {
cn = child currentNode = child
continue continue
} }
} }
Param: Param:
// Param node // Param node
if child := cn.paramChildren; search != "" && child != nil { if child := currentNode.paramChild; search != "" && child != nil {
cn = child currentNode = child
// FIXME: when param node does not have any children then param node should act similarly to any node - consider all remaining search as match
i, l := 0, len(search) i, l := 0, len(search)
for ; i < l && search[i] != '/'; i++ { for ; i < l && search[i] != '/'; i++ {
} }
pvalues[n] = search[:i] paramValues[paramIndex] = search[:i]
n++ paramIndex++
search = search[i:] search = search[i:]
searchIndex = searchIndex + i searchIndex = searchIndex + i
continue continue
@ -440,20 +459,20 @@ func (r *Router) Find(method, path string, c Context) {
Any: Any:
// Any node // Any node
if child := cn.anyChildren; child != nil { if child := currentNode.anyChild; child != nil {
// If any node is found, use remaining path for pvalues // If any node is found, use remaining path for paramValues
cn = child currentNode = child
pvalues[len(cn.pnames)-1] = search paramValues[len(currentNode.pnames)-1] = search
break break
} }
// Let's backtrack to the first possible alternative node of the decision path // Let's backtrack to the first possible alternative node of the decision path
nk, ok := backtrackToNextNodeKind(akind) nk, ok := backtrackToNextNodeKind(anyKind)
if !ok { if !ok {
return // No other possibilities on the decision path return // No other possibilities on the decision path
} else if nk == pkind { } else if nk == paramKind {
goto Param goto Param
} else if nk == akind { } else if nk == anyKind {
goto Any goto Any
} else { } else {
// Not found // Not found
@ -461,12 +480,12 @@ func (r *Router) Find(method, path string, c Context) {
} }
} }
ctx.handler = cn.findHandler(method) ctx.handler = currentNode.findHandler(method)
ctx.path = cn.ppath ctx.path = currentNode.ppath
ctx.pnames = cn.pnames ctx.pnames = currentNode.pnames
if ctx.handler == nil { if ctx.handler == nil {
ctx.handler = cn.checkMethodNotAllowed() ctx.handler = currentNode.checkMethodNotAllowed()
} }
return return
} }

File diff suppressed because it is too large Load Diff