Added basic routing
This commit is contained in:
parent
cdc8303c74
commit
1357c04549
10
.gitignore
vendored
10
.gitignore
vendored
@ -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
|
||||
!*/
|
||||
|
||||
|
2
LICENSE
2
LICENSE
@ -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:
|
||||
|
||||
|
248
tree.go
Normal file
248
tree.go
Normal 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
282
treeNode.go
Normal 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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user