diff --git a/.idea/kratos.iml b/.idea/kratos.iml
deleted file mode 100644
index c956989b2..000000000
--- a/.idea/kratos.iml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index 28a804d89..000000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index c3d31912a..000000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7f4..000000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/pkg/net/http/blademaster/context.go b/pkg/net/http/blademaster/context.go
index 1879bd3de..b17866706 100644
--- a/pkg/net/http/blademaster/context.go
+++ b/pkg/net/http/blademaster/context.go
@@ -44,6 +44,11 @@ type Context struct {
method string
engine *Engine
+
+ RoutePath string
+
+ Params Params
+
}
/************************************/
@@ -55,21 +60,13 @@ type Context struct {
// See example in godoc.
func (c *Context) Next() {
c.index++
- s := int8(len(c.handlers))
- for ; c.index < s; c.index++ {
- // only check method on last handler, otherwise middlewares
- // will never be effected if request method is not matched
- if c.index == s-1 && c.method != c.Request.Method {
- code := http.StatusMethodNotAllowed
- c.Error = ecode.MethodNotAllowed
- http.Error(c.Writer, http.StatusText(code), code)
- return
- }
-
+ for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
+ c.index++
}
}
+
// Abort prevents pending handlers from being called. Note that this will not stop the current handler.
// Let's say you have an authorization middleware that validates that the current request is authorized.
// If the authorization fails (ex: the password does not match), call Abort to ensure the remaining handlers
diff --git a/pkg/net/http/blademaster/routergroup.go b/pkg/net/http/blademaster/routergroup.go
index 28d09a805..9859539fc 100644
--- a/pkg/net/http/blademaster/routergroup.go
+++ b/pkg/net/http/blademaster/routergroup.go
@@ -99,11 +99,6 @@ func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...Ha
return group.handle(httpMethod, relativePath, handlers...)
}
-// HEAD is a shortcut for router.Handle("HEAD", path, handle).
-func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes {
- return group.handle("HEAD", relativePath, handlers...)
-}
-
// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle("GET", relativePath, handlers...)
@@ -124,6 +119,22 @@ func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) I
return group.handle("DELETE", relativePath, handlers...)
}
+// PATCH is a shortcut for router.Handle("PATCH", path, handle).
+func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes {
+ return group.handle("PATCH", relativePath, handlers...)
+}
+
+// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle).
+func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes {
+ return group.handle("OPTIONS", relativePath, handlers...)
+}
+
+// HEAD is a shortcut for router.Handle("HEAD", path, handle).
+func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes {
+ return group.handle("HEAD", relativePath, handlers...)
+}
+
+
func (group *RouterGroup) combineHandlers(handlerGroups ...[]HandlerFunc) []HandlerFunc {
finalSize := len(group.Handlers)
for _, handlers := range handlerGroups {
@@ -164,3 +175,19 @@ func (group *RouterGroup) injections(relativePath string) []HandlerFunc {
}
return nil
}
+
+// Any registers a route that matches all the HTTP methods.
+// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE.
+func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes {
+ group.handle("GET", relativePath, handlers...)
+ group.handle("POST", relativePath, handlers...)
+ group.handle("PUT", relativePath, handlers...)
+ group.handle("PATCH", relativePath, handlers...)
+ group.handle("HEAD", relativePath, handlers...)
+ group.handle("OPTIONS", relativePath, handlers...)
+ group.handle("DELETE", relativePath, handlers...)
+ group.handle("CONNECT", relativePath, handlers...)
+ group.handle("TRACE", relativePath, handlers...)
+ return group.returnObj()
+}
+
diff --git a/pkg/net/http/blademaster/server.go b/pkg/net/http/blademaster/server.go
index d685cf13d..19965b5dc 100644
--- a/pkg/net/http/blademaster/server.go
+++ b/pkg/net/http/blademaster/server.go
@@ -31,7 +31,10 @@ var (
_ IRouter = &Engine{}
stats = stat.HTTPServer
- _httpDSN string
+ _httpDSN string
+ default405Body = []byte("405 method not allowed")
+ default404Body = []byte("404 page not found")
+
)
func init() {
@@ -122,7 +125,7 @@ type Engine struct {
address string
- mux *http.ServeMux // http mux router
+ trees methodTrees
server atomic.Value // store *http.Server
metastore map[string]map[string]interface{} // metastore is the path as key and the metadata of this path as value, it export via /metadata
@@ -130,6 +133,28 @@ type Engine struct {
methodConfigs map[string]*MethodConfig
injections []injection
+
+ // If enabled, the url.RawPath will be used to find parameters.
+ UseRawPath bool
+
+ // If true, the path value will be unescaped.
+ // If UseRawPath is false (by default), the UnescapePathValues effectively is true,
+ // as url.Path gonna be used, which is already unescaped.
+ UnescapePathValues bool
+
+ // If enabled, the router checks if another method is allowed for the
+ // current route, if the current request can not be routed.
+ // If this is the case, the request is answered with 'Method Not Allowed'
+ // and HTTP status code 405.
+ // If no other Method is allowed, the request is delegated to the NotFound
+ // handler.
+ HandleMethodNotAllowed bool
+
+ allNoRoute []HandlerFunc
+ allNoMethod []HandlerFunc
+ noRoute []HandlerFunc
+ noMethod []HandlerFunc
+
}
type injection struct {
@@ -151,18 +176,28 @@ func NewServer(conf *ServerConfig) *Engine {
basePath: "/",
root: true,
},
- address: ip.InternalIP(),
- mux: http.NewServeMux(),
- metastore: make(map[string]map[string]interface{}),
- methodConfigs: make(map[string]*MethodConfig),
- }
- if err := engine.SetConfig(conf); err != nil {
- panic(err)
+ conf: &ServerConfig{
+ Timeout: xtime.Duration(time.Second),
+ },
+ address: ip.InternalIP(),
+ trees: make(methodTrees, 0, 9),
+ metastore: make(map[string]map[string]interface{}),
+ methodConfigs: make(map[string]*MethodConfig),
+ HandleMethodNotAllowed: true,
+ injections: make([]injection, 0),
}
engine.RouterGroup.engine = engine
// NOTE add prometheus monitor location
engine.addRoute("GET", "/metrics", monitor())
engine.addRoute("GET", "/metadata", engine.metadata())
+ engine.NoRoute(func(c *Context) {
+ c.Bytes(404, "text/plain", default404Body)
+ c.Abort()
+ })
+ engine.NoMethod(func(c *Context) {
+ c.Bytes(405, "text/plain", []byte(http.StatusText(405)))
+ c.Abort()
+ })
startPerf()
return engine
}
@@ -195,46 +230,60 @@ func (engine *Engine) addRoute(method, path string, handlers ...HandlerFunc) {
engine.metastore[path] = make(map[string]interface{})
}
engine.metastore[path]["method"] = method
- engine.mux.HandleFunc(path, func(w http.ResponseWriter, req *http.Request) {
- c := &Context{
- Context: nil,
- engine: engine,
- index: -1,
- handlers: nil,
- Keys: nil,
- method: "",
- Error: nil,
- }
+ root := engine.trees.get(method)
+ if root == nil {
+ root = new(node)
+ engine.trees = append(engine.trees, methodTree{method: method, root: root})
+ }
- c.Request = req
- c.Writer = w
- c.handlers = handlers
+ prelude := func(c *Context) {
c.method = method
-
- engine.handleContext(c)
- })
+ c.RoutePath = path
+ }
+ handlers = append([]HandlerFunc{prelude}, handlers...)
+ root.addRoute(path, handlers)
}
-// SetConfig is used to set the engine configuration.
-// Only the valid config will be loaded.
-func (engine *Engine) SetConfig(conf *ServerConfig) (err error) {
- if conf.Timeout <= 0 {
- return errors.New("blademaster: config timeout must greater than 0")
+func (engine *Engine) prepareHandler(c *Context) {
+ httpMethod := c.Request.Method
+ rPath := c.Request.URL.Path
+ unescape := false
+ if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
+ rPath = c.Request.URL.RawPath
+ unescape = engine.UnescapePathValues
}
- if conf.Network == "" {
- conf.Network = "tcp"
+ rPath = cleanPath(rPath)
+
+ // Find root of the tree for the given HTTP method
+ t := engine.trees
+ for i, tl := 0, len(t); i < tl; i++ {
+ if t[i].method != httpMethod {
+ continue
+ }
+ root := t[i].root
+ // Find route in tree
+ handlers, params, _ := root.getValue(rPath, c.Params, unescape)
+ if handlers != nil {
+ c.handlers = handlers
+ c.Params = params
+ return
+ }
+ break
}
- engine.lock.Lock()
- engine.conf = conf
- engine.lock.Unlock()
- return
-}
-func (engine *Engine) methodConfig(path string) *MethodConfig {
- engine.pcLock.RLock()
- mc := engine.methodConfigs[path]
- engine.pcLock.RUnlock()
- return mc
+ if engine.HandleMethodNotAllowed {
+ for _, tree := range engine.trees {
+ if tree.method == httpMethod {
+ continue
+ }
+ if handlers, _, _ := tree.root.getValue(rPath, nil, unescape); handlers != nil {
+ c.handlers = engine.allNoMethod
+ return
+ }
+ }
+ }
+ c.handlers = engine.allNoRoute
+ return
}
func (engine *Engine) handleContext(c *Context) {
@@ -274,12 +323,35 @@ func (engine *Engine) handleContext(c *Context) {
c.Context, cancel = context.WithCancel(ctx)
}
defer cancel()
+ engine.prepareHandler(c)
c.Next()
}
+// SetConfig is used to set the engine configuration.
+// Only the valid config will be loaded.
+func (engine *Engine) SetConfig(conf *ServerConfig) (err error) {
+ if conf.Timeout <= 0 {
+ return errors.New("blademaster: config timeout must greater than 0")
+ }
+ if conf.Network == "" {
+ conf.Network = "tcp"
+ }
+ engine.lock.Lock()
+ engine.conf = conf
+ engine.lock.Unlock()
+ return
+}
+
+func (engine *Engine) methodConfig(path string) *MethodConfig {
+ engine.pcLock.RLock()
+ mc := engine.methodConfigs[path]
+ engine.pcLock.RUnlock()
+ return mc
+}
+
// Router return a http.Handler for using http.ListenAndServe() directly.
func (engine *Engine) Router() http.Handler {
- return engine.mux
+ return engine
}
// Server is used to load stored http server.
@@ -305,6 +377,8 @@ func (engine *Engine) Shutdown(ctx context.Context) error {
// For example, this is the right place for a logger or error management middleware.
func (engine *Engine) UseFunc(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.UseFunc(middleware...)
+ engine.rebuild404Handlers()
+ engine.rebuild405Handlers()
return engine
}
@@ -333,7 +407,7 @@ func (engine *Engine) Run(addr ...string) (err error) {
address := resolveAddress(addr)
server := &http.Server{
Addr: address,
- Handler: engine.mux,
+ Handler: engine,
}
engine.server.Store(server)
if err = server.ListenAndServe(); err != nil {
@@ -348,7 +422,7 @@ func (engine *Engine) Run(addr ...string) (err error) {
func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
server := &http.Server{
Addr: addr,
- Handler: engine.mux,
+ Handler: engine,
}
engine.server.Store(server)
if err = server.ListenAndServeTLS(certFile, keyFile); err != nil {
@@ -369,7 +443,7 @@ func (engine *Engine) RunUnix(file string) (err error) {
}
defer listener.Close()
server := &http.Server{
- Handler: engine.mux,
+ Handler: engine,
}
engine.server.Store(server)
if err = server.Serve(listener); err != nil {
@@ -381,7 +455,7 @@ func (engine *Engine) RunUnix(file string) (err error) {
// RunServer will serve and start listening HTTP requests by given server and listener.
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) RunServer(server *http.Server, l net.Listener) (err error) {
- server.Handler = engine.mux
+ server.Handler = engine
engine.server.Store(server)
if err = server.Serve(l); err != nil {
err = errors.Wrapf(err, "listen server: %+v/%+v", server, l)
@@ -403,3 +477,41 @@ func (engine *Engine) Inject(pattern string, handlers ...HandlerFunc) {
handlers: handlers,
})
}
+
+// ServeHTTP conforms to the http.Handler interface.
+func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+ c := &Context{
+ Context: nil,
+ engine: engine,
+ index: -1,
+ handlers: nil,
+ Keys: nil,
+ method: "",
+ Error: nil,
+ }
+
+ c.Request = req
+ c.Writer = w
+
+ engine.handleContext(c)
+}
+
+// NoRoute adds handlers for NoRoute. It return a 404 code by default.
+func (engine *Engine) NoRoute(handlers ...HandlerFunc) {
+ engine.noRoute = handlers
+ engine.rebuild404Handlers()
+}
+
+// NoMethod sets the handlers called when... TODO.
+func (engine *Engine) NoMethod(handlers ...HandlerFunc) {
+ engine.noMethod = handlers
+ engine.rebuild405Handlers()
+}
+
+func (engine *Engine) rebuild404Handlers() {
+ engine.allNoRoute = engine.combineHandlers(engine.noRoute)
+}
+
+func (engine *Engine) rebuild405Handlers() {
+ engine.allNoMethod = engine.combineHandlers(engine.noMethod)
+}
diff --git a/pkg/net/http/blademaster/tree_test.go b/pkg/net/http/blademaster/tree_test.go
deleted file mode 100644
index 991b5ce93..000000000
--- a/pkg/net/http/blademaster/tree_test.go
+++ /dev/null
@@ -1,698 +0,0 @@
-// Copyright 2013 Julien Schmidt. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be found
-// at https://github.com/julienschmidt/httprouter/blob/master/LICENSE
-
-package blademaster
-
-import (
-"fmt"
-"reflect"
-"regexp"
-"strings"
-"testing"
-)
-
-// Used as a workaround since we can't compare functions or their addresses
-var fakeHandlerValue string
-
-func fakeHandler(val string) []HandlerFunc {
- return []HandlerFunc{func(c *Context) {
- fakeHandlerValue = val
- }}
-}
-
-type testRequests []struct {
- path string
- nilHandler bool
- route string
- ps Params
-}
-
-func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ...bool) {
- unescape := false
- if len(unescapes) >= 1 {
- unescape = unescapes[0]
- }
-
- for _, request := range requests {
- handler, ps, _ := tree.getValue(request.path, nil, unescape)
-
- if handler == nil {
- if !request.nilHandler {
- t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path)
- }
- } else if request.nilHandler {
- t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path)
- } else {
- handler[0](nil)
- if fakeHandlerValue != request.route {
- t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route)
- }
- }
-
- if !reflect.DeepEqual(ps, request.ps) {
- t.Errorf("Params mismatch for route '%s'", request.path)
- }
- }
-}
-
-func checkPriorities(t *testing.T, n *node) uint32 {
- var prio uint32
- for i := range n.children {
- prio += checkPriorities(t, n.children[i])
- }
-
- if n.handlers != nil {
- prio++
- }
-
- if n.priority != prio {
- t.Errorf(
- "priority mismatch for node '%s': is %d, should be %d",
- n.path, n.priority, prio,
- )
- }
-
- return prio
-}
-
-func checkMaxParams(t *testing.T, n *node) uint8 {
- var maxParams uint8
- for i := range n.children {
- params := checkMaxParams(t, n.children[i])
- if params > maxParams {
- maxParams = params
- }
- }
- if n.nType > root && !n.wildChild {
- maxParams++
- }
-
- if n.maxParams != maxParams {
- t.Errorf(
- "maxParams mismatch for node '%s': is %d, should be %d",
- n.path, n.maxParams, maxParams,
- )
- }
-
- return maxParams
-}
-
-func TestCountParams(t *testing.T) {
- if countParams("/path/:param1/static/*catch-all") != 2 {
- t.Fail()
- }
- if countParams(strings.Repeat("/:param", 256)) != 255 {
- t.Fail()
- }
-}
-
-func TestTreeAddAndGet(t *testing.T) {
- tree := &node{}
-
- routes := [...]string{
- "/hi",
- "/contact",
- "/co",
- "/c",
- "/a",
- "/ab",
- "/doc/",
- "/doc/go_faq.html",
- "/doc/go1.html",
- "/α",
- "/β",
- }
- for _, route := range routes {
- tree.addRoute(route, fakeHandler(route))
- }
-
- checkRequests(t, tree, testRequests{
- {"/a", false, "/a", nil},
- {"/", true, "", nil},
- {"/hi", false, "/hi", nil},
- {"/contact", false, "/contact", nil},
- {"/co", false, "/co", nil},
- {"/con", true, "", nil}, // key mismatch
- {"/cona", true, "", nil}, // key mismatch
- {"/no", true, "", nil}, // no matching child
- {"/ab", false, "/ab", nil},
- {"/α", false, "/α", nil},
- {"/β", false, "/β", nil},
- })
-
- checkPriorities(t, tree)
- checkMaxParams(t, tree)
-}
-
-func TestTreeWildcard(t *testing.T) {
- tree := &node{}
-
- routes := [...]string{
- "/",
- "/cmd/:tool/:sub",
- "/cmd/:tool/",
- "/src/*filepath",
- "/search/",
- "/search/:query",
- "/user_:name",
- "/user_:name/about",
- "/files/:dir/*filepath",
- "/doc/",
- "/doc/go_faq.html",
- "/doc/go1.html",
- "/info/:user/public",
- "/info/:user/project/:project",
- }
- for _, route := range routes {
- tree.addRoute(route, fakeHandler(route))
- }
-
- checkRequests(t, tree, testRequests{
- {"/", false, "/", nil},
- {"/cmd/test/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}},
- {"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}},
- {"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "test"}, Param{Key: "sub", Value: "3"}}},
- {"/src/", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/"}}},
- {"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
- {"/search/", false, "/search/", nil},
- {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
- {"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
- {"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}},
- {"/user_gopher/about", false, "/user_:name/about", Params{Param{Key: "name", Value: "gopher"}}},
- {"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{Key: "dir", Value: "js"}, Param{Key: "filepath", Value: "/inc/framework.js"}}},
- {"/info/gordon/public", false, "/info/:user/public", Params{Param{Key: "user", Value: "gordon"}}},
- {"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}},
- })
-
- checkPriorities(t, tree)
- checkMaxParams(t, tree)
-}
-
-func TestUnescapeParameters(t *testing.T) {
- tree := &node{}
-
- routes := [...]string{
- "/",
- "/cmd/:tool/:sub",
- "/cmd/:tool/",
- "/src/*filepath",
- "/search/:query",
- "/files/:dir/*filepath",
- "/info/:user/project/:project",
- "/info/:user",
- }
- for _, route := range routes {
- tree.addRoute(route, fakeHandler(route))
- }
-
- unescape := true
- checkRequests(t, tree, testRequests{
- {"/", false, "/", nil},
- {"/cmd/test/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}},
- {"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}},
- {"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
- {"/src/some/file+test.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file test.png"}}},
- {"/src/some/file++++%%%%test.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file++++%%%%test.png"}}},
- {"/src/some/file%2Ftest.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file/test.png"}}},
- {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng in ünìcodé"}}},
- {"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}},
- {"/info/slash%2Fgordon", false, "/info/:user", Params{Param{Key: "user", Value: "slash/gordon"}}},
- {"/info/slash%2Fgordon/project/Project%20%231", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "slash/gordon"}, Param{Key: "project", Value: "Project #1"}}},
- {"/info/slash%%%%", false, "/info/:user", Params{Param{Key: "user", Value: "slash%%%%"}}},
- {"/info/slash%%%%2Fgordon/project/Project%%%%20%231", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "slash%%%%2Fgordon"}, Param{Key: "project", Value: "Project%%%%20%231"}}},
- }, unescape)
-
- checkPriorities(t, tree)
- checkMaxParams(t, tree)
-}
-
-func catchPanic(testFunc func()) (recv interface{}) {
- defer func() {
- recv = recover()
- }()
-
- testFunc()
- return
-}
-
-type testRoute struct {
- path string
- conflict bool
-}
-
-func testRoutes(t *testing.T, routes []testRoute) {
- tree := &node{}
-
- for _, route := range routes {
- recv := catchPanic(func() {
- tree.addRoute(route.path, nil)
- })
-
- if route.conflict {
- if recv == nil {
- t.Errorf("no panic for conflicting route '%s'", route.path)
- }
- } else if recv != nil {
- t.Errorf("unexpected panic for route '%s': %v", route.path, recv)
- }
- }
-}
-
-func TestTreeWildcardConflict(t *testing.T) {
- routes := []testRoute{
- {"/cmd/:tool/:sub", false},
- {"/cmd/vet", true},
- {"/src/*filepath", false},
- {"/src/*filepathx", true},
- {"/src/", true},
- {"/src1/", false},
- {"/src1/*filepath", true},
- {"/src2*filepath", true},
- {"/search/:query", false},
- {"/search/invalid", true},
- {"/user_:name", false},
- {"/user_x", true},
- {"/user_:name", false},
- {"/id:id", false},
- {"/id/:id", true},
- }
- testRoutes(t, routes)
-}
-
-func TestTreeChildConflict(t *testing.T) {
- routes := []testRoute{
- {"/cmd/vet", false},
- {"/cmd/:tool/:sub", true},
- {"/src/AUTHORS", false},
- {"/src/*filepath", true},
- {"/user_x", false},
- {"/user_:name", true},
- {"/id/:id", false},
- {"/id:id", true},
- {"/:id", true},
- {"/*filepath", true},
- }
- testRoutes(t, routes)
-}
-
-func TestTreeDupliatePath(t *testing.T) {
- tree := &node{}
-
- routes := [...]string{
- "/",
- "/doc/",
- "/src/*filepath",
- "/search/:query",
- "/user_:name",
- }
- for _, route := range routes {
- recv := catchPanic(func() {
- tree.addRoute(route, fakeHandler(route))
- })
- if recv != nil {
- t.Fatalf("panic inserting route '%s': %v", route, recv)
- }
-
- // Add again
- recv = catchPanic(func() {
- tree.addRoute(route, nil)
- })
- if recv == nil {
- t.Fatalf("no panic while inserting duplicate route '%s", route)
- }
- }
-
- checkRequests(t, tree, testRequests{
- {"/", false, "/", nil},
- {"/doc/", false, "/doc/", nil},
- {"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
- {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
- {"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}},
- })
-}
-
-func TestEmptyWildcardName(t *testing.T) {
- tree := &node{}
-
- routes := [...]string{
- "/user:",
- "/user:/",
- "/cmd/:/",
- "/src/*",
- }
- for _, route := range routes {
- recv := catchPanic(func() {
- tree.addRoute(route, nil)
- })
- if recv == nil {
- t.Fatalf("no panic while inserting route with empty wildcard name '%s", route)
- }
- }
-}
-
-func TestTreeCatchAllConflict(t *testing.T) {
- routes := []testRoute{
- {"/src/*filepath/x", true},
- {"/src2/", false},
- {"/src2/*filepath/x", true},
- }
- testRoutes(t, routes)
-}
-
-func TestTreeCatchAllConflictRoot(t *testing.T) {
- routes := []testRoute{
- {"/", false},
- {"/*filepath", true},
- }
- testRoutes(t, routes)
-}
-
-func TestTreeDoubleWildcard(t *testing.T) {
- const panicMsg = "only one wildcard per path segment is allowed"
-
- routes := [...]string{
- "/:foo:bar",
- "/:foo:bar/",
- "/:foo*bar",
- }
-
- for _, route := range routes {
- tree := &node{}
- recv := catchPanic(func() {
- tree.addRoute(route, nil)
- })
-
- if rs, ok := recv.(string); !ok || !strings.HasPrefix(rs, panicMsg) {
- t.Fatalf(`"Expected panic "%s" for route '%s', got "%v"`, panicMsg, route, recv)
- }
- }
-}
-
-/*func TestTreeDuplicateWildcard(t *testing.T) {
- tree := &node{}
- routes := [...]string{
- "/:id/:name/:id",
- }
- for _, route := range routes {
- ...
- }
-}*/
-
-func TestTreeTrailingSlashRedirect(t *testing.T) {
- tree := &node{}
-
- routes := [...]string{
- "/hi",
- "/b/",
- "/search/:query",
- "/cmd/:tool/",
- "/src/*filepath",
- "/x",
- "/x/y",
- "/y/",
- "/y/z",
- "/0/:id",
- "/0/:id/1",
- "/1/:id/",
- "/1/:id/2",
- "/aa",
- "/a/",
- "/admin",
- "/admin/:category",
- "/admin/:category/:page",
- "/doc",
- "/doc/go_faq.html",
- "/doc/go1.html",
- "/no/a",
- "/no/b",
- "/api/hello/:name",
- }
- for _, route := range routes {
- recv := catchPanic(func() {
- tree.addRoute(route, fakeHandler(route))
- })
- if recv != nil {
- t.Fatalf("panic inserting route '%s': %v", route, recv)
- }
- }
-
- tsrRoutes := [...]string{
- "/hi/",
- "/b",
- "/search/gopher/",
- "/cmd/vet",
- "/src",
- "/x/",
- "/y",
- "/0/go/",
- "/1/go",
- "/a",
- "/admin/",
- "/admin/config/",
- "/admin/config/permissions/",
- "/doc/",
- }
- for _, route := range tsrRoutes {
- handler, _, tsr := tree.getValue(route, nil, false)
- if handler != nil {
- t.Fatalf("non-nil handler for TSR route '%s", route)
- } else if !tsr {
- t.Errorf("expected TSR recommendation for route '%s'", route)
- }
- }
-
- noTsrRoutes := [...]string{
- "/",
- "/no",
- "/no/",
- "/_",
- "/_/",
- "/api/world/abc",
- }
- for _, route := range noTsrRoutes {
- handler, _, tsr := tree.getValue(route, nil, false)
- if handler != nil {
- t.Fatalf("non-nil handler for No-TSR route '%s", route)
- } else if tsr {
- t.Errorf("expected no TSR recommendation for route '%s'", route)
- }
- }
-}
-
-func TestTreeRootTrailingSlashRedirect(t *testing.T) {
- tree := &node{}
-
- recv := catchPanic(func() {
- tree.addRoute("/:test", fakeHandler("/:test"))
- })
- if recv != nil {
- t.Fatalf("panic inserting test route: %v", recv)
- }
-
- handler, _, tsr := tree.getValue("/", nil, false)
- if handler != nil {
- t.Fatalf("non-nil handler")
- } else if tsr {
- t.Errorf("expected no TSR recommendation")
- }
-}
-
-func TestTreeFindCaseInsensitivePath(t *testing.T) {
- tree := &node{}
-
- routes := [...]string{
- "/hi",
- "/b/",
- "/ABC/",
- "/search/:query",
- "/cmd/:tool/",
- "/src/*filepath",
- "/x",
- "/x/y",
- "/y/",
- "/y/z",
- "/0/:id",
- "/0/:id/1",
- "/1/:id/",
- "/1/:id/2",
- "/aa",
- "/a/",
- "/doc",
- "/doc/go_faq.html",
- "/doc/go1.html",
- "/doc/go/away",
- "/no/a",
- "/no/b",
- }
-
- for _, route := range routes {
- recv := catchPanic(func() {
- tree.addRoute(route, fakeHandler(route))
- })
- if recv != nil {
- t.Fatalf("panic inserting route '%s': %v", route, recv)
- }
- }
-
- // Check out == in for all registered routes
- // With fixTrailingSlash = true
- for _, route := range routes {
- out, found := tree.findCaseInsensitivePath(route, true)
- if !found {
- t.Errorf("Route '%s' not found!", route)
- } else if string(out) != route {
- t.Errorf("Wrong result for route '%s': %s", route, string(out))
- }
- }
- // With fixTrailingSlash = false
- for _, route := range routes {
- out, found := tree.findCaseInsensitivePath(route, false)
- if !found {
- t.Errorf("Route '%s' not found!", route)
- } else if string(out) != route {
- t.Errorf("Wrong result for route '%s': %s", route, string(out))
- }
- }
-
- tests := []struct {
- in string
- out string
- found bool
- slash bool
- }{
- {"/HI", "/hi", true, false},
- {"/HI/", "/hi", true, true},
- {"/B", "/b/", true, true},
- {"/B/", "/b/", true, false},
- {"/abc", "/ABC/", true, true},
- {"/abc/", "/ABC/", true, false},
- {"/aBc", "/ABC/", true, true},
- {"/aBc/", "/ABC/", true, false},
- {"/abC", "/ABC/", true, true},
- {"/abC/", "/ABC/", true, false},
- {"/SEARCH/QUERY", "/search/QUERY", true, false},
- {"/SEARCH/QUERY/", "/search/QUERY", true, true},
- {"/CMD/TOOL/", "/cmd/TOOL/", true, false},
- {"/CMD/TOOL", "/cmd/TOOL/", true, true},
- {"/SRC/FILE/PATH", "/src/FILE/PATH", true, false},
- {"/x/Y", "/x/y", true, false},
- {"/x/Y/", "/x/y", true, true},
- {"/X/y", "/x/y", true, false},
- {"/X/y/", "/x/y", true, true},
- {"/X/Y", "/x/y", true, false},
- {"/X/Y/", "/x/y", true, true},
- {"/Y/", "/y/", true, false},
- {"/Y", "/y/", true, true},
- {"/Y/z", "/y/z", true, false},
- {"/Y/z/", "/y/z", true, true},
- {"/Y/Z", "/y/z", true, false},
- {"/Y/Z/", "/y/z", true, true},
- {"/y/Z", "/y/z", true, false},
- {"/y/Z/", "/y/z", true, true},
- {"/Aa", "/aa", true, false},
- {"/Aa/", "/aa", true, true},
- {"/AA", "/aa", true, false},
- {"/AA/", "/aa", true, true},
- {"/aA", "/aa", true, false},
- {"/aA/", "/aa", true, true},
- {"/A/", "/a/", true, false},
- {"/A", "/a/", true, true},
- {"/DOC", "/doc", true, false},
- {"/DOC/", "/doc", true, true},
- {"/NO", "", false, true},
- {"/DOC/GO", "", false, true},
- }
- // With fixTrailingSlash = true
- for _, test := range tests {
- out, found := tree.findCaseInsensitivePath(test.in, true)
- if found != test.found || (found && (string(out) != test.out)) {
- t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t",
- test.in, string(out), found, test.out, test.found)
- return
- }
- }
- // With fixTrailingSlash = false
- for _, test := range tests {
- out, found := tree.findCaseInsensitivePath(test.in, false)
- if test.slash {
- if found { // test needs a trailingSlash fix. It must not be found!
- t.Errorf("Found without fixTrailingSlash: %s; got %s", test.in, string(out))
- }
- } else {
- if found != test.found || (found && (string(out) != test.out)) {
- t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t",
- test.in, string(out), found, test.out, test.found)
- return
- }
- }
- }
-}
-
-func TestTreeInvalidNodeType(t *testing.T) {
- const panicMsg = "invalid node type"
-
- tree := &node{}
- tree.addRoute("/", fakeHandler("/"))
- tree.addRoute("/:page", fakeHandler("/:page"))
-
- // set invalid node type
- tree.children[0].nType = 42
-
- // normal lookup
- recv := catchPanic(func() {
- tree.getValue("/test", nil, false)
- })
- if rs, ok := recv.(string); !ok || rs != panicMsg {
- t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv)
- }
-
- // case-insensitive lookup
- recv = catchPanic(func() {
- tree.findCaseInsensitivePath("/test", true)
- })
- if rs, ok := recv.(string); !ok || rs != panicMsg {
- t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv)
- }
-}
-
-func TestTreeWildcardConflictEx(t *testing.T) {
- conflicts := [...]struct {
- route string
- segPath string
- existPath string
- existSegPath string
- }{
- {"/who/are/foo", "/foo", `/who/are/\*you`, `/\*you`},
- {"/who/are/foo/", "/foo/", `/who/are/\*you`, `/\*you`},
- {"/who/are/foo/bar", "/foo/bar", `/who/are/\*you`, `/\*you`},
- {"/conxxx", "xxx", `/con:tact`, `:tact`},
- {"/conooo/xxx", "ooo", `/con:tact`, `:tact`},
- }
-
- for _, conflict := range conflicts {
- // I have to re-create a 'tree', because the 'tree' will be
- // in an inconsistent state when the loop recovers from the
- // panic which threw by 'addRoute' function.
- tree := &node{}
- routes := [...]string{
- "/con:tact",
- "/who/are/*you",
- "/who/foo/hello",
- }
-
- for _, route := range routes {
- tree.addRoute(route, fakeHandler(route))
- }
-
- recv := catchPanic(func() {
- tree.addRoute(conflict.route, fakeHandler(conflict.route))
- })
-
- if !regexp.MustCompile(fmt.Sprintf("'%s' in new path .* conflicts with existing wildcard '%s' in existing prefix '%s'",
- conflict.segPath, conflict.existSegPath, conflict.existPath)).MatchString(fmt.Sprint(recv)) {
- t.Fatalf("invalid wildcard conflict error (%v)", recv)
- }
- }
-}
-