Skip to content

Commit 3093bab

Browse files
committed
backtrack by calculating next node instead of poping from stack
1 parent 3e817bc commit 3093bab

File tree

2 files changed

+63
-52
lines changed

2 files changed

+63
-52
lines changed

router.go

Lines changed: 35 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -328,52 +328,45 @@ func (n *node) checkMethodNotAllowed() HandlerFunc {
328328
// - Reset it `Context#Reset()`
329329
// - Return it `Echo#ReleaseContext()`.
330330
func (r *Router) Find(method, path string, c Context) {
331-
const backTrackingDepth = 10
332-
333331
ctx := c.(*context)
334332
ctx.path = path
335333
cn := r.tree // Current node as root
336334

337335
var (
338-
search = path
339-
n int // Param counter
340-
pvalues = ctx.pvalues // Use the internal slice so the interface can keep the illusion of a dynamic slice
341-
342-
// Backtracking Information
343-
state [backTrackingDepth]struct {
344-
nk kind
345-
nn *node
346-
ns string
347-
np int
348-
}
349-
stateIndex int = -1
336+
search = path
337+
searchIndex = 0
338+
n int // Param counter
339+
pvalues = ctx.pvalues // Use the internal slice so the interface can keep the illusion of a dynamic slice
350340
)
351341

352-
pushNext := func(nodeKind kind) {
353-
stateIndex++
354-
if stateIndex >= backTrackingDepth {
355-
panic("Max backtracking depth reached. TODO: this must be detected during registering the paths")
356-
}
342+
// backtracking happens when we reach dead end when matching nodes in the router tree. To backtrack we set
343+
// current node to parent to current node (this was previous node we checked) and choose next node kind to check.
344+
// Next node kind relies on routing priority (static->param->any). So for example if there is no static node match we
345+
// should check parent next sibling by kind (param). Backtracking itself does not check if there is next sibling this
346+
// is left up to matching logic
347+
backtrackToNextNodeKind := func(fromKind kind) (nextNodeKind kind, valid bool) {
348+
previous := cn
349+
cn = previous.parent
350+
valid = cn != nil
357351

358-
state[stateIndex].nk = nodeKind
359-
state[stateIndex].nn = cn
360-
state[stateIndex].ns = search
361-
state[stateIndex].np = n
362-
}
352+
// next node type by priority
353+
// NOTE: by current implementation we never backtrack from any route so `previous.kind` value is here always static or any
354+
// if this requirement is to change then for any route next kind would be static kind and this statement should be changed
355+
nextNodeKind = previous.kind + 1
363356

364-
popNext := func() (nodeKind kind, valid bool) {
365-
if stateIndex < 0 {
357+
if fromKind == skind {
358+
// when backtracking is done from static kind block we did not change search so nothing to restore
366359
return
367360
}
368361

369-
last := state[stateIndex]
370-
stateIndex--
371-
372-
nodeKind = last.nk
373-
cn = last.nn
374-
search = last.ns
375-
n = last.np
376-
valid = cn != nil
362+
if previous.kind == skind {
363+
searchIndex -= len(previous.prefix)
364+
search = path[searchIndex:]
365+
} else {
366+
n--
367+
searchIndex -= len(pvalues[n])
368+
search = path[searchIndex:]
369+
}
377370
return
378371
}
379372

@@ -397,21 +390,23 @@ func (r *Router) Find(method, path string, c Context) {
397390

398391
if l != pl {
399392
// No matching prefix, let's backtrack to the first possible alternative node of the decision path
400-
nk, ok := popNext()
393+
nk, ok := backtrackToNextNodeKind(skind)
401394
if !ok {
402395
return // No other possibilities on the decision path
403396
} else if nk == pkind {
404397
goto Param
405-
} else if nk == akind {
406-
goto Any
398+
// 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
399+
//} else if nk == akind {
400+
// goto Any
407401
} else {
408-
// Not found
402+
// Not found (this should never be possible for static node we are looking currently)
409403
return
410404
}
411405
}
412406

413407
// The full prefix has matched, remove the prefix from the remaining search
414408
search = search[l:]
409+
searchIndex = searchIndex + l
415410

416411
// Finish routing if no remaining search and we are on an leaf node
417412
if search == "" && cn.ppath != "" {
@@ -421,12 +416,6 @@ func (r *Router) Find(method, path string, c Context) {
421416
// Static node
422417
if search != "" {
423418
if child := cn.findStaticChild(search[0]); child != nil {
424-
if cn.paramChildren != nil || cn.anyChildren != nil {
425-
// Push a new entry into the decision path, if we don't find anything downtree
426-
// try the current node again searching for a param or any node
427-
// Optimization: The node is only pushed for backtracking if there's an praramChildren or an anyChildren
428-
pushNext(pkind)
429-
}
430419
cn = child
431420
continue
432421
}
@@ -435,20 +424,14 @@ func (r *Router) Find(method, path string, c Context) {
435424
Param:
436425
// Param node
437426
if child := cn.paramChildren; search != "" && child != nil {
438-
if cn.anyChildren != nil {
439-
// Push a new entry into the decision path, if we have nothing found downtree try the current node again
440-
// searching for an any node.
441-
// Optimization: The node is only pushed for backtracking if there's an anyChildren
442-
pushNext(akind)
443-
}
444-
445427
cn = child
446428
i, l := 0, len(search)
447429
for ; i < l && search[i] != '/'; i++ {
448430
}
449431
pvalues[n] = search[:i]
450432
n++
451433
search = search[i:]
434+
searchIndex = searchIndex + i
452435
continue
453436
}
454437

@@ -462,7 +445,7 @@ func (r *Router) Find(method, path string, c Context) {
462445
}
463446

464447
// Let's backtrack to the first possible alternative node of the decision path
465-
nk, ok := popNext()
448+
nk, ok := backtrackToNextNodeKind(akind)
466449
if !ok {
467450
return // No other possibilities on the decision path
468451
} else if nk == pkind {

router_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,34 @@ func TestRouterMatchAny(t *testing.T) {
754754
assert.Equal(t, "joe", c.Param("*"))
755755
}
756756

757+
// NOTE: this is to document current implementation. Last added route with `*` asterisk is always the match and no
758+
// backtracking or more precise matching is done to find more suitable match.
759+
//
760+
// Current behaviour might not be correct or expected.
761+
// But this is where we are without well defined requirements/rules how (multiple) asterisks work in route
762+
func TestRouterAnyMatchesLastAddedAnyRoute(t *testing.T) {
763+
e := New()
764+
r := e.router
765+
766+
r.Add(http.MethodGet, "/users/*", handlerHelper("case", 1))
767+
r.Add(http.MethodGet, "/users/*/action*", handlerHelper("case", 2))
768+
769+
c := e.NewContext(nil, nil).(*context)
770+
771+
r.Find(http.MethodGet, "/users/xxx/action/sea", c)
772+
c.handler(c)
773+
assert.Equal(t, "/users/*/action*", c.Get("path"))
774+
assert.Equal(t, "xxx/action/sea", c.Param("*"))
775+
776+
// if we add another route then it is the last added and so it is matched
777+
r.Add(http.MethodGet, "/users/*/action/search", handlerHelper("case", 3))
778+
779+
r.Find(http.MethodGet, "/users/xxx/action/sea", c)
780+
c.handler(c)
781+
assert.Equal(t, "/users/*/action/search", c.Get("path"))
782+
assert.Equal(t, "xxx/action/sea", c.Param("*"))
783+
}
784+
757785
// Issue #1739
758786
func TestRouterMatchAnyPrefixIssue(t *testing.T) {
759787
e := New()

0 commit comments

Comments
 (0)