forked from ebhomengo/niki
749 lines
21 KiB
Go
749 lines
21 KiB
Go
|
package echo
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"fmt"
|
||
|
"net/http"
|
||
|
)
|
||
|
|
||
|
type (
|
||
|
// Router is the registry of all registered routes for an `Echo` instance for
|
||
|
// request matching and URL path parameter parsing.
|
||
|
Router struct {
|
||
|
tree *node
|
||
|
routes map[string]*Route
|
||
|
echo *Echo
|
||
|
}
|
||
|
node struct {
|
||
|
kind kind
|
||
|
label byte
|
||
|
prefix string
|
||
|
parent *node
|
||
|
staticChildren children
|
||
|
originalPath string
|
||
|
methods *routeMethods
|
||
|
paramChild *node
|
||
|
anyChild *node
|
||
|
paramsCount int
|
||
|
// isLeaf indicates that node does not have child routes
|
||
|
isLeaf bool
|
||
|
// isHandler indicates that node has at least one handler registered to it
|
||
|
isHandler bool
|
||
|
|
||
|
// notFoundHandler is handler registered with RouteNotFound method and is executed for 404 cases
|
||
|
notFoundHandler *routeMethod
|
||
|
}
|
||
|
kind uint8
|
||
|
children []*node
|
||
|
routeMethod struct {
|
||
|
ppath string
|
||
|
pnames []string
|
||
|
handler HandlerFunc
|
||
|
}
|
||
|
routeMethods struct {
|
||
|
connect *routeMethod
|
||
|
delete *routeMethod
|
||
|
get *routeMethod
|
||
|
head *routeMethod
|
||
|
options *routeMethod
|
||
|
patch *routeMethod
|
||
|
post *routeMethod
|
||
|
propfind *routeMethod
|
||
|
put *routeMethod
|
||
|
trace *routeMethod
|
||
|
report *routeMethod
|
||
|
anyOther map[string]*routeMethod
|
||
|
allowHeader string
|
||
|
}
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
staticKind kind = iota
|
||
|
paramKind
|
||
|
anyKind
|
||
|
|
||
|
paramLabel = byte(':')
|
||
|
anyLabel = byte('*')
|
||
|
)
|
||
|
|
||
|
func (m *routeMethods) isHandler() bool {
|
||
|
return m.connect != nil ||
|
||
|
m.delete != nil ||
|
||
|
m.get != nil ||
|
||
|
m.head != nil ||
|
||
|
m.options != nil ||
|
||
|
m.patch != nil ||
|
||
|
m.post != nil ||
|
||
|
m.propfind != nil ||
|
||
|
m.put != nil ||
|
||
|
m.trace != nil ||
|
||
|
m.report != nil ||
|
||
|
len(m.anyOther) != 0
|
||
|
// RouteNotFound/404 is not considered as a handler
|
||
|
}
|
||
|
|
||
|
func (m *routeMethods) updateAllowHeader() {
|
||
|
buf := new(bytes.Buffer)
|
||
|
buf.WriteString(http.MethodOptions)
|
||
|
|
||
|
if m.connect != nil {
|
||
|
buf.WriteString(", ")
|
||
|
buf.WriteString(http.MethodConnect)
|
||
|
}
|
||
|
if m.delete != nil {
|
||
|
buf.WriteString(", ")
|
||
|
buf.WriteString(http.MethodDelete)
|
||
|
}
|
||
|
if m.get != nil {
|
||
|
buf.WriteString(", ")
|
||
|
buf.WriteString(http.MethodGet)
|
||
|
}
|
||
|
if m.head != nil {
|
||
|
buf.WriteString(", ")
|
||
|
buf.WriteString(http.MethodHead)
|
||
|
}
|
||
|
if m.patch != nil {
|
||
|
buf.WriteString(", ")
|
||
|
buf.WriteString(http.MethodPatch)
|
||
|
}
|
||
|
if m.post != nil {
|
||
|
buf.WriteString(", ")
|
||
|
buf.WriteString(http.MethodPost)
|
||
|
}
|
||
|
if m.propfind != nil {
|
||
|
buf.WriteString(", PROPFIND")
|
||
|
}
|
||
|
if m.put != nil {
|
||
|
buf.WriteString(", ")
|
||
|
buf.WriteString(http.MethodPut)
|
||
|
}
|
||
|
if m.trace != nil {
|
||
|
buf.WriteString(", ")
|
||
|
buf.WriteString(http.MethodTrace)
|
||
|
}
|
||
|
if m.report != nil {
|
||
|
buf.WriteString(", REPORT")
|
||
|
}
|
||
|
for method := range m.anyOther { // for simplicity, we use map and therefore order is not deterministic here
|
||
|
buf.WriteString(", ")
|
||
|
buf.WriteString(method)
|
||
|
}
|
||
|
m.allowHeader = buf.String()
|
||
|
}
|
||
|
|
||
|
// NewRouter returns a new Router instance.
|
||
|
func NewRouter(e *Echo) *Router {
|
||
|
return &Router{
|
||
|
tree: &node{
|
||
|
methods: new(routeMethods),
|
||
|
},
|
||
|
routes: map[string]*Route{},
|
||
|
echo: e,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Routes returns the registered routes.
|
||
|
func (r *Router) Routes() []*Route {
|
||
|
routes := make([]*Route, 0, len(r.routes))
|
||
|
for _, v := range r.routes {
|
||
|
routes = append(routes, v)
|
||
|
}
|
||
|
return routes
|
||
|
}
|
||
|
|
||
|
// Reverse generates a URL from route name and provided parameters.
|
||
|
func (r *Router) Reverse(name string, params ...interface{}) string {
|
||
|
uri := new(bytes.Buffer)
|
||
|
ln := len(params)
|
||
|
n := 0
|
||
|
for _, route := range r.routes {
|
||
|
if route.Name == name {
|
||
|
for i, l := 0, len(route.Path); i < l; i++ {
|
||
|
hasBackslash := route.Path[i] == '\\'
|
||
|
if hasBackslash && i+1 < l && route.Path[i+1] == ':' {
|
||
|
i++ // backslash before colon escapes that colon. in that case skip backslash
|
||
|
}
|
||
|
if n < ln && (route.Path[i] == '*' || (!hasBackslash && route.Path[i] == ':')) {
|
||
|
// in case of `*` wildcard or `:` (unescaped colon) param we replace everything till next slash or end of path
|
||
|
for ; i < l && route.Path[i] != '/'; i++ {
|
||
|
}
|
||
|
uri.WriteString(fmt.Sprintf("%v", params[n]))
|
||
|
n++
|
||
|
}
|
||
|
if i < l {
|
||
|
uri.WriteByte(route.Path[i])
|
||
|
}
|
||
|
}
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
return uri.String()
|
||
|
}
|
||
|
|
||
|
func (r *Router) add(method, path, name string, h HandlerFunc) *Route {
|
||
|
r.Add(method, path, h)
|
||
|
|
||
|
route := &Route{
|
||
|
Method: method,
|
||
|
Path: path,
|
||
|
Name: name,
|
||
|
}
|
||
|
r.routes[method+path] = route
|
||
|
return route
|
||
|
}
|
||
|
|
||
|
// Add registers a new route for method and path with matching handler.
|
||
|
func (r *Router) Add(method, path string, h HandlerFunc) {
|
||
|
// Validate path
|
||
|
if path == "" {
|
||
|
path = "/"
|
||
|
}
|
||
|
if path[0] != '/' {
|
||
|
path = "/" + path
|
||
|
}
|
||
|
pnames := []string{} // Param names
|
||
|
ppath := path // Pristine path
|
||
|
|
||
|
if h == nil && r.echo.Logger != nil {
|
||
|
// FIXME: in future we should return error
|
||
|
r.echo.Logger.Errorf("Adding route without handler function: %v:%v", method, path)
|
||
|
}
|
||
|
|
||
|
for i, lcpIndex := 0, len(path); i < lcpIndex; i++ {
|
||
|
if path[i] == ':' {
|
||
|
if i > 0 && path[i-1] == '\\' {
|
||
|
path = path[:i-1] + path[i:]
|
||
|
i--
|
||
|
lcpIndex--
|
||
|
continue
|
||
|
}
|
||
|
j := i + 1
|
||
|
|
||
|
r.insert(method, path[:i], staticKind, routeMethod{})
|
||
|
for ; i < lcpIndex && path[i] != '/'; i++ {
|
||
|
}
|
||
|
|
||
|
pnames = append(pnames, path[j:i])
|
||
|
path = path[:j] + path[i:]
|
||
|
i, lcpIndex = j, len(path)
|
||
|
|
||
|
if i == lcpIndex {
|
||
|
// path node is last fragment of route path. ie. `/users/:id`
|
||
|
r.insert(method, path[:i], paramKind, routeMethod{ppath, pnames, h})
|
||
|
} else {
|
||
|
r.insert(method, path[:i], paramKind, routeMethod{})
|
||
|
}
|
||
|
} else if path[i] == '*' {
|
||
|
r.insert(method, path[:i], staticKind, routeMethod{})
|
||
|
pnames = append(pnames, "*")
|
||
|
r.insert(method, path[:i+1], anyKind, routeMethod{ppath, pnames, h})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
r.insert(method, path, staticKind, routeMethod{ppath, pnames, h})
|
||
|
}
|
||
|
|
||
|
func (r *Router) insert(method, path string, t kind, rm routeMethod) {
|
||
|
// Adjust max param
|
||
|
paramLen := len(rm.pnames)
|
||
|
if *r.echo.maxParam < paramLen {
|
||
|
*r.echo.maxParam = paramLen
|
||
|
}
|
||
|
|
||
|
currentNode := r.tree // Current node as root
|
||
|
if currentNode == nil {
|
||
|
panic("echo: invalid method")
|
||
|
}
|
||
|
search := path
|
||
|
|
||
|
for {
|
||
|
searchLen := len(search)
|
||
|
prefixLen := len(currentNode.prefix)
|
||
|
lcpLen := 0
|
||
|
|
||
|
// LCP - Longest Common Prefix (https://en.wikipedia.org/wiki/LCP_array)
|
||
|
max := prefixLen
|
||
|
if searchLen < max {
|
||
|
max = searchLen
|
||
|
}
|
||
|
for ; lcpLen < max && search[lcpLen] == currentNode.prefix[lcpLen]; lcpLen++ {
|
||
|
}
|
||
|
|
||
|
if lcpLen == 0 {
|
||
|
// At root node
|
||
|
currentNode.label = search[0]
|
||
|
currentNode.prefix = search
|
||
|
if rm.handler != nil {
|
||
|
currentNode.kind = t
|
||
|
currentNode.addMethod(method, &rm)
|
||
|
currentNode.paramsCount = len(rm.pnames)
|
||
|
currentNode.originalPath = rm.ppath
|
||
|
}
|
||
|
currentNode.isLeaf = currentNode.staticChildren == nil && currentNode.paramChild == nil && currentNode.anyChild == nil
|
||
|
} else if lcpLen < prefixLen {
|
||
|
// Split node into two before we insert new node.
|
||
|
// This happens when we are inserting path that is submatch of any existing inserted paths.
|
||
|
// For example, we have node `/test` and now are about to insert `/te/*`. In that case
|
||
|
// 1. overlapping part is `/te` that is used as parent node
|
||
|
// 2. `st` is part from existing node that is not matching - it gets its own node (child to `/te`)
|
||
|
// 3. `/*` is the new part we are about to insert (child to `/te`)
|
||
|
n := newNode(
|
||
|
currentNode.kind,
|
||
|
currentNode.prefix[lcpLen:],
|
||
|
currentNode,
|
||
|
currentNode.staticChildren,
|
||
|
currentNode.originalPath,
|
||
|
currentNode.methods,
|
||
|
currentNode.paramsCount,
|
||
|
currentNode.paramChild,
|
||
|
currentNode.anyChild,
|
||
|
currentNode.notFoundHandler,
|
||
|
)
|
||
|
// Update parent path for all children to new node
|
||
|
for _, child := range currentNode.staticChildren {
|
||
|
child.parent = n
|
||
|
}
|
||
|
if currentNode.paramChild != nil {
|
||
|
currentNode.paramChild.parent = n
|
||
|
}
|
||
|
if currentNode.anyChild != nil {
|
||
|
currentNode.anyChild.parent = n
|
||
|
}
|
||
|
|
||
|
// Reset parent node
|
||
|
currentNode.kind = staticKind
|
||
|
currentNode.label = currentNode.prefix[0]
|
||
|
currentNode.prefix = currentNode.prefix[:lcpLen]
|
||
|
currentNode.staticChildren = nil
|
||
|
currentNode.originalPath = ""
|
||
|
currentNode.methods = new(routeMethods)
|
||
|
currentNode.paramsCount = 0
|
||
|
currentNode.paramChild = nil
|
||
|
currentNode.anyChild = nil
|
||
|
currentNode.isLeaf = false
|
||
|
currentNode.isHandler = false
|
||
|
currentNode.notFoundHandler = nil
|
||
|
|
||
|
// Only Static children could reach here
|
||
|
currentNode.addStaticChild(n)
|
||
|
|
||
|
if lcpLen == searchLen {
|
||
|
// At parent node
|
||
|
currentNode.kind = t
|
||
|
if rm.handler != nil {
|
||
|
currentNode.addMethod(method, &rm)
|
||
|
currentNode.paramsCount = len(rm.pnames)
|
||
|
currentNode.originalPath = rm.ppath
|
||
|
}
|
||
|
} else {
|
||
|
// Create child node
|
||
|
n = newNode(t, search[lcpLen:], currentNode, nil, "", new(routeMethods), 0, nil, nil, nil)
|
||
|
if rm.handler != nil {
|
||
|
n.addMethod(method, &rm)
|
||
|
n.paramsCount = len(rm.pnames)
|
||
|
n.originalPath = rm.ppath
|
||
|
}
|
||
|
// Only Static children could reach here
|
||
|
currentNode.addStaticChild(n)
|
||
|
}
|
||
|
currentNode.isLeaf = currentNode.staticChildren == nil && currentNode.paramChild == nil && currentNode.anyChild == nil
|
||
|
} else if lcpLen < searchLen {
|
||
|
search = search[lcpLen:]
|
||
|
c := currentNode.findChildWithLabel(search[0])
|
||
|
if c != nil {
|
||
|
// Go deeper
|
||
|
currentNode = c
|
||
|
continue
|
||
|
}
|
||
|
// Create child node
|
||
|
n := newNode(t, search, currentNode, nil, rm.ppath, new(routeMethods), 0, nil, nil, nil)
|
||
|
if rm.handler != nil {
|
||
|
n.addMethod(method, &rm)
|
||
|
n.paramsCount = len(rm.pnames)
|
||
|
}
|
||
|
|
||
|
switch t {
|
||
|
case staticKind:
|
||
|
currentNode.addStaticChild(n)
|
||
|
case paramKind:
|
||
|
currentNode.paramChild = n
|
||
|
case anyKind:
|
||
|
currentNode.anyChild = n
|
||
|
}
|
||
|
currentNode.isLeaf = currentNode.staticChildren == nil && currentNode.paramChild == nil && currentNode.anyChild == nil
|
||
|
} else {
|
||
|
// Node already exists
|
||
|
if rm.handler != nil {
|
||
|
currentNode.addMethod(method, &rm)
|
||
|
currentNode.paramsCount = len(rm.pnames)
|
||
|
currentNode.originalPath = rm.ppath
|
||
|
}
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func newNode(
|
||
|
t kind,
|
||
|
pre string,
|
||
|
p *node,
|
||
|
sc children,
|
||
|
originalPath string,
|
||
|
methods *routeMethods,
|
||
|
paramsCount int,
|
||
|
paramChildren,
|
||
|
anyChildren *node,
|
||
|
notFoundHandler *routeMethod,
|
||
|
) *node {
|
||
|
return &node{
|
||
|
kind: t,
|
||
|
label: pre[0],
|
||
|
prefix: pre,
|
||
|
parent: p,
|
||
|
staticChildren: sc,
|
||
|
originalPath: originalPath,
|
||
|
methods: methods,
|
||
|
paramsCount: paramsCount,
|
||
|
paramChild: paramChildren,
|
||
|
anyChild: anyChildren,
|
||
|
isLeaf: sc == nil && paramChildren == nil && anyChildren == nil,
|
||
|
isHandler: methods.isHandler(),
|
||
|
notFoundHandler: notFoundHandler,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (n *node) addStaticChild(c *node) {
|
||
|
n.staticChildren = append(n.staticChildren, c)
|
||
|
}
|
||
|
|
||
|
func (n *node) findStaticChild(l byte) *node {
|
||
|
for _, c := range n.staticChildren {
|
||
|
if c.label == l {
|
||
|
return c
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (n *node) findChildWithLabel(l byte) *node {
|
||
|
if c := n.findStaticChild(l); c != nil {
|
||
|
return c
|
||
|
}
|
||
|
if l == paramLabel {
|
||
|
return n.paramChild
|
||
|
}
|
||
|
if l == anyLabel {
|
||
|
return n.anyChild
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (n *node) addMethod(method string, h *routeMethod) {
|
||
|
switch method {
|
||
|
case http.MethodConnect:
|
||
|
n.methods.connect = h
|
||
|
case http.MethodDelete:
|
||
|
n.methods.delete = h
|
||
|
case http.MethodGet:
|
||
|
n.methods.get = h
|
||
|
case http.MethodHead:
|
||
|
n.methods.head = h
|
||
|
case http.MethodOptions:
|
||
|
n.methods.options = h
|
||
|
case http.MethodPatch:
|
||
|
n.methods.patch = h
|
||
|
case http.MethodPost:
|
||
|
n.methods.post = h
|
||
|
case PROPFIND:
|
||
|
n.methods.propfind = h
|
||
|
case http.MethodPut:
|
||
|
n.methods.put = h
|
||
|
case http.MethodTrace:
|
||
|
n.methods.trace = h
|
||
|
case REPORT:
|
||
|
n.methods.report = h
|
||
|
case RouteNotFound:
|
||
|
n.notFoundHandler = h
|
||
|
return // RouteNotFound/404 is not considered as a handler so no further logic needs to be executed
|
||
|
default:
|
||
|
if n.methods.anyOther == nil {
|
||
|
n.methods.anyOther = make(map[string]*routeMethod)
|
||
|
}
|
||
|
if h.handler == nil {
|
||
|
delete(n.methods.anyOther, method)
|
||
|
} else {
|
||
|
n.methods.anyOther[method] = h
|
||
|
}
|
||
|
}
|
||
|
|
||
|
n.methods.updateAllowHeader()
|
||
|
n.isHandler = true
|
||
|
}
|
||
|
|
||
|
func (n *node) findMethod(method string) *routeMethod {
|
||
|
switch method {
|
||
|
case http.MethodConnect:
|
||
|
return n.methods.connect
|
||
|
case http.MethodDelete:
|
||
|
return n.methods.delete
|
||
|
case http.MethodGet:
|
||
|
return n.methods.get
|
||
|
case http.MethodHead:
|
||
|
return n.methods.head
|
||
|
case http.MethodOptions:
|
||
|
return n.methods.options
|
||
|
case http.MethodPatch:
|
||
|
return n.methods.patch
|
||
|
case http.MethodPost:
|
||
|
return n.methods.post
|
||
|
case PROPFIND:
|
||
|
return n.methods.propfind
|
||
|
case http.MethodPut:
|
||
|
return n.methods.put
|
||
|
case http.MethodTrace:
|
||
|
return n.methods.trace
|
||
|
case REPORT:
|
||
|
return n.methods.report
|
||
|
default: // RouteNotFound/404 is not considered as a handler
|
||
|
return n.methods.anyOther[method]
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func optionsMethodHandler(allowMethods string) func(c Context) error {
|
||
|
return func(c Context) error {
|
||
|
// Note: we are not handling most of the CORS headers here. CORS is handled by CORS middleware
|
||
|
// 'OPTIONS' method RFC: https://httpwg.org/specs/rfc7231.html#OPTIONS
|
||
|
// 'Allow' header RFC: https://datatracker.ietf.org/doc/html/rfc7231#section-7.4.1
|
||
|
c.Response().Header().Add(HeaderAllow, allowMethods)
|
||
|
return c.NoContent(http.StatusNoContent)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Find lookup a handler registered for method and path. It also parses URL for path
|
||
|
// parameters and load them into context.
|
||
|
//
|
||
|
// For performance:
|
||
|
//
|
||
|
// - Get context from `Echo#AcquireContext()`
|
||
|
// - Reset it `Context#Reset()`
|
||
|
// - Return it `Echo#ReleaseContext()`.
|
||
|
func (r *Router) Find(method, path string, c Context) {
|
||
|
ctx := c.(*context)
|
||
|
currentNode := r.tree // Current node as root
|
||
|
|
||
|
var (
|
||
|
previousBestMatchNode *node
|
||
|
matchedRouteMethod *routeMethod
|
||
|
// 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
|
||
|
searchIndex = 0
|
||
|
paramIndex int // Param counter
|
||
|
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.
|
||
|
// To backtrack the current node will be changed to the parent node and the next kind for the
|
||
|
// router logic will be returned based on fromKind or kind of the dead end node (static > param > any).
|
||
|
// 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.
|
||
|
backtrackToNextNodeKind := func(fromKind kind) (nextNodeKind kind, valid bool) {
|
||
|
previous := currentNode
|
||
|
currentNode = previous.parent
|
||
|
valid = currentNode != nil
|
||
|
|
||
|
// Next node type by priority
|
||
|
if previous.kind == anyKind {
|
||
|
nextNodeKind = staticKind
|
||
|
} else {
|
||
|
nextNodeKind = previous.kind + 1
|
||
|
}
|
||
|
|
||
|
if fromKind == staticKind {
|
||
|
// when backtracking is done from static kind block we did not change search so nothing to restore
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// restore search to value it was before we move to current node we are backtracking from.
|
||
|
if previous.kind == staticKind {
|
||
|
searchIndex -= len(previous.prefix)
|
||
|
} else {
|
||
|
paramIndex--
|
||
|
// 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
|
||
|
searchIndex -= len(paramValues[paramIndex])
|
||
|
paramValues[paramIndex] = ""
|
||
|
}
|
||
|
search = path[searchIndex:]
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// 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 {
|
||
|
prefixLen := 0 // Prefix length
|
||
|
lcpLen := 0 // LCP (longest common prefix) length
|
||
|
|
||
|
if currentNode.kind == staticKind {
|
||
|
searchLen := len(search)
|
||
|
prefixLen = len(currentNode.prefix)
|
||
|
|
||
|
// LCP - Longest Common Prefix (https://en.wikipedia.org/wiki/LCP_array)
|
||
|
max := prefixLen
|
||
|
if searchLen < max {
|
||
|
max = searchLen
|
||
|
}
|
||
|
for ; lcpLen < max && search[lcpLen] == currentNode.prefix[lcpLen]; lcpLen++ {
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if lcpLen != prefixLen {
|
||
|
// No matching prefix, let's backtrack to the first possible alternative node of the decision path
|
||
|
nk, ok := backtrackToNextNodeKind(staticKind)
|
||
|
if !ok {
|
||
|
return // No other possibilities on the decision path, handler will be whatever context is reset to.
|
||
|
} else if nk == paramKind {
|
||
|
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
|
||
|
//} else if nk == anyKind {
|
||
|
// goto Any
|
||
|
} else {
|
||
|
// Not found (this should never be possible for static node we are looking currently)
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// The full prefix has matched, remove the prefix from the remaining search
|
||
|
search = search[lcpLen:]
|
||
|
searchIndex = searchIndex + lcpLen
|
||
|
|
||
|
// Finish routing if is no request path remaining to search
|
||
|
if search == "" {
|
||
|
// in case of node that is handler we have exact method type match or something for 405 to use
|
||
|
if currentNode.isHandler {
|
||
|
// check if current node has handler registered for http method we are looking for. we store currentNode as
|
||
|
// best matching in case we do no find no more routes matching this path+method
|
||
|
if previousBestMatchNode == nil {
|
||
|
previousBestMatchNode = currentNode
|
||
|
}
|
||
|
if h := currentNode.findMethod(method); h != nil {
|
||
|
matchedRouteMethod = h
|
||
|
break
|
||
|
}
|
||
|
} else if currentNode.notFoundHandler != nil {
|
||
|
matchedRouteMethod = currentNode.notFoundHandler
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Static node
|
||
|
if search != "" {
|
||
|
if child := currentNode.findStaticChild(search[0]); child != nil {
|
||
|
currentNode = child
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Param:
|
||
|
// Param node
|
||
|
if child := currentNode.paramChild; search != "" && child != nil {
|
||
|
currentNode = child
|
||
|
i := 0
|
||
|
l := len(search)
|
||
|
if currentNode.isLeaf {
|
||
|
// when param node does not have any children (path param is last piece of route path) then param node should
|
||
|
// act similarly to any node - consider all remaining search as match
|
||
|
i = l
|
||
|
} else {
|
||
|
for ; i < l && search[i] != '/'; i++ {
|
||
|
}
|
||
|
}
|
||
|
|
||
|
paramValues[paramIndex] = search[:i]
|
||
|
paramIndex++
|
||
|
search = search[i:]
|
||
|
searchIndex = searchIndex + i
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
Any:
|
||
|
// Any node
|
||
|
if child := currentNode.anyChild; child != nil {
|
||
|
// If any node is found, use remaining path for paramValues
|
||
|
currentNode = child
|
||
|
paramValues[currentNode.paramsCount-1] = search
|
||
|
|
||
|
// update indexes/search in case we need to backtrack when no handler match is found
|
||
|
paramIndex++
|
||
|
searchIndex += +len(search)
|
||
|
search = ""
|
||
|
|
||
|
if h := currentNode.findMethod(method); h != nil {
|
||
|
matchedRouteMethod = h
|
||
|
break
|
||
|
}
|
||
|
// we store currentNode as best matching in case we do not find more routes matching this path+method. Needed for 405
|
||
|
if previousBestMatchNode == nil {
|
||
|
previousBestMatchNode = currentNode
|
||
|
}
|
||
|
if currentNode.notFoundHandler != nil {
|
||
|
matchedRouteMethod = currentNode.notFoundHandler
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Let's backtrack to the first possible alternative node of the decision path
|
||
|
nk, ok := backtrackToNextNodeKind(anyKind)
|
||
|
if !ok {
|
||
|
break // No other possibilities on the decision path
|
||
|
} else if nk == paramKind {
|
||
|
goto Param
|
||
|
} else if nk == anyKind {
|
||
|
goto Any
|
||
|
} else {
|
||
|
// Not found
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if currentNode == nil && previousBestMatchNode == nil {
|
||
|
return // nothing matched at all
|
||
|
}
|
||
|
|
||
|
// matchedHandler could be method+path handler that we matched or notFoundHandler from node with matching path
|
||
|
// user provided not found (404) handler has priority over generic method not found (405) handler or global 404 handler
|
||
|
var rPath string
|
||
|
var rPNames []string
|
||
|
if matchedRouteMethod != nil {
|
||
|
rPath = matchedRouteMethod.ppath
|
||
|
rPNames = matchedRouteMethod.pnames
|
||
|
ctx.handler = matchedRouteMethod.handler
|
||
|
} else {
|
||
|
// use previous match as basis. although we have no matching handler we have path match.
|
||
|
// so we can send http.StatusMethodNotAllowed (405) instead of http.StatusNotFound (404)
|
||
|
currentNode = previousBestMatchNode
|
||
|
|
||
|
rPath = currentNode.originalPath
|
||
|
rPNames = nil // no params here
|
||
|
ctx.handler = NotFoundHandler
|
||
|
if currentNode.notFoundHandler != nil {
|
||
|
rPath = currentNode.notFoundHandler.ppath
|
||
|
rPNames = currentNode.notFoundHandler.pnames
|
||
|
ctx.handler = currentNode.notFoundHandler.handler
|
||
|
} else if currentNode.isHandler {
|
||
|
ctx.Set(ContextKeyHeaderAllow, currentNode.methods.allowHeader)
|
||
|
ctx.handler = MethodNotAllowedHandler
|
||
|
if method == http.MethodOptions {
|
||
|
ctx.handler = optionsMethodHandler(currentNode.methods.allowHeader)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
ctx.path = rPath
|
||
|
ctx.pnames = rPNames
|
||
|
}
|