Added basic routing

This commit is contained in:
Eduard Urbach 2023-07-09 12:42:33 +02:00
parent cdc8303c74
commit 1357c04549
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
5 changed files with 534 additions and 11 deletions

10
.gitignore vendored
View File

@ -1,10 +1,3 @@
# ---> Go.AllowList
# Allowlisting gitignore template for GO projects prevents us
# from adding various unwanted local files, such as generated
# files, developer configurations or IDE-specific files etc.
#
# Recommended: Go.AllowList.gitignore
# Ignore everything
*
@ -18,8 +11,5 @@
!README.md
!LICENSE
# !Makefile
# ...even if they are in subdirectories
!*/

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) <year> <copyright holders>
Copyright (c) 2023 Eduard Urbach
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module git.akyoto.dev/go/router
go 1.20

248
tree.go Normal file
View File

@ -0,0 +1,248 @@
package router
import (
"strings"
)
// controlFlow tells the main loop what it should do next.
type controlFlow int
// controlFlow values.
const (
controlStop controlFlow = 0
controlBegin controlFlow = 1
controlNext controlFlow = 2
)
// tree represents a radix tree.
type tree[T comparable] struct {
root treeNode[T]
static map[string]T
canBeStatic [2048]bool
}
// add adds a new element to the tree.
func (tree *tree[T]) add(path string, data T) {
if !strings.Contains(path, ":") && !strings.Contains(path, "*") {
if tree.static == nil {
tree.static = map[string]T{}
}
tree.static[path] = data
tree.canBeStatic[len(path)] = true
return
}
// 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 {
var control controlFlow
node, offset, control = node.end(path, data, i, offset)
switch control {
case controlStop:
return
case controlBegin:
goto begin
case controlNext:
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 controlFlow
node, offset, control = node.end(path, data, i, offset)
switch control {
case controlStop:
return
case controlBegin:
goto begin
case controlNext:
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++
}
}
// find finds the data for the given path and assigns it to ctx.handler, if available.
func (tree *tree[T]) find(path string, ctx *context) {
if tree.canBeStatic[len(path)] {
handler, found := tree.static[path]
if found {
ctx.handler = handler
return
}
}
var (
i uint
offset uint
lastWildcardOffset uint
lastWildcard *treeNode[T]
node = &tree.root
)
begin:
// Search tree for equal parts until we can no longer proceed
for {
// We reached the end.
if i == uint(len(path)) {
// node: /blog|
// path: /blog|
if i-offset == uint(len(node.prefix)) {
ctx.handler = node.data
return
}
// node: /blog|feed
// path: /blog|
ctx.handler = nil
return
}
// The node we just checked is entirely included in our path.
// node: /|
// path: /|blog
if i-offset == uint(len(node.prefix)) {
if node.wildcard != nil {
lastWildcard = node.wildcard
lastWildcardOffset = i
}
char := path[i]
if char >= node.startIndex && char < node.endIndex {
index := node.indices[char-node.startIndex]
if index != 0 {
node = node.children[index]
offset = i
i++
continue
}
}
// node: /|:id
// path: /|blog
if node.parameter != nil {
node = node.parameter
offset = i
i++
for {
// We reached the end.
if i == uint(len(path)) {
ctx.addParameter(node.prefix, path[offset:i])
ctx.handler = node.data
return
}
// node: /:id|/posts
// path: /123|/posts
if path[i] == separator {
ctx.addParameter(node.prefix, path[offset:i])
index := node.indices[separator-node.startIndex]
node = node.children[index]
offset = i
i++
goto begin
}
i++
}
}
// node: /|*any
// path: /|image.png
if node.wildcard != nil {
ctx.addParameter(node.wildcard.prefix, path[i:])
ctx.handler = node.wildcard.data
return
}
ctx.handler = nil
return
}
// We got a conflict.
// node: /b|ag
// path: /b|riefcase
if path[i] != node.prefix[i-offset] {
if lastWildcard != nil {
ctx.addParameter(lastWildcard.prefix, path[lastWildcardOffset:])
ctx.handler = lastWildcard.data
return
}
ctx.handler = nil
return
}
i++
}
}
// bind binds all handlers to a new one provided by the callback.
func (tree *tree[T]) bind(transform func(T) T) {
var empty T
tree.root.each(func(node *treeNode[T]) {
if node.data != empty {
node.data = transform(node.data)
}
})
for key, value := range tree.static {
tree.static[key] = transform(value)
}
}

282
treeNode.go Normal file
View File

@ -0,0 +1,282 @@
package router
import (
"strings"
)
// node types
const (
separator = '/'
parameter = ':'
wildcard = '*'
)
// treeNode represents a radix tree node.
type treeNode[T any] struct {
startIndex uint8
endIndex uint8
kind byte
prefix string
indices []uint8
children []*treeNode[T]
data T
parameter *treeNode[T]
wildcard *treeNode[T]
}
// split 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 clones the node with a new prefix.
func (node *treeNode[T]) clone(prefix string) *treeNode[T] {
return &treeNode[T]{
prefix: prefix,
data: node.data,
indices: node.indices,
startIndex: node.startIndex,
endIndex: node.endIndex,
children: node.children,
parameter: node.parameter,
wildcard: node.wildcard,
kind: node.kind,
}
}
// reset resets the existing node data.
func (node *treeNode[T]) reset(prefix string) {
var empty T
node.prefix = prefix
node.data = empty
node.parameter = nil
node.wildcard = nil
node.kind = 0
node.startIndex = 0
node.endIndex = 0
node.indices = nil
node.children = nil
}
// addChild adds a child tree.
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.startIndex == 0:
node.startIndex = firstChar
node.indices = []uint8{0}
node.endIndex = node.startIndex + uint8(len(node.indices))
case firstChar < node.startIndex:
diff := node.startIndex - firstChar
newIndices := make([]uint8, diff+uint8(len(node.indices)))
copy(newIndices[diff:], node.indices)
node.startIndex = firstChar
node.indices = newIndices
node.endIndex = node.startIndex + uint8(len(node.indices))
case firstChar >= node.endIndex:
diff := firstChar - node.endIndex + 1
newIndices := make([]uint8, diff+uint8(len(node.indices)))
copy(newIndices, node.indices)
node.indices = newIndices
node.endIndex = node.startIndex + uint8(len(node.indices))
}
index := node.indices[firstChar-node.startIndex]
if index == 0 {
node.indices[firstChar-node.startIndex] = uint8(len(node.children))
node.children = append(node.children, child)
return
}
node.children[index] = child
}
// addTrailingSlash adds a trailing slash with the same data.
func (node *treeNode[T]) addTrailingSlash(data T) {
if strings.HasSuffix(node.prefix, "/") || node.kind == wildcard || (separator >= node.startIndex && separator < node.endIndex && node.indices[separator-node.startIndex] != 0) {
return
}
node.addChild(&treeNode[T]{
prefix: "/",
data: data,
})
}
// append appends 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
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:]
}
}
// end is called when the node was fully parsed
// and needs to decide the next control flow.
func (node *treeNode[T]) end(path string, data T, i int, offset int) (*treeNode[T], int, controlFlow) {
char := path[i]
if char >= node.startIndex && char < node.endIndex {
index := node.indices[char-node.startIndex]
if index != 0 {
node = node.children[index]
offset = i
return node, offset, controlNext
}
}
// 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, controlStop
}
// node: /user/|:id
// path: /user/|:id/profile
if node.parameter != nil {
node = node.parameter
offset = i
return node, offset, controlBegin
}
node.append(path[i:], data)
return node, offset, controlStop
}
// each 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)
}
}