Reimplement generics

This commit is contained in:
Sky Johnson 2024-09-06 08:16:32 -05:00
parent 0dc5189e35
commit 34e8d28e95
6 changed files with 50 additions and 48 deletions

View File

@ -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

View File

@ -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

View File

@ -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, "")

View File

@ -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")

18
tree.go
View File

@ -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)
})
}

View File

@ -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 {