initial commit

This commit is contained in:
Sky Johnson 2024-08-22 21:46:21 -05:00
parent 6e7634052f
commit 048458e52e
6 changed files with 577 additions and 0 deletions

9
Flow.go Normal file
View File

@ -0,0 +1,9 @@
package router
type Flow int
const (
flowStop Flow = iota
flowBegin
flowNext
)

6
Parameter.go Normal file
View File

@ -0,0 +1,6 @@
package router
type Parameter struct {
Key string
Value string
}

84
Router.go Normal file
View File

@ -0,0 +1,84 @@
package router
// Router is a high-performance router.
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]
}
// New creates a new router containing trees for every HTTP method.
func New[T any]() *Router[T] {
return &Router[T]{}
}
// Add registers a new handler for the given method and path.
func (router *Router[T]) Add(method string, path string, handler T) {
tree := router.selectTree(method)
tree.Add(path, handler)
}
// Lookup finds the handler and parameters for the given route.
func (router *Router[T]) Lookup(method string, path string) (T, []Parameter) {
if method[0] == 'G' {
return router.get.Lookup(path)
}
tree := router.selectTree(method)
return tree.Lookup(path)
}
// LookupNoAlloc finds the handler and parameters for the given route without using any memory allocations.
func (router *Router[T]) LookupNoAlloc(method string, path string, addParameter func(string, string)) T {
if method[0] == 'G' {
return router.get.LookupNoAlloc(path, addParameter)
}
tree := router.selectTree(method)
return tree.LookupNoAlloc(path, addParameter)
}
// Map traverses all trees and calls the given function on every node.
func (router *Router[T]) Map(transform func(T) T) {
router.get.Map(transform)
router.post.Map(transform)
router.delete.Map(transform)
router.put.Map(transform)
router.patch.Map(transform)
router.head.Map(transform)
router.connect.Map(transform)
router.trace.Map(transform)
router.options.Map(transform)
}
// selectTree returns the tree by the given HTTP method.
func (router *Router[T]) selectTree(method string) *Tree[T] {
switch method {
case "GET":
return &router.get
case "POST":
return &router.post
case "DELETE":
return &router.delete
case "PUT":
return &router.put
case "PATCH":
return &router.patch
case "HEAD":
return &router.head
case "CONNECT":
return &router.connect
case "TRACE":
return &router.trace
case "OPTIONS":
return &router.options
default:
return nil
}
}

195
Tree.go Normal file
View File

