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 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***! 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 ## Installation
```shell ```shell

View File

@ -1,30 +1,30 @@
package router package router
type Router struct { type Router[T any] struct {
get Tree get Tree[T]
post Tree post Tree[T]
delete Tree delete Tree[T]
put Tree put Tree[T]
patch Tree patch Tree[T]
head Tree head Tree[T]
connect Tree connect Tree[T]
trace Tree trace Tree[T]
options Tree options Tree[T]
} }
// Create a new Router containing trees for every HTTP method. // Create a new Router containing trees for every HTTP method.
func New() *Router { func New[T any]() *Router[T] {
return &Router{} return &Router[T]{}
} }
// Registers a new handler for the given method and path. // 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 := router.selectTree(method)
tree.Add(path, handler) tree.Add(path, handler)
} }
// Finds the handler and parameters for the given route. // 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' { if method[0] == 'G' {
return router.get.Lookup(path) 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. // 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' { if method[0] == 'G' {
return router.get.LookupNoAlloc(path, addParameter) 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. // 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.get.Map(transform)
router.post.Map(transform) router.post.Map(transform)
router.delete.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. // 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 { switch method {
case "GET": case "GET":
return &router.get return &router.get

View File

@ -11,7 +11,7 @@ import (
func BenchmarkBlog(b *testing.B) { func BenchmarkBlog(b *testing.B) {
routes := routes("blog.txt") routes := routes("blog.txt")
r := router.New() r := router.New[string]()
for _, route := range routes { for _, route := range routes {
r.Add(route.Method, route.Path, "") r.Add(route.Method, route.Path, "")
@ -44,7 +44,7 @@ func BenchmarkBlog(b *testing.B) {
func BenchmarkGithub(b *testing.B) { func BenchmarkGithub(b *testing.B) {
routes := routes("github.txt") routes := routes("github.txt")
r := router.New() r := router.New[string]()
for _, route := range routes { for _, route := range routes {
r.Add(route.Method, route.Path, "") r.Add(route.Method, route.Path, "")

View File

@ -7,7 +7,7 @@ import (
) )
func TestHello(t *testing.T) { func TestHello(t *testing.T) {
r := router.New() r := router.New[string]()
r.Add("GET", "/blog", "Blog") r.Add("GET", "/blog", "Blog")
r.Add("GET", "/blog/post", "Blog post") r.Add("GET", "/blog/post", "Blog post")

18
tree.go
View File

@ -1,12 +1,12 @@
package router package router
// Super-fancy radix tree // Super-fancy radix tree
type Tree struct { type Tree[T any] struct {
root TreeNode root TreeNode[T]
} }
// Adds a new element to the tree // 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 // Search tree for equal parts until we can no longer proceed
i := 0 i := 0
offset := 0 offset := 0
@ -79,7 +79,7 @@ func (tree *Tree) Add(path string, data string) {
} }
// Lookup finds the data for the given path. // 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 var params []Parameter
data := tree.LookupNoAlloc(path, func(key string, value string) { 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. // 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 ( var (
i uint i uint
wildcardPath string wildcardPath string
wildcard *TreeNode wildcard *TreeNode[T]
node = &tree.root node = &tree.root
) )
@ -183,13 +183,13 @@ notFound:
return wildcard.data return wildcard.data
} }
var empty string var empty T
return empty return empty
} }
// Binds all handlers to a new one provided by the callback. // Binds all handlers to a new one provided by the callback.
func (tree *Tree) Map(transform func(string) string) { func (tree *Tree[T]) Map(transform func(T) T) {
tree.root.each(func(node *TreeNode) { tree.root.each(func(node *TreeNode[T]) {
node.data = transform(node.data) node.data = transform(node.data)
}) })
} }

View File

@ -10,12 +10,12 @@ const (
) )
// A node on our radix tree // A node on our radix tree
type TreeNode struct { type TreeNode[T any] struct {
prefix string prefix string
data string data T
children []*TreeNode children []*TreeNode[T]
parameter *TreeNode parameter *TreeNode[T]
wildcard *TreeNode wildcard *TreeNode[T]
indexes []uint8 indexes []uint8
start uint8 start uint8
end uint8 end uint8
@ -26,7 +26,7 @@ type TreeNode struct {
// node with the given path and data. If path is empty, it will // node with the given path and data. If path is empty, it will
// not create another child node and instead assign the data // not create another child node and instead assign the data
// directly to the node. // 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 // Create split node with the remaining string
splitNode := node.clone(node.prefix[index:]) 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 // Clone the node with a new prefix
func (node *TreeNode) clone(prefix string) *TreeNode { func (node *TreeNode[T]) clone(prefix string) *TreeNode[T] {
return &TreeNode{ return &TreeNode[T]{
prefix: prefix, prefix: prefix,
data: node.data, data: node.data,
children: node.children, children: node.children,
@ -63,8 +63,8 @@ func (node *TreeNode) clone(prefix string) *TreeNode {
} }
// Reset the node, set the prefix // Reset the node, set the prefix
func (node *TreeNode) reset(prefix string) { func (node *TreeNode[T]) reset(prefix string) {
var empty string var empty T
node.prefix = prefix node.prefix = prefix
node.data = empty node.data = empty
node.children = nil node.children = nil
@ -77,7 +77,7 @@ func (node *TreeNode) reset(prefix string) {
} }
// Append the given path to the tree // 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 // At this point, all we know is that somewhere
// in the remaining string we have parameters. // in the remaining string we have parameters.
// node: /user| // node: /user|
@ -106,7 +106,7 @@ func (node *TreeNode) append(path string, data string) {
return return
} }
child := &TreeNode{ child := &TreeNode[T]{
prefix: path, prefix: path,
data: data, data: data,
} }
@ -125,7 +125,7 @@ func (node *TreeNode) append(path string, data string) {
paramEnd = len(path) paramEnd = len(path)
} }
child := &TreeNode{ child := &TreeNode[T]{
prefix: path[1:paramEnd], prefix: path[1:paramEnd],
kind: path[paramStart], 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. // Add a normal node with the path before the parameter start.
child := &TreeNode{ child := &TreeNode[T]{
prefix: path[:paramStart], prefix: path[:paramStart],
} }
@ -173,7 +173,7 @@ func (node *TreeNode) append(path string, data string) {
} }
// Add a child tree node // Add a child tree node
func (node *TreeNode) addChild(child *TreeNode) { func (node *TreeNode[T]) addChild(child *TreeNode[T]) {
if len(node.children) == 0 { if len(node.children) == 0 {
node.children = append(node.children, nil) node.children = append(node.children, nil)
} }
@ -213,19 +213,19 @@ func (node *TreeNode) addChild(child *TreeNode) {
node.children[index] = child 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) { if strings.HasSuffix(node.prefix, "/") || node.kind == wildcard || (separator >= node.start && separator < node.end && node.indexes[separator-node.start] != 0) {
return return
} }
node.addChild(&TreeNode{ node.addChild(&TreeNode[T]{
prefix: "/", prefix: "/",
data: data, data: data,
}) })
} }
// Traverses the tree and calls the given function on every node. // 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) callback(node)
for _, child := range node.children { 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. // Called when the node was fully parsed and needs to decide the next control flow.
// finish is only called from `tree.Add`. // 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] char := path[i]
if char >= node.start && char < node.end { if char >= node.start && char < node.end {