diff --git a/README.md b/README.md index 0e1718d..8c07e7c 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ A radix-tree based no-allocation router in Go. All credit to Eduard Urbach for his incredible work. This router sports a fancy PATRICIA tree structure for efficient string lookups. It also has ***zero dependencies***! +The router has a generic data structure, so any kind of handler can be used. + ## Installation ```shell diff --git a/router.go b/router.go index d9bfa7d..839fade 100644 --- a/router.go +++ b/router.go @@ -1,30 +1,30 @@ package router -type Router struct { - get Tree - post Tree - delete Tree - put Tree - patch Tree - head Tree - connect Tree - trace Tree - options Tree +type Router[T any] struct { + get Tree[T] + post Tree[T] + delete Tree[T] + put Tree[T] + patch Tree[T] + head Tree[T] + connect Tree[T] + trace Tree[T] + options Tree[T] } // Create a new Router containing trees for every HTTP method. -func New() *Router { - return &Router{} +func New[T any]() *Router[T] { + return &Router[T]{} } // Registers a new handler for the given method and path. -func (router *Router) Add(method string, path string, handler string) { +func (router *Router[T]) Add(method string, path string, handler T) { tree := router.selectTree(method) tree.Add(path, handler) } // Finds the handler and parameters for the given route. -func (router *Router) Lookup(method string, path string) (string, []Parameter) { +func (router *Router[T]) Lookup(method string, path string) (T, []Parameter) { if method[0] == 'G' { return router.get.Lookup(path) } @@ -34,7 +34,7 @@ func (router *Router) Lookup(method string, path string) (string, []Parameter) { } // Finds the handler and parameters for the given route without using any memory allocations. -func (router *Router) LookupNoAlloc(method string, path string, addParameter func(string, string)) string { +func (router *Router[T]) LookupNoAlloc(method string, path string, addParameter func(string, string)) T { if method[0] == 'G' { return router.get.LookupNoAlloc(path, addParameter) } @@ -44,7 +44,7 @@ func (router *Router) LookupNoAlloc(method string, path string, addParameter fun } // Traverses all trees and calls the given function on every node. -func (router *Router) Map(transform func(string) string) { +func (router *Router[T]) Map(transform func(T) T) { router.get.Map(transform) router.post.Map(transform) router.delete.Map(transform) @@ -57,7 +57,7 @@ func (router *Router) Map(transform func(string) string) { } // Returns the tree of the given HTTP method. -func (router *Router) selectTree(method string) *Tree { +func (router *Router[T]) selectTree(method string) *Tree[T] { switch method { case "GET": return &router.get diff --git a/tests/benchmark_test.go b/tests/benchmark_test.go index c551fb4..fa22e7f 100644 --- a/tests/benchmark_test.go +++ b/tests/benchmark_test.go @@ -11,7 +11,7 @@ import ( func BenchmarkBlog(b *testing.B) { routes := routes("blog.txt") - r := router.New() + r := router.New[string]() for _, route := range routes { r.Add(route.Method, route.Path, "") @@ -44,7 +44,7 @@ func BenchmarkBlog(b *testing.B) { func BenchmarkGithub(b *testing.B) { routes := routes("github.txt") - r := router.New() + r := router.New[string]() for _, route := range routes { r.Add(route.Method, route.Path, "") diff --git a/tests/router_test.go b/tests/router_test.go index 7b8ecb1..8c5abdc 100644 --- a/tests/router_test.go +++ b/tests/router_test.go @@ -7,7 +7,7 @@ import ( ) func TestHello(t *testing.T) { - r := router.New() + r := router.New[string]() r.Add("GET", "/blog", "Blog") r.Add("GET", "/blog/post", "Blog post") diff --git a/tree.go b/tree.go index a8df8ff..bd1d438 100644 --- a/tree.go +++ b/tree.go @@ -1,12 +1,12 @@ package router // Super-fancy radix tree -type Tree struct { - root TreeNode +type Tree[T any] struct { + root TreeNode[T] } // Adds a new element to the tree -func (tree *Tree) Add(path string, data string) { +func (tree *Tree[T]) Add(path string, data T) { // Search tree for equal parts until we can no longer proceed i := 0 offset := 0 @@ -79,7 +79,7 @@ func (tree *Tree) Add(path string, data string) { } // Lookup finds the data for the given path. -func (tree *Tree) Lookup(path string) (string, []Parameter) { +func (tree *Tree[T]) Lookup(path string) (T, []Parameter) { var params []Parameter data := tree.LookupNoAlloc(path, func(key string, value string) { @@ -90,11 +90,11 @@ func (tree *Tree) Lookup(path string) (string, []Parameter) { } // Finds the data for the given path without using any memory allocations. -func (tree *Tree) LookupNoAlloc(path string, addParameter func(key string, value string)) string { +func (tree *Tree[T]) LookupNoAlloc(path string, addParameter func(key string, value string)) T { var ( i uint wildcardPath string - wildcard *TreeNode + wildcard *TreeNode[T] node = &tree.root ) @@ -183,13 +183,13 @@ notFound: return wildcard.data } - var empty string + var empty T return empty } // Binds all handlers to a new one provided by the callback. -func (tree *Tree) Map(transform func(string) string) { - tree.root.each(func(node *TreeNode) { +func (tree *Tree[T]) Map(transform func(T) T) { + tree.root.each(func(node *TreeNode[T]) { node.data = transform(node.data) }) } diff --git a/treeNode.go b/treeNode.go index 748be90..1eb269b 100644 --- a/treeNode.go +++ b/treeNode.go @@ -10,12 +10,12 @@ const ( ) // A node on our radix tree -type TreeNode struct { +type TreeNode[T any] struct { prefix string - data string - children []*TreeNode - parameter *TreeNode - wildcard *TreeNode + data T + children []*TreeNode[T] + parameter *TreeNode[T] + wildcard *TreeNode[T] indexes []uint8 start uint8 end uint8 @@ -26,7 +26,7 @@ type TreeNode struct { // node with the given path and data. If path is empty, it will // not create another child node and instead assign the data // directly to the node. -func (node *TreeNode) split(index int, path string, data string) { +func (node *TreeNode[T]) split(index int, path string, data T) { // Create split node with the remaining string splitNode := node.clone(node.prefix[index:]) @@ -48,8 +48,8 @@ func (node *TreeNode) split(index int, path string, data string) { } // Clone the node with a new prefix -func (node *TreeNode) clone(prefix string) *TreeNode { - return &TreeNode{ +func (node *TreeNode[T]) clone(prefix string) *TreeNode[T] { + return &TreeNode[T]{ prefix: prefix, data: node.data, children: node.children, @@ -63,8 +63,8 @@ func (node *TreeNode) clone(prefix string) *TreeNode { } // Reset the node, set the prefix -func (node *TreeNode) reset(prefix string) { - var empty string +func (node *TreeNode[T]) reset(prefix string) { + var empty T node.prefix = prefix node.data = empty node.children = nil @@ -77,7 +77,7 @@ func (node *TreeNode) reset(prefix string) { } // Append the given path to the tree -func (node *TreeNode) append(path string, data string) { +func (node *TreeNode[T]) append(path string, data T) { // At this point, all we know is that somewhere // in the remaining string we have parameters. // node: /user| @@ -106,7 +106,7 @@ func (node *TreeNode) append(path string, data string) { return } - child := &TreeNode{ + child := &TreeNode[T]{ prefix: path, data: data, } @@ -125,7 +125,7 @@ func (node *TreeNode) append(path string, data string) { paramEnd = len(path) } - child := &TreeNode{ + child := &TreeNode[T]{ prefix: path[1:paramEnd], kind: path[paramStart], } @@ -156,7 +156,7 @@ func (node *TreeNode) append(path string, data string) { } // Add a normal node with the path before the parameter start. - child := &TreeNode{ + child := &TreeNode[T]{ prefix: path[:paramStart], } @@ -173,7 +173,7 @@ func (node *TreeNode) append(path string, data string) { } // Add a child tree node -func (node *TreeNode) addChild(child *TreeNode) { +func (node *TreeNode[T]) addChild(child *TreeNode[T]) { if len(node.children) == 0 { node.children = append(node.children, nil) } @@ -213,19 +213,19 @@ func (node *TreeNode) addChild(child *TreeNode) { node.children[index] = child } -func (node *TreeNode) addTrailingSlash(data string) { +func (node *TreeNode[T]) addTrailingSlash(data T) { if strings.HasSuffix(node.prefix, "/") || node.kind == wildcard || (separator >= node.start && separator < node.end && node.indexes[separator-node.start] != 0) { return } - node.addChild(&TreeNode{ + node.addChild(&TreeNode[T]{ prefix: "/", data: data, }) } // Traverses the tree and calls the given function on every node. -func (node *TreeNode) each(callback func(*TreeNode)) { +func (node *TreeNode[T]) each(callback func(*TreeNode[T])) { callback(node) for _, child := range node.children { @@ -247,7 +247,7 @@ func (node *TreeNode) each(callback func(*TreeNode)) { // Called when the node was fully parsed and needs to decide the next control flow. // finish is only called from `tree.Add`. -func (node *TreeNode) finish(path string, data string, i int, offset int) (*TreeNode, int, Flow) { +func (node *TreeNode[T]) finish(path string, data T, i int, offset int) (*TreeNode[T], int, Flow) { char := path[i] if char >= node.start && char < node.end {