@ -0,0 +1,195 @@
package router
// Super-fancy radix tree
type Tree[T any] struct {
root TreeNode[T]
}
// Adds a new element to the tree
func (tree *Tree[T]) Add(path string, data T) {
// Search tree for equal parts until we can no longer proceed
i := 0
offset := 0
node := &tree.root
for {
begin:
switch node.kind {
case parameter:
// This only occurs when the same parameter based route is added twice.
// node: /post/:id|
// path: /post/:id|
if i == len(path) {
node.data = data
return
}
// When we hit a separator, we'll search for a fitting child.
if path[i] == separator {
node, offset, _ = node.finish(path, data, i, offset)
goto next
}
default:
if i == len(path) {
// The path already exists.
// node: /blog|
// path: /blog|
if i-offset == len(node.prefix) {
node.data = data
return
}
// The path ended but the node prefix is longer.
// node: /blog|feed
// path: /blog|
node.split(i-offset, "", data)
return
}
// The node we just checked is entirely included in our path.
// node: /|
// path: /|blog
if i-offset == len(node.prefix) {
var control Flow
node, offset, control = node.finish(path, data, i, offset)
switch control {
case flowStop:
return
case flowBegin:
goto begin
case flowNext:
goto next
}
}
// We got a conflict.
// node: /b|ag
// path: /b|riefcase
if path[i] != node.prefix[i-offset] {
node.split(i-offset, path[i:], data)
return
}
}
next:
i++
}
}
// Lookup finds the data for the given path.
func (tree *Tree[T]) Lookup(path string) (T, []Parameter) {
var params []Parameter
data := tree.LookupNoAlloc(path, func(key string, value string) {
params = append(params, Parameter{key, value})
})
return data, params
}
// LookupNoAlloc finds the data for the given path without using any memory allocations.
func (tree *Tree[T]) LookupNoAlloc(path string, addParameter func(key string, value string)) T {
var (
i uint
wildcardPath string
wildcard *TreeNode[T]
node = &tree.root
)
// Skip the first loop iteration if the starting characters are equal
if len(path) > 0 && len(node.prefix) > 0 && path[0] == node.prefix[0] {
i = 1
}
begin:
// Search tree for equal parts until we can no longer proceed
for i < uint(len(path)) {
// The node we just checked is entirely included in our path.
// node: /|
// path: /|blog
if i == uint(len(node.prefix)) {
if node.wildcard != nil {
wildcard = node.wildcard
wildcardPath = path[i:]
}
char := path[i]
if char >= node.start && char < node.end {
index := node.indexes[char-node.start]
if index != 0 {
node = node.children[index]
path = path[i:]
i = 1
continue
}
}
// node: /|:id
// path: /|blog
if node.parameter != nil {
node = node.parameter
path = path[i:]
i = 1
for i < uint(len(path)) {
// node: /:id|/posts
// path: /123|/posts
if path[i] == separator {
addParameter(node.prefix, path[:i])
index := node.indexes[separator-node.start]
node = node.children[index]
path = path[i:]
i = 1
goto begin
}
i++
}
addParameter(node.prefix, path[:i])
return node.data
}
// node: /|*any
// path: /|image.png
goto notFound
}
// We got a conflict.
// node: /b|ag
// path: /b|riefcase
if path[i] != node.prefix[i] {
goto notFound
}
i++
}
// node: /blog|
// path: /blog|
if i == uint(len(node.prefix)) {
return node.data
}
// node: /|*any
// path: /|image.png
notFound:
if wildcard != nil {
addParameter(wildcard.prefix, wildcardPath)
return wildcard.data
}
var empty T
return empty
}
// Binds all handlers to a new one provided by the callback.
func (tree *Tree[T]) Map(transform func(T) T) {
tree.root.each(func(node *TreeNode[T]) {
node.data = transform(node.data)
})
}

280
TreeNode.go Normal file
View File

