Compare commits
No commits in common. "b391f061213cb0db42b023a2afcb270362a86684" and "6e7634052f98bc17b1131bf860524dfb72510896" have entirely different histories.
b391f06121
...
6e7634052f
9
Flow.go
9
Flow.go
|
@ -1,9 +0,0 @@
|
||||||
package router
|
|
||||||
|
|
||||||
type Flow int
|
|
||||||
|
|
||||||
const (
|
|
||||||
flowStop Flow = iota
|
|
||||||
flowBegin
|
|
||||||
flowNext
|
|
||||||
)
|
|
|
@ -1,6 +0,0 @@
|
||||||
package router
|
|
||||||
|
|
||||||
type Parameter struct {
|
|
||||||
Key string
|
|
||||||
Value string
|
|
||||||
}
|
|
84
Router.go
84
Router.go
|
@ -1,84 +0,0 @@
|
||||||
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
195
Tree.go
|
@ -1,195 +0,0 @@
|
||||||
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
280
TreeNode.go
|
@ -1,280 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
package router_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
router "git.sharkk.net/Go/Router"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestHello(t *testing.T) {
|
|
||||||
r := router.New[string]()
|
|
||||||
r.Add("GET", "/blog", "Blog")
|
|
||||||
r.Add("GET", "/blog/post", "Blog post")
|
|
||||||
|
|
||||||
data, params := r.Lookup("GET", "/blog")
|
|
||||||
if data != "Blog" || len(params) != 0 {
|
|
||||||
t.Error("Invalid data")
|
|
||||||
}
|
|
||||||
|
|
||||||
data, params = r.Lookup("GET", "/blog/post")
|
|
||||||
if data != "Blog post" || len(params) != 0 {
|
|
||||||
t.Error("Invalid data")
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user