diff --git a/Flow.go b/Flow.go new file mode 100644 index 0000000..a45d140 --- /dev/null +++ b/Flow.go @@ -0,0 +1,9 @@ +package router + +type Flow int + +const ( + flowStop Flow = iota + flowBegin + flowNext +) diff --git a/Parameter.go b/Parameter.go new file mode 100644 index 0000000..2de46d6 --- /dev/null +++ b/Parameter.go @@ -0,0 +1,6 @@ +package router + +type Parameter struct { + Key string + Value string +} diff --git a/Router.go b/Router.go new file mode 100644 index 0000000..3db0647 --- /dev/null +++ b/Router.go @@ -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 + } +} diff --git a/Tree.go b/Tree.go new file mode 100644 index 0000000..5bcd10b --- /dev/null +++ b/Tree.go @@ -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) + }) +} diff --git a/TreeNode.go b/TreeNode.go new file mode 100644 index 0000000..1eb269b --- /dev/null +++ b/TreeNode.go @@ -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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e71e483 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.sharkk.net/Go/Router + +go 1.23.0