@ -0,0 +1,280 @@
package router
import "strings"
// Node types
const (
separator = '/'
parameter = ':'
wildcard = '*'
)
// A node on our radix tree
type TreeNode[T any] struct {
prefix string
data T
children []*TreeNode[T]
parameter *TreeNode[T]
wildcard *TreeNode[T]
indexes []uint8
start uint8
end uint8
kind byte
}
// Splits the node at the given index and inserts a new child
// 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[T]) split(index int, path string, data T) {
// Create split node with the remaining string
splitNode := node.clone(node.prefix[index:])
// The existing data must be removed
node.reset(node.prefix[:index])
// If the path is empty, it means we don't create a 2nd child node.
// Just assign the data for the existing node and store a single child node.
if path == "" {
node.data = data
node.addChild(splitNode)
return
}
node.addChild(splitNode)
// Create new nodes with the remaining path
node.append(path, data)
}
// Clone the node with a new prefix
func (node *TreeNode[T]) clone(prefix string) *TreeNode[T] {
return &TreeNode[T]{
prefix: prefix,
data: node.data,
children: node.children,
parameter: node.parameter,
wildcard: node.wildcard,
indexes: node.indexes,
start: node.start,
end: node.end,
kind: node.kind,
}
}
// Reset the node, set the prefix
func (node *TreeNode[T]) reset(prefix string) {
var empty T
node.prefix = prefix
node.data = empty
node.children = nil
node.parameter = nil
node.wildcard = nil
node.indexes = nil
node.start = 0
node.end = 0
node.kind = 0
}
// Append the given path to the tree
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|
// path: /user|/:userid
for {
if path == "" {
node.data = data
return
}
paramStart := strings.IndexByte(path, parameter)
if paramStart == -1 {
paramStart = strings.IndexByte(path, wildcard)
}
// If it's a static route we are adding,
// just add the remainder as a normal node.
if paramStart == -1 {
// If the node itself doesn't have a prefix (root node),
// don't add a child and use the node itself.
if node.prefix == "" {
node.prefix = path
node.data = data
node.addTrailingSlash(data)
return
}
child := &TreeNode[T]{
prefix: path,
data: data,
}
node.addChild(child)
child.addTrailingSlash(data)
return
}
// If we're directly in front of a parameter,
// add a parameter node.
if paramStart == 0 {
paramEnd := strings.IndexByte(path, separator)
if paramEnd == -1 {
paramEnd = len(path)
}
child := &TreeNode[T]{
prefix: path[1:paramEnd],
kind: path[paramStart],
}
switch child.kind {
case parameter:
child.addTrailingSlash(data)
node.parameter = child
node = child
path = path[paramEnd:]
continue
case wildcard:
child.data = data
node.wildcard = child
return
}
}
// We know there's a parameter, but not directly at the start.
// If the node itself doesn't have a prefix (root node),
// don't add a child and use the node itself.
if node.prefix == "" {
node.prefix = path[:paramStart]
path = path[paramStart:]
continue
}
// Add a normal node with the path before the parameter start.
child := &TreeNode[T]{
prefix: path[:paramStart],
}
// Allow trailing slashes to return
// the same content as their parent node.
if child.prefix == "/" {
child.data = node.data
}
node.addChild(child)
node = child
path = path[paramStart:]
}
}
// Add a child tree node
func (node *TreeNode[T]) addChild(child *TreeNode[T]) {
if len(node.children) == 0 {
node.children = append(node.children, nil)
}
firstChar := child.prefix[0]
switch {
case node.start == 0:
node.start = firstChar
node.indexes = []uint8{0}
node.end = node.start + uint8(len(node.indexes))
case firstChar < node.start:
diff := node.start - firstChar
newindexes := make([]uint8, diff+uint8(len(node.indexes)))
copy(newindexes[diff:], node.indexes)
node.start = firstChar
node.indexes = newindexes
node.end = node.start + uint8(len(node.indexes))
case firstChar >= node.end:
diff := firstChar - node.end + 1
newindexes := make([]uint8, diff+uint8(len(node.indexes)))
copy(newindexes, node.indexes)
node.indexes = newindexes
node.end = node.start + uint8(len(node.indexes))
}
index := node.indexes[firstChar-node.start]
if index == 0 {
node.indexes[firstChar-node.start] = uint8(len(node.children))
node.children = append(node.children, child)
return
}
node.children[index] = child
}
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[T]{
prefix: "/",
data: data,
})
}
// Traverses the tree and calls the given function on every node.
func (node *TreeNode[T]) each(callback func(*TreeNode[T])) {
callback(node)
for _, child := range node.children {
if child == nil {
continue
}
child.each(callback)
}
if node.parameter != nil {
node.parameter.each(callback)
}
if node.wildcard != nil {
node.wildcard.each(callback)
}
}
// 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[T]) finish(path string, data T, i int, offset int) (*TreeNode[T], int, Flow) {
char := path[i]
if char >= node.start && char < node.end {
index := node.indexes[char-node.start]
if index != 0 {
node = node.children[index]
offset = i
return node, offset, flowNext
}
}
// No fitting children found, does this node even contain a prefix yet?
// If no prefix is set, this is the starting node.
if node.prefix == "" {
node.append(path[i:], data)
return node, offset, flowStop
}
// node: /user/|:id
// path: /user/|:id/profile
if node.parameter != nil && path[i] == parameter {
node = node.parameter
offset = i
return node, offset, flowBegin
}
node.append(path[i:], data)
return node, offset, flowStop
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module git.sharkk.net/Go/Router
go 1.23.0