From 09ddb258cf8c46d93b6f6e432ff9fa038053381d Mon Sep 17 00:00:00 2001 From: Abdelrahman Ahmed Date: Thu, 18 Jun 2020 19:27:43 +0200 Subject: [PATCH] use maps instead of tst (#42) * use maps instead of tst * add dependabot * update readme --- .github/auto_assign.yml | 4 +- .github/dependabot.yml | 6 + .github/workflows/security.yml | 2 +- .github/workflows/test_and_build.yml | 4 +- README.md | 33 ++-- cache.go | 39 ++-- cache_test.go | 43 +++-- context.go | 2 +- gearbox.go | 46 +++-- gearbox_test.go | 9 +- go.mod | 2 +- router.go | 114 ++++++------ router_test.go | 263 ++++++++++++++------------- tst.go | 91 --------- tst_test.go | 65 ------- utils.go | 11 ++ utils_test.go | 31 ++++ 17 files changed, 334 insertions(+), 431 deletions(-) create mode 100644 .github/dependabot.yml delete mode 100644 tst.go delete mode 100644 tst_test.go create mode 100644 utils.go create mode 100644 utils_test.go diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml index da07d6d..cee834f 100644 --- a/.github/auto_assign.yml +++ b/.github/auto_assign.yml @@ -5,10 +5,10 @@ addReviewers: true addAssignees: true # A list of reviewers to be added to pull requests (GitHub user name) -reviewers: +reviewers: - abahmed -# A list of keywords to be skipped the process that add reviewers if pull requests include it +# A list of keywords to be skipped the process that add reviewers if pull requests include it skipKeywords: - wip diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..20e1ef1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "daily" \ No newline at end of file diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 7b60de5..a974010 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -16,7 +16,7 @@ jobs: env: GO111MODULE: on steps: - - name: Checkout Source + - name: Checkout Source uses: actions/checkout@v2 - name: Run Gosec Security Scanner uses: securego/gosec@master diff --git a/.github/workflows/test_and_build.yml b/.github/workflows/test_and_build.yml index dee9fdb..7130b80 100644 --- a/.github/workflows/test_and_build.yml +++ b/.github/workflows/test_and_build.yml @@ -40,13 +40,13 @@ jobs: - name: Install dependencies run: go get -v golang.org/x/lint/golint - + - name: Build run: go build - name: Lint run: golint -set_exit_status ./... - + - name: Test run: go test -race --coverprofile=coverage.txt --covermode=atomic ./... diff --git a/README.md b/README.md index 2cb20dc..af06f7e 100644 --- a/README.md +++ b/README.md @@ -81,17 +81,17 @@ func main() { // Handler with parameter gb.Get("/users/:user", func(ctx *gearbox.Context) { - fmt.Printf("%s\n", ctx.Params.GetString("user")) + fmt.Printf("%s\n", ctx.Params["user"]) }) // Handler with optional parameter gb.Get("/search/:pattern?", func(ctx *gearbox.Context) { - fmt.Printf("%s\n", ctx.Params.GetString("pattern")) + fmt.Printf("%s\n", ctx.Params["pattern"]) }) // Handler with regex parameter gb.Get("/book/:name:([a-z]+[0-3])", func(ctx *gearbox.Context) { - fmt.Printf("%s\n", ctx.Params.GetString("name")) + fmt.Printf("%s\n", ctx.Params["name"]) }) // Start service @@ -132,18 +132,21 @@ func main() { ctx.RequestCtx.Response.SetBodyString("Hello World!") }) - // Register the routes to be used when grouping routes - routes := []*gearbox.Route{gb.Get("/id", func(ctx *gearbox.Context) { - ctx.RequestCtx.Response.SetBodyString("User X") - }), gb.Delete("/id", func(ctx *gearbox.Context) { - ctx.RequestCtx.Response.SetBodyString("Deleted") - })} - - // Group account routes - accountRoutes := gb.Group("/account", routes) - - // Group account routes to be under api - gb.Group("/api", accountRoutes) + // Register the routes to be used when grouping routes + routes := []*gearbox.Route { + gb.Get("/id", func(ctx *gearbox.Context) { + ctx.RequestCtx.Response.SetBodyString("User X") + }), + gb.Delete("/id", func(ctx *gearbox.Context) { + ctx.RequestCtx.Response.SetBodyString("Deleted") + }) + } + + // Group account routes + accountRoutes := gb.Group("/account", routes) + + // Group account routes to be under api + gb.Group("/api", accountRoutes) // Define a route with unAuthorizedMiddleware as the middleware // you can define as many middlewares as you want and have the handler as the last argument diff --git a/cache.go b/cache.go index 7cc2888..4345eb1 100644 --- a/cache.go +++ b/cache.go @@ -7,28 +7,28 @@ import ( // Implementation of LRU caching using doubly linked list and tst -// cache returns LRU cache -type cache interface { - Set(key []byte, value interface{}) - Get(key []byte) interface{} +// Cache returns LRU cache +type Cache interface { + Set(key string, value interface{}) + Get(key string) interface{} } // lruCache holds info used for caching internally type lruCache struct { capacity int list *list.List - store tst + store map[string]interface{} mutex sync.RWMutex } // pair contains key and value of element type pair struct { - key []byte + key string value interface{} } -// newCache returns LRU cache -func newCache(capacity int) cache { +// NewCache returns LRU cache +func NewCache(capacity int) Cache { // minimum is 1 if capacity <= 0 { capacity = 1 @@ -37,17 +37,17 @@ func newCache(capacity int) cache { return &lruCache{ capacity: capacity, list: new(list.List), - store: newTST(), + store: make(map[string]interface{}), } } // Get returns value of provided key if it's existing -func (c *lruCache) Get(key []byte) interface{} { +func (c *lruCache) Get(key string) interface{} { c.mutex.RLock() defer c.mutex.RUnlock() // check if list node exists - if node, ok := c.store.Get(key).(*list.Element); ok { + if node, ok := c.store[key].(*list.Element); ok { c.list.MoveToFront(node) return node.Value.(*pair).value @@ -56,31 +56,30 @@ func (c *lruCache) Get(key []byte) interface{} { } // Set adds a value to provided key in cache -func (c *lruCache) Set(key []byte, value interface{}) { +func (c *lruCache) Set(key string, value interface{}) { c.mutex.Lock() defer c.mutex.Unlock() // update the value if key is existing - if node, ok := c.store.Get(key).(*list.Element); ok { + if node, ok := c.store[key].(*list.Element); ok { c.list.MoveToFront(node) - node.Value.(*pair).value = value + node.Value.(*pair).value = value return } // remove last node if cache is full if c.list.Len() == c.capacity { - lastKey := c.list.Back().Value.(*pair).key + lastNode := c.list.Back() // delete key's value - c.store.Set(lastKey, nil) + delete(c.store, lastNode.Value.(*pair).key) - c.list.Remove(c.list.Back()) + c.list.Remove(lastNode) } - newValue := &pair{ + c.store[key] = c.list.PushFront(&pair{ key: key, value: value, - } - c.store.Set(key, c.list.PushFront(newValue)) + }) } diff --git a/cache_test.go b/cache_test.go index 3f29ea5..c5d0d50 100644 --- a/cache_test.go +++ b/cache_test.go @@ -1,32 +1,35 @@ package gearbox -import "fmt" +import ( + "fmt" +) -// ExampleCache tests Cache set and get methods -func ExampleCache() { - cache := newCache(3) - cache.Set([]byte("user1"), 1) - fmt.Println(cache.Get([]byte("user1")).(int)) +// ExampleNewCache tests Cache set and get methods +func ExampleNewCache() { + cache := NewCache(3) + cache.Set("user1", 1) + fmt.Println(cache.Get("user1").(int)) - cache.Set([]byte("user2"), 2) - fmt.Println(cache.Get([]byte("user2")).(int)) + cache.Set("user2", 2) + fmt.Println(cache.Get("user2").(int)) - cache.Set([]byte("user3"), 3) - fmt.Println(cache.Get([]byte("user3")).(int)) + cache.Set("user3", 3) + fmt.Println(cache.Get("user3").(int)) - cache.Set([]byte("user4"), 4) - fmt.Println(cache.Get([]byte("user1"))) - fmt.Println(cache.Get([]byte("user2")).(int)) + cache.Set("user4", 4) + fmt.Println(cache.Get("user1")) + fmt.Println(cache.Get("user2").(int)) - cache.Set([]byte("user5"), 5) - fmt.Println(cache.Get([]byte("user3"))) + cache.Set("user5", 5) + fmt.Println(cache.Get("user3")) - cache.Set([]byte("user5"), 6) - fmt.Println(cache.Get([]byte("user5")).(int)) + cache.Set("user5", 6) + fmt.Println(cache.Get("user5").(int)) + + cache2 := NewCache(0) + cache2.Set("user1", 1) + fmt.Println(cache2.Get("user1").(int)) - cache2 := newCache(0) - cache2.Set([]byte("user1"), 1) - fmt.Println(cache2.Get([]byte("user1")).(int)) // Output: // 1 // 2 diff --git a/context.go b/context.go index 8ea6510..dcb6ecc 100644 --- a/context.go +++ b/context.go @@ -13,7 +13,7 @@ type handlersChain []handlerFunc // Context defines the current context of request and handlers/middlewares to execute type Context struct { RequestCtx *fasthttp.RequestCtx - Params tst + Params map[string]string handlers handlersChain index int } diff --git a/gearbox.go b/gearbox.go index dc3f27b..2f06899 100644 --- a/gearbox.go +++ b/gearbox.go @@ -12,7 +12,7 @@ import ( // Exported constants const ( - Version = "1.0.1" // Version of gearbox + Version = "1.0.2" // Version of gearbox Name = "Gearbox" // Name of gearbox // http://patorjk.com/software/taag/#p=display&f=Big%20Money-ne&t=Gearbox banner = ` @@ -136,7 +136,7 @@ type Gearbox interface { Method(method, path string, handlers ...handlerFunc) *Route Fallback(handlers ...handlerFunc) error Use(middlewares ...handlerFunc) - Group(path string, routes []*Route) []*Route + Group(prefix string, routes []*Route) []*Route } // gearbox implements Gearbox interface @@ -147,14 +147,14 @@ type gearbox struct { address string // server address handlers handlersChain registeredFallback *routerFallback - cache cache + cache Cache settings *Settings } // Settings struct holds server settings type Settings struct { // Enable case sensitive routing - CaseSensitive bool // default false + CaseInSensitive bool // default false // Maximum size of LRU cache that will be used in routing if it's enabled CacheSize int // default 1000 @@ -198,8 +198,8 @@ type Settings struct { // Route struct which holds each route info type Route struct { - Method []byte - Path []byte + Method string + Path string Handlers handlersChain } @@ -239,7 +239,7 @@ func (gb *gearbox) Start(address string) error { return fmt.Errorf("unable to construct routing %s", err.Error()) } - gb.cache = newCache(gb.settings.CacheSize) + gb.cache = NewCache(gb.settings.CacheSize) ln, err := net.Listen("tcp4", address) if err != nil { @@ -294,52 +294,52 @@ func (gb *gearbox) Stop() error { // Get registers an http relevant method func (gb *gearbox) Get(path string, handlers ...handlerFunc) *Route { - return gb.registerRoute([]byte(MethodGet), []byte(path), handlers) + return gb.registerRoute(string(MethodGet), string(path), handlers) } // Head registers an http relevant method func (gb *gearbox) Head(path string, handlers ...handlerFunc) *Route { - return gb.registerRoute([]byte(MethodHead), []byte(path), handlers) + return gb.registerRoute(string(MethodHead), string(path), handlers) } // Post registers an http relevant method func (gb *gearbox) Post(path string, handlers ...handlerFunc) *Route { - return gb.registerRoute([]byte(MethodPost), []byte(path), handlers) + return gb.registerRoute(string(MethodPost), string(path), handlers) } // Put registers an http relevant method func (gb *gearbox) Put(path string, handlers ...handlerFunc) *Route { - return gb.registerRoute([]byte(MethodPut), []byte(path), handlers) + return gb.registerRoute(string(MethodPut), string(path), handlers) } // Patch registers an http relevant method func (gb *gearbox) Patch(path string, handlers ...handlerFunc) *Route { - return gb.registerRoute([]byte(MethodPatch), []byte(path), handlers) + return gb.registerRoute(string(MethodPatch), string(path), handlers) } // Delete registers an http relevant method func (gb *gearbox) Delete(path string, handlers ...handlerFunc) *Route { - return gb.registerRoute([]byte(MethodDelete), []byte(path), handlers) + return gb.registerRoute(string(MethodDelete), string(path), handlers) } // Connect registers an http relevant method func (gb *gearbox) Connect(path string, handlers ...handlerFunc) *Route { - return gb.registerRoute([]byte(MethodConnect), []byte(path), handlers) + return gb.registerRoute(string(MethodConnect), string(path), handlers) } // Options registers an http relevant method func (gb *gearbox) Options(path string, handlers ...handlerFunc) *Route { - return gb.registerRoute([]byte(MethodOptions), []byte(path), handlers) + return gb.registerRoute(string(MethodOptions), string(path), handlers) } // Trace registers an http relevant method func (gb *gearbox) Trace(path string, handlers ...handlerFunc) *Route { - return gb.registerRoute([]byte(MethodTrace), []byte(path), handlers) + return gb.registerRoute(string(MethodTrace), string(path), handlers) } // Trace registers an http relevant method func (gb *gearbox) Method(method, path string, handlers ...handlerFunc) *Route { - return gb.registerRoute([]byte(method), []byte(path), handlers) + return gb.registerRoute(string(method), string(path), handlers) } // Fallback registers an http handler only fired when no other routes match with request @@ -355,10 +355,20 @@ func (gb *gearbox) Use(middlewares ...handlerFunc) { gb.handlers = append(gb.handlers, middlewares...) } +// Group appends a prefix to registered routes. +func (gb *gearbox) Group(prefix string, routes []*Route) []*Route { + for _, route := range routes { + route.Path = prefix + route.Path + } + return routes +} + // Handles all incoming requests and route them to proper handler according to // method and path func (gb *gearbox) handler(ctx *fasthttp.RequestCtx) { - if handlers, params := gb.matchRoute(ctx.Request.Header.Method(), ctx.URI().Path()); handlers != nil { + if handlers, params := gb.matchRoute( + GetString(ctx.Request.Header.Method()), + GetString(ctx.URI().Path())); handlers != nil { context := Context{ RequestCtx: ctx, Params: params, diff --git a/gearbox_test.go b/gearbox_test.go index 375827d..d3c023d 100644 --- a/gearbox_test.go +++ b/gearbox_test.go @@ -38,7 +38,7 @@ func (c *fakeConn) Write(b []byte) (int, error) { // startGearbox constructs routing tree and creates server func startGearbox(gb *gearbox) { - gb.cache = newCache(defaultCacheSize) + gb.cache = NewCache(defaultCacheSize) gb.constructRoutingTree() gb.httpServer = &fasthttp.Server{ Handler: gb.handler, @@ -157,7 +157,7 @@ func TestMethods(t *testing.T) { // get instance of gearbox gb := setupGearbox(&Settings{ - CaseSensitive: true, + CaseInSensitive: true, }) // register routes according to method @@ -176,12 +176,13 @@ func TestMethods(t *testing.T) { body string }{ {method: MethodGet, path: "/articles/search", statusCode: StatusOK}, - {method: MethodPost, path: "/articles/search", statusCode: StatusNotFound}, + {method: MethodGet, path: "/articles/search", statusCode: StatusOK}, + {method: MethodGet, path: "/Articles/search", statusCode: StatusOK}, {method: MethodGet, path: "/articles/searching", statusCode: StatusNotFound}, {method: MethodHead, path: "/articles/test", statusCode: StatusOK}, {method: MethodPost, path: "/articles/204", statusCode: StatusOK}, {method: MethodPost, path: "/articles/205", statusCode: StatusUnauthorized}, - {method: MethodPost, path: "/Articles/205", statusCode: StatusNotFound}, + {method: MethodPost, path: "/Articles/205", statusCode: StatusUnauthorized}, {method: MethodPost, path: "/articles/206", statusCode: StatusNotFound}, {method: MethodGet, path: "/ping", statusCode: StatusOK, body: "pong"}, {method: MethodPut, path: "/posts", statusCode: StatusOK}, diff --git a/go.mod b/go.mod index 09523c4..47e6385 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module github.com/gogearbox/gearbox go 1.14 -require github.com/valyala/fasthttp v1.14.0 +require github.com/valyala/fasthttp v1.14.0 \ No newline at end of file diff --git a/router.go b/router.go index 12412c2..b7bf28e 100644 --- a/router.go +++ b/router.go @@ -1,17 +1,17 @@ package gearbox import ( - "bytes" "fmt" "regexp" "sort" + "strings" "sync" ) type routeNode struct { - Name []byte - Endpoints tst - Children tst + Name string + Endpoints map[string][]*endpoint + Children map[string]*routeNode } type paramType uint8 @@ -25,7 +25,7 @@ const ( ) type param struct { - Name []byte + Name string Value string Type paramType IsOptional bool @@ -43,11 +43,11 @@ type routerFallback struct { type matchParamsResult struct { Matched bool Handlers handlersChain - Params tst + Params map[string]string } // validateRoutePath makes sure that path complies with path's rules -func validateRoutePath(path []byte) error { +func validateRoutePath(path string) error { // Check length of the path length := len(path) if length == 0 { @@ -59,11 +59,11 @@ func validateRoutePath(path []byte) error { return fmt.Errorf("path must start with /") } - params := newTST() - parts := bytes.Split(trimPath(path), []byte("/")) + params := make(map[string]bool) + parts := strings.Split(trimPath(path), "/") partsLen := len(parts) for i := 0; i < partsLen; i++ { - if len(parts[i]) == 0 { + if parts[i] == "" { continue } if p := parseParameter(parts[i]); p != nil { @@ -72,10 +72,10 @@ func validateRoutePath(path []byte) error { } else if p.IsOptional && i != partsLen-1 { return fmt.Errorf("only last parameter can be optional") } else if p.Type == ptParam || p.Type == ptRegexp { - if pName := params.Get(p.Name); pName != nil { + if _, ok := params[p.Name]; ok { return fmt.Errorf("parameter is duplicated") } - params.Set(p.Name, true) + params[p.Name] = true } } } @@ -84,10 +84,10 @@ func validateRoutePath(path []byte) error { } // registerRoute registers handler with method and path -func (gb *gearbox) registerRoute(method, path []byte, handlers handlersChain) *Route { +func (gb *gearbox) registerRoute(method, path string, handlers handlersChain) *Route { - if !gb.settings.CaseSensitive { - path = bytes.ToLower(path) + if gb.settings.CaseInSensitive { + path = strings.ToLower(path) } route := &Route{ @@ -101,13 +101,6 @@ func (gb *gearbox) registerRoute(method, path []byte, handlers handlersChain) *R return route } -func (gb *gearbox) Group(path string, routes []*Route) []*Route { - for _, route := range routes { - route.Path = append([]byte(path), route.Path...) - } - return routes -} - // registerFallback registers a single handler that will match only if all other routes fail to match func (gb *gearbox) registerFallback(handlers handlersChain) error { // Handler is not provided @@ -120,17 +113,17 @@ func (gb *gearbox) registerFallback(handlers handlersChain) error { } // createEmptyRouteNode creates a new route node with name -func createEmptyRouteNode(name []byte) *routeNode { +func createEmptyRouteNode(name string) *routeNode { return &routeNode{ Name: name, - Children: newTST(), - Endpoints: newTST(), + Children: make(map[string]*routeNode), + Endpoints: make(map[string][]*endpoint), } } // parseParameter parses path part into param struct, or returns nil if it's // not a parameter -func parseParameter(pathPart []byte) *param { +func parseParameter(pathPart string) *param { pathPartLen := len(pathPart) if pathPartLen == 0 { return nil @@ -139,7 +132,7 @@ func parseParameter(pathPart []byte) *param { // match all if pathPart[0] == '*' { return ¶m{ - Name: []byte("*"), + Name: "*", Type: ptMatchAll, } } @@ -149,16 +142,16 @@ func parseParameter(pathPart []byte) *param { pathPart = pathPart[0 : pathPartLen-1] } - params := bytes.Split(pathPart, []byte(":")) + params := strings.Split(pathPart, ":") paramsLen := len(params) - if paramsLen == 2 && len(params[0]) == 0 { // Just a parameter + if paramsLen == 2 && params[0] == "" { // Just a parameter return ¶m{ Name: params[1], Type: ptParam, IsOptional: isOptional, } - } else if paramsLen == 3 && len(params[0]) == 0 { // Regex parameter + } else if paramsLen == 3 && params[0] == "" { // Regex parameter return ¶m{ Name: params[1], Value: string(params[2]), @@ -203,7 +196,7 @@ func isValidEndpoint(endpoints []*endpoint, newEndpoint *endpoint) bool { } // trimPath trims left and right slashes in path -func trimPath(path []byte) []byte { +func trimPath(path string) string { pathLastIndex := len(path) - 1 for path[pathLastIndex] == '/' && pathLastIndex > 0 { @@ -221,7 +214,7 @@ func trimPath(path []byte) []byte { // constructRoutingTree constructs routing tree according to provided routes func (gb *gearbox) constructRoutingTree() error { // Firstly, create root node - gb.routingTreeRoot = createEmptyRouteNode([]byte("root")) + gb.routingTreeRoot = createEmptyRouteNode("root") for _, route := range gb.registeredRoutes { currentNode := gb.routingTreeRoot @@ -239,14 +232,14 @@ func (gb *gearbox) constructRoutingTree() error { params := make([]*param, 0) // Split path into slices of parts - parts := bytes.Split(route.Path, []byte("/")) + parts := strings.Split(route.Path, "/") partsLen := len(parts) for i := 1; i < partsLen; i++ { part := parts[i] // Do not create node if part is empty - if len(part) == 0 { + if part == "" { continue } @@ -258,10 +251,10 @@ func (gb *gearbox) constructRoutingTree() error { // Try to get a child of current node with part, otherwise //creates a new node and make it current node - partNode, ok := currentNode.Children.Get(part).(*routeNode) + partNode, ok := currentNode.Children[string(part)] if !ok { partNode = createEmptyRouteNode(part) - currentNode.Children.Set(part, partNode) + currentNode.Children[string(part)] = partNode } currentNode = partNode } @@ -273,7 +266,7 @@ func (gb *gearbox) constructRoutingTree() error { // Make sure that current node does not have a handler for route's method var endpoints []*endpoint - if result, ok := currentNode.Endpoints.Get(route.Method).([]*endpoint); ok { + if result, ok := currentNode.Endpoints[string(route.Method)]; ok { if ok := isValidEndpoint(result, currentEndpoint); !ok { return fmt.Errorf("there already registered method %s for %s", route.Method, route.Path) } @@ -295,30 +288,29 @@ func (gb *gearbox) constructRoutingTree() error { } // Save handler to route's method for current node - currentNode.Endpoints.Set(route.Method, endpoints) + currentNode.Endpoints[string(route.Method)] = endpoints } return nil } // matchRoute matches provided method and path with handler if it's existing -func (gb *gearbox) matchRoute(method, path []byte) (handlersChain, tst) { +func (gb *gearbox) matchRoute(method, path string) (handlersChain, map[string]string) { if handlers, params := gb.matchRouteAgainstRegistered(method, path); handlers != nil { return handlers, params } if gb.registeredFallback != nil && gb.registeredFallback.Handlers != nil { - tst := newTST() - return gb.registeredFallback.Handlers, tst + return gb.registeredFallback.Handlers, make(map[string]string) } return nil, nil } -func matchEndpointParams(ep *endpoint, paths [][]byte, pathIndex int) (tst, bool) { - paramDic := newTST() +func matchEndpointParams(ep *endpoint, paths []string, pathIndex int) (map[string]string, bool) { endpointParams := ep.Params endpointParamsLen := len(endpointParams) pathsLen := len(paths) + paramDic := make(map[string]string, endpointParamsLen) paramIdx := 0 for paramIdx < endpointParamsLen { @@ -337,16 +329,16 @@ func matchEndpointParams(ep *endpoint, paths [][]byte, pathIndex int) (tst, bool return nil, false } - if len(paths[pathIndex]) == 0 { + if paths[pathIndex] == "" { pathIndex++ continue } if endpointParams[paramIdx].Type == ptParam { - paramDic.Set(endpointParams[paramIdx].Name, paths[pathIndex]) + paramDic[string(endpointParams[paramIdx].Name)] = string(paths[pathIndex]) } else if endpointParams[paramIdx].Type == ptRegexp { - if match, _ := regexp.Match(endpointParams[paramIdx].Value, paths[pathIndex]); match { - paramDic.Set(endpointParams[paramIdx].Name, paths[pathIndex]) + if match, _ := regexp.MatchString(endpointParams[paramIdx].Value, paths[pathIndex]); match { + paramDic[string(endpointParams[paramIdx].Name)] = string(paths[pathIndex]) } else if !endpointParams[paramIdx].IsOptional { return nil, false } @@ -356,7 +348,7 @@ func matchEndpointParams(ep *endpoint, paths [][]byte, pathIndex int) (tst, bool pathIndex++ } - for pathIndex < pathsLen && len(paths[pathIndex]) == 0 { + for pathIndex < pathsLen && paths[pathIndex] == "" { pathIndex++ } @@ -368,9 +360,9 @@ func matchEndpointParams(ep *endpoint, paths [][]byte, pathIndex int) (tst, bool return paramDic, true } -func matchNodeEndpoints(node *routeNode, method []byte, paths [][]byte, +func matchNodeEndpoints(node *routeNode, method string, paths []string, pathIndex int, result *matchParamsResult, wg *sync.WaitGroup) { - if endpoints, ok := node.Endpoints.Get(method).([]*endpoint); ok { + if endpoints, ok := node.Endpoints[string(method)]; ok { for j := range endpoints { if params, matched := matchEndpointParams(endpoints[j], paths, pathIndex); matched { result.Matched = true @@ -386,31 +378,31 @@ func matchNodeEndpoints(node *routeNode, method []byte, paths [][]byte, wg.Done() } -func (gb *gearbox) matchRouteAgainstRegistered(method, path []byte) (handlersChain, tst) { +func (gb *gearbox) matchRouteAgainstRegistered(method, path string) (handlersChain, map[string]string) { // Start with root node currentNode := gb.routingTreeRoot // Return if root is empty, or path is not valid - if currentNode == nil || len(path) == 0 || path[0] != '/' { + if currentNode == nil || path == "" || path[0] != '/' { return nil, nil } - if !gb.settings.CaseSensitive { - path = bytes.ToLower(path) + if gb.settings.CaseInSensitive { + path = strings.ToLower(path) } trimmedPath := trimPath(path) // Try to get from cache if it's enabled - cacheKey := make([]byte, 0) + cacheKey := "" if !gb.settings.DisableCaching { - cacheKey = append(method, trimmedPath...) + cacheKey = method + trimmedPath if cacheResult, ok := gb.cache.Get(cacheKey).(*matchParamsResult); ok { return cacheResult.Handlers, cacheResult.Params } } - paths := bytes.Split(trimmedPath, []byte("/")) + paths := strings.Split(trimmedPath, "/") var wg sync.WaitGroup lastMatchedNodes := []*matchParamsResult{{}} @@ -419,12 +411,12 @@ func (gb *gearbox) matchRouteAgainstRegistered(method, path []byte) (handlersCha go matchNodeEndpoints(currentNode, method, paths, 0, lastMatchedNodes[0], &wg) for i := range paths { - if len(paths[i]) == 0 { + if paths[i] == "" { continue } // Try to match part with a child of current node - pathNode, ok := currentNode.Children.Get(paths[i]).(*routeNode) + pathNode, ok := currentNode.Children[string(paths[i])] if !ok { break } @@ -445,9 +437,9 @@ func (gb *gearbox) matchRouteAgainstRegistered(method, path []byte) (handlersCha for i := lastMatchedNodesIndex - 1; i >= 0; i-- { if lastMatchedNodes[i].Matched { if !gb.settings.DisableCaching { - go func(key []byte, matchResult *matchParamsResult) { + go func(key string, matchResult *matchParamsResult) { gb.cache.Set(key, matchResult) - }(append(make([]byte, 0, len(cacheKey)), cacheKey...), lastMatchedNodes[i]) + }(string(cacheKey), lastMatchedNodes[i]) } return lastMatchedNodes[i].Handlers, lastMatchedNodes[i].Params diff --git a/router_test.go b/router_test.go index 18d39cf..66f8673 100644 --- a/router_test.go +++ b/router_test.go @@ -1,6 +1,7 @@ package gearbox import ( + "fmt" "testing" ) @@ -15,7 +16,7 @@ func setupGearbox(settings ...*Settings) *gearbox { gb.settings = &Settings{} } - gb.cache = newCache(defaultCacheSize) + gb.cache = NewCache(defaultCacheSize) return gb } @@ -23,18 +24,18 @@ func setupGearbox(settings ...*Settings) *gearbox { func TestValidateRoutePath(t *testing.T) { // test cases tests := []struct { - input []byte + input string isErr bool }{ - {input: []byte(""), isErr: true}, - {input: []byte("user"), isErr: true}, - {input: []byte("/user"), isErr: false}, - {input: []byte("/admin/"), isErr: false}, - {input: []byte("/user/*/get"), isErr: true}, - {input: []byte("/user/*"), isErr: false}, - {input: []byte("/user/:name"), isErr: false}, - {input: []byte("/user/:name/:name"), isErr: true}, - {input: []byte("/user/:name?/get"), isErr: true}, + {input: "", isErr: true}, + {input: "user", isErr: true}, + {input: "/user", isErr: false}, + {input: "/admin/", isErr: false}, + {input: "/user/*/get", isErr: true}, + {input: "/user/*", isErr: false}, + {input: "/user/:name", isErr: false}, + {input: "/user/:name/:name", isErr: true}, + {input: "/user/:name?/get", isErr: true}, } for _, tt := range tests { @@ -54,7 +55,7 @@ func TestValidateRoutePath(t *testing.T) { // TestCreateEmptyNode tests creating route node with specific name func TestCreateEmptyNode(t *testing.T) { - name := []byte("test_node") + name := "test_node" node := createEmptyRouteNode(name) if node == nil { @@ -73,21 +74,21 @@ var emptyHandlersChain = handlersChain{} func TestRegisterRoute(t *testing.T) { // test cases tests := []struct { - method []byte - path []byte + method string + path string handler handlersChain isErr bool }{ - {method: []byte(MethodPut), path: []byte("/admin/welcome"), handler: emptyHandlersChain, isErr: false}, - {method: []byte(MethodPost), path: []byte("/user/add"), handler: emptyHandlersChain, isErr: false}, - {method: []byte(MethodGet), path: []byte("/account/get"), handler: emptyHandlersChain, isErr: false}, - {method: []byte(MethodGet), path: []byte("/account/*"), handler: emptyHandlersChain, isErr: false}, - {method: []byte(MethodGet), path: []byte("/account/*"), handler: emptyHandlersChain, isErr: false}, - {method: []byte(MethodDelete), path: []byte("/account/delete"), handler: emptyHandlersChain, isErr: false}, - {method: []byte(MethodDelete), path: []byte("/account/delete"), handler: nil, isErr: true}, - {method: []byte(MethodGet), path: []byte("/account/*/getAccount"), handler: nil, isErr: true}, - {method: []byte(MethodGet), path: []byte("/books/:name/:test"), handler: emptyHandlersChain, isErr: false}, - {method: []byte(MethodGet), path: []byte("/books/:name/:name"), handler: nil, isErr: true}, + {method: MethodPut, path: "/admin/welcome", handler: emptyHandlersChain, isErr: false}, + {method: MethodPost, path: "/user/add", handler: emptyHandlersChain, isErr: false}, + {method: MethodGet, path: "/account/get", handler: emptyHandlersChain, isErr: false}, + {method: MethodGet, path: "/account/*", handler: emptyHandlersChain, isErr: false}, + {method: MethodGet, path: "/account/*", handler: emptyHandlersChain, isErr: false}, + {method: MethodDelete, path: "/account/delete", handler: emptyHandlersChain, isErr: false}, + {method: MethodDelete, path: "/account/delete", handler: nil, isErr: true}, + {method: MethodGet, path: "/account/*/getAccount", handler: nil, isErr: true}, + {method: MethodGet, path: "/books/:name/:test", handler: emptyHandlersChain, isErr: false}, + {method: MethodGet, path: "/books/:name/:name", handler: nil, isErr: true}, } // counter for valid routes @@ -122,7 +123,7 @@ func TestRegisterInvalidRoute(t *testing.T) { gb := setupGearbox() // test handler is nil - gb.registerRoute([]byte(MethodGet), []byte("invalid Path"), emptyHandlersChain) + gb.registerRoute(MethodGet, "invalid Path", emptyHandlersChain) if err := gb.constructRoutingTree(); err == nil { t.Errorf("input GET invalid Path find nil expecting error") @@ -132,15 +133,15 @@ func TestRegisterInvalidRoute(t *testing.T) { // TestParseParameter tests parsing parameters into param struct func TestParseParameter(t *testing.T) { tests := []struct { - path []byte + path string output *param }{ - {path: []byte(":test"), output: ¶m{Name: []byte("test"), Type: ptParam}}, - {path: []byte(":test2:[a-z]"), output: ¶m{Name: []byte("test2"), Value: "[a-z]", Type: ptRegexp}}, - {path: []byte("*"), output: ¶m{Name: []byte("*"), Type: ptMatchAll}}, - {path: []byte("user:[a-z]"), output: nil}, - {path: []byte("user"), output: nil}, - {path: []byte(""), output: nil}, + {path: ":test", output: ¶m{Name: "test", Type: ptParam}}, + {path: ":test2:[a-z]", output: ¶m{Name: "test2", Value: "[a-z]", Type: ptRegexp}}, + {path: "*", output: ¶m{Name: "*", Type: ptMatchAll}}, + {path: "user:[a-z]", output: nil}, + {path: "user", output: nil}, + {path: "", output: nil}, } for _, test := range tests { @@ -150,7 +151,7 @@ func TestParseParameter(t *testing.T) { } if (test.output == nil && p != nil) || (test.output != nil && p == nil) || - (string(test.output.Name) != string(p.Name)) || + (test.output.Name != p.Name) || (test.output.Type != p.Type) || (test.output.Value != p.Value) { t.Errorf("path %s, find %v expected %v", test.path, p, test.output) @@ -166,21 +167,21 @@ func TestGetLeastStrictParamType(t *testing.T) { }{ {params: []*param{}, output: ptNoParam}, {params: []*param{ - {Type: ptParam, Name: []byte("name")}, - {Type: ptRegexp, Name: []byte("test"), Value: "[a-z]"}, - {Type: ptMatchAll, Name: []byte("*")}, + {Type: ptParam, Name: "name"}, + {Type: ptRegexp, Name: "test", Value: "[a-z]"}, + {Type: ptMatchAll, Name: "*"}, }, output: ptMatchAll}, {params: []*param{ - {Type: ptParam, Name: []byte("name")}, - {Type: ptMatchAll, Name: []byte("*")}, + {Type: ptParam, Name: "name"}, + {Type: ptMatchAll, Name: "*"}, }, output: ptMatchAll}, {params: []*param{ - {Type: ptParam, Name: []byte("name")}, - {Type: ptRegexp, Name: []byte("test3"), Value: "[a-z]"}, + {Type: ptParam, Name: "name"}, + {Type: ptRegexp, Name: "test3", Value: "[a-z]"}, }, output: ptParam}, {params: []*param{ - {Type: ptRegexp, Name: []byte("test3"), Value: "[a-z]"}, - {Type: ptMatchAll, Name: []byte("*")}, + {Type: ptRegexp, Name: "test3", Value: "[a-z]"}, + {Type: ptMatchAll, Name: "*"}, }, output: ptMatchAll}, } @@ -195,20 +196,20 @@ func TestGetLeastStrictParamType(t *testing.T) { // TestTrimPath test func TestTrimPath(t *testing.T) { tests := []struct { - input []byte - output []byte + input string + output string }{ - {input: []byte("/"), output: []byte("")}, - {input: []byte("/test/"), output: []byte("test")}, - {input: []byte("test2/"), output: []byte("test2")}, - {input: []byte("test2"), output: []byte("test2")}, - {input: []byte("/user/test"), output: []byte("user/test")}, - {input: []byte("/books/get/test/"), output: []byte("books/get/test")}, + {input: "/", output: ""}, + {input: "/test/", output: "test"}, + {input: "test2/", output: "test2"}, + {input: "test2", output: "test2"}, + {input: "/user/test", output: "user/test"}, + {input: "/books/get/test/", output: "books/get/test"}, } for _, test := range tests { trimmedPath := trimPath(test.input) - if string(trimmedPath) != string(test.output) { + if trimmedPath != test.output { t.Errorf("path %s, find %s expected %s", test.input, trimmedPath, test.output) } } @@ -224,37 +225,37 @@ func TestIsValidEndpoint(t *testing.T) { {endpoints: []*endpoint{}, newEndpoint: &endpoint{}, output: true}, {endpoints: []*endpoint{ {Handlers: handlersChain{emptyHandler}, Params: []*param{ - {Name: []byte("user"), Type: ptParam}, - {Name: []byte("name"), Type: ptParam}, + {Name: "user", Type: ptParam}, + {Name: "name", Type: ptParam}, }}, {Handlers: handlersChain{emptyHandler}, Params: []*param{}}, }, newEndpoint: &endpoint{Handlers: handlersChain{emptyHandler}, Params: []*param{ - {Name: []byte("test"), Type: ptParam}, + {Name: "test", Type: ptParam}, }}, output: true}, {endpoints: []*endpoint{}, newEndpoint: &endpoint{}, output: true}, {endpoints: []*endpoint{ {Handlers: handlersChain{emptyHandler}, Params: []*param{ - {Name: []byte("user"), Type: ptParam}, + {Name: "user", Type: ptParam}, }}, {Handlers: handlersChain{emptyHandler}, Params: []*param{}}, }, newEndpoint: &endpoint{Handlers: handlersChain{emptyHandler}, Params: []*param{ - {Name: []byte("test"), Type: ptParam}, + {Name: "test", Type: ptParam}, }}, output: false}, {endpoints: []*endpoint{ {Handlers: handlersChain{emptyHandler}, Params: []*param{ - {Name: []byte("user"), Type: ptRegexp, Value: "[a-z]"}, + {Name: "user", Type: ptRegexp, Value: "[a-z]"}, }}, {Handlers: handlersChain{emptyHandler}, Params: []*param{}}, }, newEndpoint: &endpoint{Handlers: handlersChain{emptyHandler}, Params: []*param{ - {Name: []byte("test"), Type: ptParam}, + {Name: "test", Type: ptParam}, }}, output: true}, {endpoints: []*endpoint{ {Handlers: handlersChain{emptyHandler}, Params: []*param{ - {Name: []byte("*"), Type: ptMatchAll}, + {Name: "*", Type: ptMatchAll}, }}, {Handlers: handlersChain{emptyHandler}, Params: []*param{}}, }, newEndpoint: &endpoint{Handlers: handlersChain{emptyHandler}, Params: []*param{ - {Name: []byte("test"), Type: ptRegexp, Value: "[a-z]"}, + {Name: "test", Type: ptRegexp, Value: "[a-z]"}, }}, output: true}, } @@ -276,30 +277,30 @@ func TestConstructRoutingTree(t *testing.T) { // testing routes routes := []struct { - method []byte - path []byte + method string + path string handler handlersChain }{ - {method: []byte(MethodGet), path: []byte("/articles/search"), handler: emptyHandlersChain}, - {method: []byte(MethodGet), path: []byte("/articles/test"), handler: emptyHandlersChain}, - {method: []byte(MethodGet), path: []byte("/articles/204"), handler: emptyHandlersChain}, - {method: []byte(MethodGet), path: []byte("/posts"), handler: emptyHandlersChain}, - {method: []byte(MethodGet), path: []byte("/post/502"), handler: emptyHandlersChain}, - {method: []byte(MethodGet), path: []byte("/post/a23011a"), handler: emptyHandlersChain}, - {method: []byte(MethodGet), path: []byte("/user/204"), handler: emptyHandlersChain}, - {method: []byte(MethodGet), path: []byte("/user/205/"), handler: emptyHandlersChain}, - {method: []byte(MethodPost), path: []byte("/user/204/setting"), handler: emptyHandlersChain}, - {method: []byte(MethodGet), path: []byte("/users/*"), handler: emptyHandlersChain}, - {method: []byte(MethodGet), path: []byte("/books/get/:name"), handler: emptyHandlersChain}, - {method: []byte(MethodGet), path: []byte("/books/get/*"), handler: emptyHandlersChain}, - {method: []byte(MethodGet), path: []byte("/books/search/:pattern:([a-z]+)"), handler: emptyHandlersChain}, - {method: []byte(MethodGet), path: []byte("/books/search/:pattern"), handler: emptyHandlersChain}, - {method: []byte(MethodGet), path: []byte("/books/search/:pattern1/:pattern2/:pattern3"), handler: emptyHandlersChain}, - {method: []byte(MethodGet), path: []byte("/books//search/*"), handler: emptyHandlersChain}, - {method: []byte(MethodGet), path: []byte("/account/:name?"), handler: emptyHandlersChain}, - {method: []byte(MethodGet), path: []byte("/profile/:name:([a-z]+)?"), handler: emptyHandlersChain}, - {method: []byte(MethodGet), path: []byte("/order/:name1/:name2:([a-z]+)?"), handler: emptyHandlersChain}, - {method: []byte(MethodGet), path: []byte("/"), handler: emptyHandlersChain}, + {method: MethodGet, path: "/articles/search", handler: emptyHandlersChain}, + {method: MethodGet, path: "/articles/test", handler: emptyHandlersChain}, + {method: MethodGet, path: "/articles/204", handler: emptyHandlersChain}, + {method: MethodGet, path: "/posts", handler: emptyHandlersChain}, + {method: MethodGet, path: "/post/502", handler: emptyHandlersChain}, + {method: MethodGet, path: "/post/a23011a", handler: emptyHandlersChain}, + {method: MethodGet, path: "/user/204", handler: emptyHandlersChain}, + {method: MethodGet, path: "/user/205/", handler: emptyHandlersChain}, + {method: MethodPost, path: "/user/204/setting", handler: emptyHandlersChain}, + {method: MethodGet, path: "/users/*", handler: emptyHandlersChain}, + {method: MethodGet, path: "/books/get/:name", handler: emptyHandlersChain}, + {method: MethodGet, path: "/books/get/*", handler: emptyHandlersChain}, + {method: MethodGet, path: "/books/search/:pattern:([a-z]+", handler: emptyHandlersChain}, + {method: MethodGet, path: "/books/search/:pattern", handler: emptyHandlersChain}, + {method: MethodGet, path: "/books/search/:pattern1/:pattern2/:pattern3", handler: emptyHandlersChain}, + {method: MethodGet, path: "/books//search/*", handler: emptyHandlersChain}, + {method: MethodGet, path: "/account/:name?", handler: emptyHandlersChain}, + {method: MethodGet, path: "/profile/:name:([a-z]+)?", handler: emptyHandlersChain}, + {method: MethodGet, path: "/order/:name1/:name2:([a-z]+)?", handler: emptyHandlersChain}, + {method: MethodGet, path: "/", handler: emptyHandlersChain}, } // register routes @@ -311,43 +312,43 @@ func TestConstructRoutingTree(t *testing.T) { // requests test cases requests := []struct { - method []byte - path []byte + method string + path string params map[string]string match bool }{ - {method: []byte(MethodPut), path: []byte("/admin/welcome"), match: false, params: make(map[string]string)}, - {method: []byte(MethodGet), path: []byte("/articles/search"), match: true, params: make(map[string]string)}, - {method: []byte(MethodGet), path: []byte("/articles/test"), match: true, params: make(map[string]string)}, - {method: []byte(MethodGet), path: []byte("/articles/test"), match: true, params: make(map[string]string)}, - {method: []byte(MethodGet), path: []byte("/articles/test"), match: true, params: make(map[string]string)}, - {method: []byte(MethodGet), path: []byte("/articles/204"), match: true, params: make(map[string]string)}, - {method: []byte(MethodGet), path: []byte("/posts"), match: true, params: make(map[string]string)}, - {method: []byte(MethodGet), path: []byte("/post/502"), match: true, params: make(map[string]string)}, - {method: []byte(MethodGet), path: []byte("/post/a23011a"), match: true, params: make(map[string]string)}, - {method: []byte(MethodPost), path: []byte("/post/a23011a"), match: false, params: make(map[string]string)}, - {method: []byte(MethodGet), path: []byte("/user/204"), match: true, params: make(map[string]string)}, - {method: []byte(MethodGet), path: []byte("/user/205"), match: true, params: make(map[string]string)}, - {method: []byte(MethodPost), path: []byte("/user/204/setting"), match: true, params: make(map[string]string)}, - {method: []byte(MethodGet), path: []byte("/users/ahmed"), match: true, params: make(map[string]string)}, - {method: []byte(MethodGet), path: []byte("/users/ahmed/ahmed"), match: true, params: make(map[string]string)}, - {method: []byte(MethodPut), path: []byte("/users/ahmed/ahmed"), match: false, params: make(map[string]string)}, - {method: []byte(MethodGet), path: []byte("/books/get/test"), match: true, params: map[string]string{"name": "test"}}, - {method: []byte(MethodGet), path: []byte("/books/search/test"), match: true, params: make(map[string]string)}, - {method: []byte(MethodGet), path: []byte("/books/search//test"), match: true, params: make(map[string]string)}, - {method: []byte(MethodGet), path: []byte("/books/search/123"), match: true, params: map[string]string{"pattern": "123"}}, - {method: []byte(MethodGet), path: []byte("/books/search/test1/test2/test3"), match: true, params: map[string]string{"pattern1": "test1", "pattern2": "test2", "pattern3": "test3"}}, - {method: []byte(MethodGet), path: []byte("/books/search/test/test2"), match: true, params: make(map[string]string)}, - {method: []byte(MethodGet), path: []byte("/books/search/test/test2"), match: true, params: make(map[string]string)}, - {method: []byte(MethodGet), path: []byte("/account/testuser"), match: true, params: map[string]string{"name": "testuser"}}, - {method: []byte(MethodGet), path: []byte("/account"), match: true, params: make(map[string]string)}, - {method: []byte(MethodPut), path: []byte("/account/test1/test2"), match: false, params: make(map[string]string)}, - {method: []byte(MethodGet), path: []byte("/profile/testuser"), match: true, params: map[string]string{"name": "testuser"}}, - {method: []byte(MethodGet), path: []byte("/profile"), match: true, params: make(map[string]string)}, - {method: []byte(MethodGet), path: []byte("/order/test1"), match: true, params: map[string]string{"name1": "test1"}}, - {method: []byte(MethodGet), path: []byte("/order/test1/test2/"), match: true, params: map[string]string{"name1": "test1", "name2": "test2"}}, - {method: []byte(MethodPut), path: []byte("/order/test1/test2/test3"), match: false, params: make(map[string]string)}, - {method: []byte(MethodGet), path: []byte("/"), match: true, params: make(map[string]string)}, + {method: MethodPut, path: "/admin/welcome", match: false, params: make(map[string]string)}, + {method: MethodGet, path: "/articles/search", match: true, params: make(map[string]string)}, + {method: MethodGet, path: "/articles/test", match: true, params: make(map[string]string)}, + {method: MethodGet, path: "/articles/test", match: true, params: make(map[string]string)}, + {method: MethodGet, path: "/articles/test", match: true, params: make(map[string]string)}, + {method: MethodGet, path: "/articles/204", match: true, params: make(map[string]string)}, + {method: MethodGet, path: "/posts", match: true, params: make(map[string]string)}, + {method: MethodGet, path: "/post/502", match: true, params: make(map[string]string)}, + {method: MethodGet, path: "/post/a23011a", match: true, params: make(map[string]string)}, + {method: MethodPost, path: "/post/a23011a", match: false, params: make(map[string]string)}, + {method: MethodGet, path: "/user/204", match: true, params: make(map[string]string)}, + {method: MethodGet, path: "/user/205", match: true, params: make(map[string]string)}, + {method: MethodPost, path: "/user/204/setting", match: true, params: make(map[string]string)}, + {method: MethodGet, path: "/users/ahmed", match: true, params: make(map[string]string)}, + {method: MethodGet, path: "/users/ahmed/ahmed", match: true, params: make(map[string]string)}, + {method: MethodPut, path: "/users/ahmed/ahmed", match: false, params: make(map[string]string)}, + {method: MethodGet, path: "/books/get/test", match: true, params: map[string]string{"name": "test"}}, + {method: MethodGet, path: "/books/search/test", match: true, params: make(map[string]string)}, + {method: MethodGet, path: "/books/search//test", match: true, params: make(map[string]string)}, + {method: MethodGet, path: "/books/search/123", match: true, params: map[string]string{"pattern": "123"}}, + {method: MethodGet, path: "/books/search/test1/test2/test3", match: true, params: map[string]string{"pattern1": "test1", "pattern2": "test2", "pattern3": "test3"}}, + {method: MethodGet, path: "/books/search/test/test2", match: true, params: make(map[string]string)}, + {method: MethodGet, path: "/books/search/test/test2", match: true, params: make(map[string]string)}, + {method: MethodGet, path: "/account/testuser", match: true, params: map[string]string{"name": "testuser"}}, + {method: MethodGet, path: "/account", match: true, params: make(map[string]string)}, + {method: MethodPut, path: "/account/test1/test2", match: false, params: make(map[string]string)}, + {method: MethodGet, path: "/profile/testuser", match: true, params: map[string]string{"name": "testuser"}}, + {method: MethodGet, path: "/profile", match: true, params: make(map[string]string)}, + {method: MethodGet, path: "/order/test1", match: true, params: map[string]string{"name1": "test1"}}, + {method: MethodGet, path: "/order/test1/test2/", match: true, params: map[string]string{"name1": "test1", "name2": "test2"}}, + {method: MethodPut, path: "/order/test1/test2/test3", match: false, params: make(map[string]string)}, + {method: MethodGet, path: "/", match: true, params: make(map[string]string)}, } // test matching routes @@ -357,10 +358,12 @@ func TestConstructRoutingTree(t *testing.T) { t.Errorf("input %s %s find nil expecting handler", rq.method, rq.path) } for paramKey, expectedParam := range rq.params { - actualParam, ok := params.GetString(paramKey).([]byte) - if !ok || string(actualParam) != expectedParam { + if actualParam, ok := params[paramKey]; !ok || actualParam != expectedParam { if !ok { - actualParam = []byte("nil") + actualParam = "nil" + } + for k, w := range params { + fmt.Println(k, string(w)) } t.Errorf("input %s %s parameter %s find %s expecting %s", @@ -376,10 +379,10 @@ func TestNullRoutingTree(t *testing.T) { gb := setupGearbox() // register route - gb.registerRoute([]byte(MethodGet), []byte("/*"), emptyHandlersChain) + gb.registerRoute(MethodGet, "/*", emptyHandlersChain) // test handler is nil - if handler, _ := gb.matchRoute([]byte(MethodGet), []byte("/hello/world")); handler != nil { + if handler, _ := gb.matchRoute(MethodGet, "/hello/world"); handler != nil { t.Errorf("input GET /hello/world find handler expecting nil") } } @@ -390,15 +393,15 @@ func TestMatchAll(t *testing.T) { gb := setupGearbox() // register route - gb.registerRoute([]byte(MethodGet), []byte("/*"), emptyHandlersChain) + gb.registerRoute(MethodGet, "/*", emptyHandlersChain) gb.constructRoutingTree() // test handler is not nil - if handler, _ := gb.matchRoute([]byte(MethodGet), []byte("/hello/world")); handler == nil { + if handler, _ := gb.matchRoute(MethodGet, "/hello/world"); handler == nil { t.Errorf("input GET /hello/world find nil expecting handler") } - if handler, _ := gb.matchRoute([]byte(MethodGet), []byte("//world")); handler == nil { + if handler, _ := gb.matchRoute(MethodGet, "//world"); handler == nil { t.Errorf("input GET //world find nil expecting handler") } } @@ -410,8 +413,8 @@ func TestConstructRoutingTreeConflict(t *testing.T) { gb := setupGearbox() // register routes - gb.registerRoute([]byte(MethodGet), []byte("/articles/test"), emptyHandlersChain) - gb.registerRoute([]byte(MethodGet), []byte("/articles/test"), emptyHandlersChain) + gb.registerRoute(MethodGet, "/articles/test", emptyHandlersChain) + gb.registerRoute(MethodGet, "/articles/test", emptyHandlersChain) if err := gb.constructRoutingTree(); err == nil { t.Fatalf("invalid listener passed") @@ -425,11 +428,11 @@ func TestNoRegisteredFallback(t *testing.T) { gb := setupGearbox() // register routes - gb.registerRoute([]byte(MethodGet), []byte("/articles"), emptyHandlersChain) + gb.registerRoute(MethodGet, "/articles", emptyHandlersChain) gb.constructRoutingTree() // attempt to match route that cannot match - if handler, _ := gb.matchRoute([]byte(MethodGet), []byte("/fail")); handler != nil { + if handler, _ := gb.matchRoute(MethodGet, "/fail"); handler != nil { t.Errorf("input GET /fail found a valid handler, expecting nil") } } @@ -441,14 +444,14 @@ func TestFallback(t *testing.T) { gb := setupGearbox() // register routes - gb.registerRoute([]byte(MethodGet), []byte("/articles"), emptyHandlersChain) + gb.registerRoute(MethodGet, "/articles", emptyHandlersChain) if err := gb.registerFallback(emptyHandlersChain); err != nil { t.Errorf("invalid fallback: %s", err.Error()) } gb.constructRoutingTree() // attempt to match route that cannot match - if handler, _ := gb.matchRoute([]byte(MethodGet), []byte("/fail")); handler == nil { + if handler, _ := gb.matchRoute(MethodGet, "/fail"); handler == nil { t.Errorf("input GET /fail did not find a valid handler, expecting valid fallback handler") } } diff --git a/tst.go b/tst.go deleted file mode 100644 index 99c24ff..0000000 --- a/tst.go +++ /dev/null @@ -1,91 +0,0 @@ -package gearbox - -// Basic Implementation for Ternary Search Tree (TST) - -// tst returns Ternary Search Tree -type tst interface { - Set(word []byte, value interface{}) - Get(word []byte) interface{} - GetString(word string) interface{} -} - -// Ternary Search Tree node that holds a single character and value if there is -type tstNode struct { - lower *tstNode - higher *tstNode - equal *tstNode - char byte - value interface{} -} - -// newTST returns Ternary Search Tree -func newTST() tst { - return &tstNode{} -} - -// Set adds a value to provided key -func (t *tstNode) Set(key []byte, value interface{}) { - if len(key) < 1 { - return - } - - t.insert(t, key, 0, value) -} - -// Get gets the value of provided key if it's existing, otherwise returns nil -func (t *tstNode) Get(key []byte) interface{} { - length := len(key) - if length < 1 || t == nil { - return nil - } - lastElm := length - 1 - - n := t - idx := 0 - char := key[idx] - for n != nil { - if char < n.char { - n = n.lower - } else if char > n.char { - n = n.higher - } else { - if idx == lastElm { - return n.value - } - - idx++ - n = n.equal - char = key[idx] - } - } - return nil -} - -// Get gets the value of provided key (string) if it's existing, otherwise returns nil -func (t *tstNode) GetString(key string) interface{} { - return t.Get([]byte(key)) -} - -// insert is an internal method for inserting a []byte with value in TST -func (t *tstNode) insert(n *tstNode, key []byte, index int, value interface{}) *tstNode { - char := key[index] - lastElm := len(key) - 1 - - if n == nil { - n = &tstNode{char: char} - } - - if char < n.char { - n.lower = t.insert(n.lower, key, index, value) - } else if char > n.char { - n.higher = t.insert(n.higher, key, index, value) - } else { - if index == lastElm { - n.value = value - } else { - n.equal = t.insert(n.equal, key, index+1, value) - } - } - - return n -} diff --git a/tst_test.go b/tst_test.go deleted file mode 100644 index 3d6e8fa..0000000 --- a/tst_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package gearbox - -import ( - "fmt" - "math/rand" - "testing" - "time" -) - -// ExampleTST tests TST set, get and remove methods -func ExampleTST() { - tst := newTST() - tst.Set([]byte("user"), 1) - fmt.Println(tst.Get([]byte("user")).(int)) - fmt.Println(tst.Get([]byte("us"))) - fmt.Println(tst.Get([]byte("user1"))) - fmt.Println(tst.Get([]byte("not-existing"))) - fmt.Println(tst.GetString(("not-existing"))) - - tst.Set([]byte("account"), 5) - tst.Set([]byte("account"), 6) - fmt.Println(tst.Get([]byte("account")).(int)) - - tst.Set([]byte("acc@unt"), 12) - fmt.Println(tst.Get([]byte("acc@unt")).(int)) - - tst.Set([]byte("حساب"), 15) - fmt.Println(tst.Get([]byte("حساب")).(int)) - tst.Set([]byte(""), 14) - fmt.Println(tst.Get([]byte(""))) - - // Output: - // 1 - // - // - // - // - // 6 - // 12 - // 15 - // -} - -// RandBytes generates random string from English letters -func RandBytes() []byte { - const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - b := make([]byte, rand.Intn(100)) - for i := range b { - b[i] = letterBytes[rand.Intn(len(letterBytes))] - } - return b -} - -func BenchmarkTSTLookup(b *testing.B) { - tst := newTST() - rand.Seed(time.Now().UnixNano()) - for n := 0; n < rand.Intn(2000); n++ { - tst.Set(RandBytes(), rand.Intn(10000)) - } - b.ResetTimer() - - for n := 0; n < b.N; n++ { - tst.Get([]byte("user")) - } -} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..0269196 --- /dev/null +++ b/utils.go @@ -0,0 +1,11 @@ +package gearbox + +import ( + "unsafe" +) + +// GetString gets the content of a string as a []byte without copying +// #nosec G103 +func GetString(b []byte) string { + return *(*string)(unsafe.Pointer(&b)) +} diff --git a/utils_test.go b/utils_test.go new file mode 100644 index 0000000..c28c357 --- /dev/null +++ b/utils_test.go @@ -0,0 +1,31 @@ +package gearbox + +import "fmt" + +// ExampleGetString tests converting []byte to string +func ExampleGetString() { + b := []byte("ABC€") + str := GetString(b) + fmt.Println(str) + fmt.Println(len(b) == len(str)) + + b = []byte("مستخدم") + str = GetString(b) + fmt.Println(str) + fmt.Println(len(b) == len(str)) + + b = nil + str = GetString(b) + fmt.Println(str) + fmt.Println(len(b) == len(str)) + fmt.Println(len(str)) + + // Output: + // ABC€ + // true + // مستخدم + // true + // + // true + // 0 +}