281 lines
6.3 KiB
Go
281 lines
6.3 KiB
Go
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
|
|
}
|