Rewrite the router
This commit is contained in:
parent
1bd1a884c4
commit
5791042454
89
README.md
89
README.md
|
@ -1,9 +1,13 @@
|
|||
# Router
|
||||
|
||||
A radix-tree based no-allocation router in Go. All credit to Eduard Urbach for his incredible work. This router sports
|
||||
a fancy PATRICIA tree structure for efficient string lookups. It also has ***zero dependencies***!
|
||||
A high-performance router for Go with support for path parameters and wildcards. Sports an incredibly simple API over the top of a prefix tree data structure. Minimal allocations and, dare we say, 🔥*blazingly* fast🔥.
|
||||
|
||||
The router has a generic data structure, so any kind of handler can be used.
|
||||
## Features
|
||||
- Zero dependencies
|
||||
- Fast path matching with radix tree structure
|
||||
- Support for path parameters (`[id]`) and wildcards (`*path`)
|
||||
- Up to 15x faster than `http.ServeMux` for dynamic routes
|
||||
- Zero allocations for static routes
|
||||
|
||||
## Installation
|
||||
|
||||
|
@ -14,50 +18,73 @@ go get git.sharkk.net/Go/Router
|
|||
## Usage
|
||||
|
||||
```go
|
||||
router := router.New[string]()
|
||||
type Handler func(params []string)
|
||||
|
||||
router := router.New()
|
||||
|
||||
// Static routes
|
||||
router.Add("GET", "/hello", "...")
|
||||
router.Add("GET", "/world", "...")
|
||||
router.Get("/", func(params []string) {
|
||||
fmt.Println("Root handler")
|
||||
})
|
||||
router.Get("/users/all", func(params []string) {
|
||||
fmt.Println("All users")
|
||||
})
|
||||
|
||||
// Parameter routes
|
||||
router.Add("GET", "/users/[id]", "...")
|
||||
router.Add("GET", "/users/[id]/comments", "...")
|
||||
router.Get("/users/[id]", func(params []string) {
|
||||
userID := params[0]
|
||||
fmt.Printf("User ID: %s\n", userID)
|
||||
})
|
||||
|
||||
// Nested parameters
|
||||
router.Get("/users/[id]/posts/[postId]", func(params []string) {
|
||||
userID := params[0]
|
||||
postID := params[1]
|
||||
fmt.Printf("User %s, Post %s\n", userID, postID)
|
||||
})
|
||||
|
||||
// Wildcard routes
|
||||
router.Add("GET", "/images/*path", "...")
|
||||
|
||||
// Simple lookup
|
||||
data, params := router.Lookup("GET", "/users/42")
|
||||
fmt.Println(data, params)
|
||||
|
||||
// Efficient lookup
|
||||
data := router.LookupNoAlloc("GET", "/users/42", func(key string, value string) {
|
||||
fmt.Println(key, value)
|
||||
router.Get("/files/*path", func(params []string) {
|
||||
filePath := params[0]
|
||||
fmt.Printf("File path: %s\n", filePath)
|
||||
})
|
||||
|
||||
// Lookup routes
|
||||
if handler, params, ok := router.Lookup("GET", "/users/123"); ok {
|
||||
handler(params)
|
||||
}
|
||||
```
|
||||
|
||||
## Benchmarks
|
||||
|
||||
Benchmark results comparing our router to the standard `http.ServeMux` on AMD Ryzen 9 7950X:
|
||||
|
||||
```
|
||||
cpu: AMD Ryzen 9 7950X 16-Core Processor
|
||||
|
||||
BenchmarkBlog/Len1-Params0-32 268391120 4.461 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkBlog/Len1-Params0-NoAlloc-32 383737024 3.123 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkBlog/Len1-Param1-32 23690427 47.85 ns/op 32 B/op 1 allocs/op
|
||||
BenchmarkBlog/Len1-Param1-NoAlloc-32 248275540 4.913 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkComparison/root_path
|
||||
Router: 2.098 ns/op 0 B/op 0 allocs/op
|
||||
ServeMux: 32.010 ns/op 0 B/op 0 allocs/op
|
||||
|
||||
BenchmarkGithub/Len7-Params0-32 148926122 8.043 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGithub/Len7-Params0-NoAlloc-32 168011829 7.153 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGithub/Len7-Params1-32 22188592 47.25 ns/op 32 B/op 1 allocs/op
|
||||
BenchmarkGithub/Len7-Params1-NoAlloc-32 100000000 11.29 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGithub/Len7-Params3-32 10181496 111.3 ns/op 96 B/op 2 allocs/op
|
||||
BenchmarkGithub/Len7-Params3-NoAlloc-32 46369563 25.54 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkComparison/static_path
|
||||
Router: 16.050 ns/op 0 B/op 0 allocs/op
|
||||
ServeMux: 67.980 ns/op 0 B/op 0 allocs/op
|
||||
|
||||
BenchmarkComparison/dynamic_path
|
||||
Router: 39.170 ns/op 16 B/op 1 allocs/op
|
||||
ServeMux: 174.000 ns/op 48 B/op 3 allocs/op
|
||||
|
||||
BenchmarkComparison/not_found
|
||||
Router: 10.580 ns/op 0 B/op 0 allocs/op
|
||||
ServeMux: 178.100 ns/op 56 B/op 3 allocs/op
|
||||
```
|
||||
|
||||
Key Performance Points:
|
||||
- Root path lookups are 15x faster
|
||||
- Static paths are 4x faster with zero allocations
|
||||
- Dynamic paths are 4.4x faster with fewer allocations
|
||||
- Not found paths are 16.8x faster with zero allocations
|
||||
|
||||
## License
|
||||
|
||||
Licensed under MIT. [Take a look!](LICENSE)
|
||||
|
||||
## Credit
|
||||
|
||||
All of the code here is built by [Eduard Urbach](https://git.akyoto.dev/go/router).
|
||||
|
|
4
go.mod
4
go.mod
|
@ -1,3 +1,5 @@
|
|||
module git.sharkk.net/Go/Router
|
||||
|
||||
go 1.23.0
|
||||
go 1.23.5
|
||||
|
||||
require git.sharkk.net/Go/Assert v0.0.0-20250215225259-e80b22c45aa3
|
||||
|
|
2
go.sum
Normal file
2
go.sum
Normal file
|
@ -0,0 +1,2 @@
|
|||
git.sharkk.net/Go/Assert v0.0.0-20250215225259-e80b22c45aa3 h1:zkadtphuR4rYrKqTKZlBfbJw9wtkhSIi5extZwbx1BY=
|
||||
git.sharkk.net/Go/Assert v0.0.0-20250215225259-e80b22c45aa3/go.mod h1:7AMVm0RCtLlQfWsnKs6h/IdSfzj52/o0nR03rCW68gM=
|
286
node.go
286
node.go
|
@ -1,286 +0,0 @@
|
|||
package router
|
||||
|
||||
import "strings"
|
||||
|
||||
// Node types
|
||||
const (
|
||||
separator = '/'
|
||||
parameter = '['
|
||||
wildcard = '*'
|
||||
)
|
||||
|
||||
// A node on our radix tree
|
||||
type Node[T any] struct {
|
||||
prefix string
|
||||
data T
|
||||
children []*Node[T]
|
||||
parameter *Node[T]
|
||||
wildcard *Node[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 *Node[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 *Node[T]) clone(prefix string) *Node[T] {
|
||||
return &Node[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 *Node[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 *Node[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 := &Node[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[1:], ']')
|
||||
if paramEnd == -1 {
|
||||
paramEnd = len(path)
|
||||
} else {
|
||||
paramEnd = paramEnd + 1 // Account for the offset from path[1:]
|
||||
}
|
||||
|
||||
nextSep := strings.IndexByte(path[paramEnd+1:], separator)
|
||||
if nextSep == -1 {
|
||||
nextSep = len(path[paramEnd+1:])
|
||||
}
|
||||
|
||||
child := &Node[T]{
|
||||
prefix: path[1:paramEnd], // Exclude the opening '['
|
||||
kind: parameter,
|
||||
}
|
||||
|
||||
switch child.kind {
|
||||
case parameter:
|
||||
child.addTrailingSlash(data)
|
||||
node.parameter = child
|
||||
node = child
|
||||
path = path[paramEnd+nextSep+1:]
|
||||
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 := &Node[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 *Node[T]) addChild(child *Node[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 *Node[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(&Node[T]{
|
||||
prefix: "/",
|
||||
data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// Traverses the tree and calls the given function on every node.
|
||||
func (node *Node[T]) each(callback func(*Node[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 *Node[T]) finish(path string, data T, i int, offset int) (*Node[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
|
||||
}
|
278
router.go
278
router.go
|
@ -1,96 +1,224 @@
|
|||
package router
|
||||
|
||||
type Flow int
|
||||
|
||||
const (
|
||||
flowStop Flow = iota
|
||||
flowBegin
|
||||
flowNext
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Parameter struct {
|
||||
Key string
|
||||
Value string
|
||||
// Handler processes HTTP requests with optional path parameters.
|
||||
type Handler func(params []string)
|
||||
|
||||
// node represents a segment in the URL path and its handling logic.
|
||||
type node struct {
|
||||
segment string // the path segment this node matches
|
||||
handler Handler // handler for this path, if it's an endpoint
|
||||
children []*node // child nodes for subsequent path segments
|
||||
isDynamic bool // true for param segments like [id]
|
||||
isWildcard bool // true for catch-all segments like *filepath
|
||||
maxParams uint8 // maximum number of parameters in paths under this node
|
||||
}
|
||||
|
||||
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]
|
||||
// Router routes HTTP requests by method and path.
|
||||
// It supports static paths, path parameters, and wildcards.
|
||||
type Router struct {
|
||||
get *node
|
||||
post *node
|
||||
put *node
|
||||
patch *node
|
||||
delete *node
|
||||
}
|
||||
|
||||
// Create a new Router containing trees for every HTTP method.
|
||||
func New[T any]() *Router[T] {
|
||||
return &Router[T]{}
|
||||
// New creates a new Router instance.
|
||||
func New() *Router {
|
||||
return &Router{
|
||||
get: &node{},
|
||||
post: &node{},
|
||||
put: &node{},
|
||||
patch: &node{},
|
||||
delete: &node{},
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
// HTTP method registration functions
|
||||
// Routes can contain static segments (/users), parameters ([id]), and wildcards (*filepath)
|
||||
|
||||
// Get registers a handler for GET requests at the given path.
|
||||
func (r *Router) Get(path string, handler Handler) error {
|
||||
return r.addRoute(r.get, path, handler)
|
||||
}
|
||||
|
||||
// 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)
|
||||
// Post registers a handler for POST requests at the given path.
|
||||
func (r *Router) Post(path string, handler Handler) error {
|
||||
return r.addRoute(r.post, path, handler)
|
||||
}
|
||||
|
||||
// Put registers a handler for PUT requests at the given path.
|
||||
func (r *Router) Put(path string, handler Handler) error {
|
||||
return r.addRoute(r.put, path, handler)
|
||||
}
|
||||
|
||||
// Patch registers a handler for PATCH requests at the given path.
|
||||
func (r *Router) Patch(path string, handler Handler) error {
|
||||
return r.addRoute(r.patch, path, handler)
|
||||
}
|
||||
|
||||
// Delete registers a handler for DELETE requests at the given path.
|
||||
func (r *Router) Delete(path string, handler Handler) error {
|
||||
return r.addRoute(r.delete, path, handler)
|
||||
}
|
||||
|
||||
// readSegment extracts the next path segment starting at the given position.
|
||||
// Returns the segment, the position after it, and whether there are more segments.
|
||||
func readSegment(path string, start int) (segment string, end int, hasMore bool) {
|
||||
if start >= len(path) {
|
||||
return "", start, false
|
||||
}
|
||||
|
||||
tree := router.selectTree(method)
|
||||
return tree.Lookup(path)
|
||||
}
|
||||
|
||||
// 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)
|
||||
if path[start] == '/' {
|
||||
start++
|
||||
}
|
||||
|
||||
tree := router.selectTree(method)
|
||||
return tree.LookupNoAlloc(path, addParameter)
|
||||
if start >= len(path) {
|
||||
return "", start, false
|
||||
}
|
||||
|
||||
end = start
|
||||
for end < len(path) && path[end] != '/' {
|
||||
end++
|
||||
}
|
||||
|
||||
return path[start:end], end, end < len(path)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Returns the tree of 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:
|
||||
// addRoute adds a new route to the prefix tree.
|
||||
// It validates wildcard positions and tracks parameter counts.
|
||||
func (r *Router) addRoute(root *node, path string, handler Handler) error {
|
||||
if path == "/" {
|
||||
root.handler = handler
|
||||
return nil
|
||||
}
|
||||
|
||||
current := root
|
||||
pos := 0
|
||||
var lastWildcard bool
|
||||
paramsCount := uint8(0)
|
||||
|
||||
for {
|
||||
segment, newPos, hasMore := readSegment(path, pos)
|
||||
if segment == "" {
|
||||
break
|
||||
}
|
||||
|
||||
isDynamic := len(segment) > 2 && segment[0] == '[' && segment[len(segment)-1] == ']'
|
||||
isWildcard := len(segment) > 0 && segment[0] == '*'
|
||||
|
||||
if isWildcard {
|
||||
if lastWildcard {
|
||||
return fmt.Errorf("wildcard must be the last segment in the path")
|
||||
}
|
||||
if hasMore {
|
||||
return fmt.Errorf("wildcard must be the last segment in the path")
|
||||
}
|
||||
lastWildcard = true
|
||||
}
|
||||
|
||||
if isDynamic || isWildcard {
|
||||
paramsCount++
|
||||
}
|
||||
|
||||
var child *node
|
||||
for _, n := range current.children {
|
||||
if n.segment == segment {
|
||||
child = n
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if child == nil {
|
||||
child = &node{
|
||||
segment: segment,
|
||||
isDynamic: isDynamic,
|
||||
isWildcard: isWildcard,
|
||||
}
|
||||
current.children = append(current.children, child)
|
||||
}
|
||||
|
||||
if child.maxParams < paramsCount {
|
||||
child.maxParams = paramsCount
|
||||
}
|
||||
current = child
|
||||
pos = newPos
|
||||
}
|
||||
|
||||
current.handler = handler
|
||||
return nil
|
||||
}
|
||||
|
||||
// Lookup finds a handler matching the given method and path.
|
||||
// Returns the handler, any captured parameters, and whether a match was found.
|
||||
func (r *Router) Lookup(method, path string) (Handler, []string, bool) {
|
||||
var root *node
|
||||
switch method {
|
||||
case "GET":
|
||||
root = r.get
|
||||
case "POST":
|
||||
root = r.post
|
||||
case "PUT":
|
||||
root = r.put
|
||||
case "PATCH":
|
||||
root = r.patch
|
||||
case "DELETE":
|
||||
root = r.delete
|
||||
default:
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
if path == "/" {
|
||||
return root.handler, nil, root.handler != nil
|
||||
}
|
||||
|
||||
params := make([]string, 0, root.maxParams)
|
||||
h, found := match(root, path, 0, ¶ms)
|
||||
if !found {
|
||||
return nil, nil, false
|
||||
}
|
||||
return h, params, true
|
||||
}
|
||||
|
||||
// match recursively traverses the prefix tree to find a matching handler.
|
||||
// It populates params with any captured path parameters or wildcard matches.
|
||||
func match(current *node, path string, start int, params *[]string) (Handler, bool) {
|
||||
// Check for wildcard children first
|
||||
for _, child := range current.children {
|
||||
if child.isWildcard {
|
||||
remaining := path[start:]
|
||||
if len(remaining) > 0 && remaining[0] == '/' {
|
||||
remaining = remaining[1:]
|
||||
}
|
||||
*params = append(*params, remaining)
|
||||
return child.handler, child.handler != nil
|
||||
}
|
||||
}
|
||||
|
||||
// Read current segment
|
||||
segment, pos, hasMore := readSegment(path, start)
|
||||
if segment == "" {
|
||||
return current.handler, current.handler != nil
|
||||
}
|
||||
|
||||
// Try to match children
|
||||
for _, child := range current.children {
|
||||
if child.segment == segment || child.isDynamic {
|
||||
if child.isDynamic {
|
||||
*params = append(*params, segment)
|
||||
}
|
||||
if !hasMore {
|
||||
return child.handler, child.handler != nil
|
||||
}
|
||||
if h, found := match(child, path, pos, params); found {
|
||||
return h, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
|
299
router_test.go
Normal file
299
router_test.go
Normal file
|
@ -0,0 +1,299 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
assert "git.sharkk.net/Go/Assert"
|
||||
)
|
||||
|
||||
func handler(t *testing.T, expectedParams []string) Handler {
|
||||
return func(params []string) {
|
||||
assert.Equal(t, len(params), len(expectedParams))
|
||||
for i, expected := range expectedParams {
|
||||
assert.Equal(t, params[i], expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootPath(t *testing.T) {
|
||||
r := New()
|
||||
r.Get("/", handler(t, nil))
|
||||
|
||||
h, params, found := r.Lookup("GET", "/")
|
||||
assert.True(t, found)
|
||||
h(params)
|
||||
}
|
||||
|
||||
func TestStaticPath(t *testing.T) {
|
||||
r := New()
|
||||
r.Get("/users/all", handler(t, nil))
|
||||
|
||||
h, params, found := r.Lookup("GET", "/users/all")
|
||||
assert.True(t, found)
|
||||
h(params)
|
||||
}
|
||||
|
||||
func TestSingleParameter(t *testing.T) {
|
||||
r := New()
|
||||
r.Get("/users/[id]", handler(t, []string{"123"}))
|
||||
|
||||
h, params, found := r.Lookup("GET", "/users/123")
|
||||
assert.True(t, found)
|
||||
h(params)
|
||||
}
|
||||
|
||||
func TestMultipleParameters(t *testing.T) {
|
||||
r := New()
|
||||
r.Get("/users/[id]/posts/[postId]", handler(t, []string{"123", "456"}))
|
||||
|
||||
h, params, found := r.Lookup("GET", "/users/123/posts/456")
|
||||
assert.True(t, found)
|
||||
h(params)
|
||||
}
|
||||
|
||||
func TestNonExistentPath(t *testing.T) {
|
||||
r := New()
|
||||
r.Get("/users/[id]", handler(t, nil))
|
||||
|
||||
_, _, found := r.Lookup("GET", "/posts/123")
|
||||
assert.False(t, found)
|
||||
}
|
||||
|
||||
func TestWrongMethod(t *testing.T) {
|
||||
r := New()
|
||||
r.Get("/users/[id]", handler(t, nil))
|
||||
|
||||
_, _, found := r.Lookup("POST", "/users/123")
|
||||
assert.False(t, found)
|
||||
}
|
||||
|
||||
func TestTrailingSlash(t *testing.T) {
|
||||
r := New()
|
||||
r.Get("/users/[id]", handler(t, []string{"123"}))
|
||||
|
||||
h, params, found := r.Lookup("GET", "/users/123/")
|
||||
assert.True(t, found)
|
||||
h(params)
|
||||
}
|
||||
|
||||
func TestDifferentMethods(t *testing.T) {
|
||||
r := New()
|
||||
|
||||
r.Get("/test", handler(t, nil))
|
||||
r.Post("/test", handler(t, nil))
|
||||
r.Put("/test", handler(t, nil))
|
||||
r.Patch("/test", handler(t, nil))
|
||||
r.Delete("/test", handler(t, nil))
|
||||
|
||||
methods := []string{"GET", "POST", "PUT", "PATCH", "DELETE"}
|
||||
for _, method := range methods {
|
||||
t.Run(method, func(t *testing.T) {
|
||||
_, _, found := r.Lookup(method, "/test")
|
||||
assert.True(t, found)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWildcardPath(t *testing.T) {
|
||||
r := New()
|
||||
|
||||
t.Run("simple wildcard", func(t *testing.T) {
|
||||
err := r.Get("/files/*path", handler(t, []string{"docs/report.pdf"}))
|
||||
assert.Nil(t, err)
|
||||
|
||||
h, params, found := r.Lookup("GET", "/files/docs/report.pdf")
|
||||
assert.True(t, found)
|
||||
h(params)
|
||||
})
|
||||
|
||||
t.Run("wildcard with empty path", func(t *testing.T) {
|
||||
err := r.Get("/download/*filepath", handler(t, []string{""}))
|
||||
assert.Nil(t, err)
|
||||
|
||||
h, params, found := r.Lookup("GET", "/download/")
|
||||
assert.True(t, found)
|
||||
h(params)
|
||||
})
|
||||
|
||||
t.Run("wildcard with parameter", func(t *testing.T) {
|
||||
err := r.Get("/users/[id]/*action", handler(t, []string{"123", "settings/profile/avatar"}))
|
||||
assert.Nil(t, err)
|
||||
|
||||
h, params, found := r.Lookup("GET", "/users/123/settings/profile/avatar")
|
||||
assert.True(t, found)
|
||||
h(params)
|
||||
})
|
||||
|
||||
t.Run("multiple wildcards not allowed", func(t *testing.T) {
|
||||
err := r.Get("/api/*version/*path", handler(t, nil))
|
||||
assert.NotNil(t, err)
|
||||
})
|
||||
|
||||
t.Run("non-last wildcard not allowed", func(t *testing.T) {
|
||||
err := r.Get("/api/*version/users", handler(t, nil))
|
||||
assert.NotNil(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmarks
|
||||
func BenchmarkRouterLookup(b *testing.B) {
|
||||
r := New()
|
||||
h := func(params []string) {}
|
||||
|
||||
// Setup routes for benchmarking
|
||||
r.Get("/", h)
|
||||
r.Get("/users/all", h)
|
||||
r.Get("/users/[id]", h)
|
||||
r.Get("/users/[id]/posts/[postId]", h)
|
||||
|
||||
b.Run("root", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
r.Lookup("GET", "/")
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("static", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
r.Lookup("GET", "/users/all")
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("single_param", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
r.Lookup("GET", "/users/123")
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("multi_param", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
r.Lookup("GET", "/users/123/posts/456")
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("not_found", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
r.Lookup("GET", "/nonexistent/path")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkParallelLookup(b *testing.B) {
|
||||
r := New()
|
||||
h := func(params []string) {}
|
||||
|
||||
r.Get("/users/[id]", h)
|
||||
r.Get("/posts/[id]/comments", h)
|
||||
r.Get("/products/[category]/[id]", h)
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
switch i % 3 {
|
||||
case 0:
|
||||
r.Lookup("GET", "/users/123")
|
||||
case 1:
|
||||
r.Lookup("GET", "/posts/456/comments")
|
||||
case 2:
|
||||
r.Lookup("GET", "/products/electronics/789")
|
||||
}
|
||||
i++
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkWildcardLookup(b *testing.B) {
|
||||
r := New()
|
||||
h := func(params []string) {}
|
||||
|
||||
// Setup routes for benchmarking
|
||||
r.Get("/files/*path", h)
|
||||
r.Get("/users/[id]/*action", h)
|
||||
|
||||
b.Run("simple_wildcard", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
r.Lookup("GET", "/files/documents/reports/2024/q1.pdf")
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("wildcard_with_param", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
r.Lookup("GET", "/users/123/settings/profile/avatar")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkComparison(b *testing.B) {
|
||||
// Custom router setup
|
||||
customRouter := New()
|
||||
customRouter.Get("/", func(params []string) {})
|
||||
customRouter.Get("/users/all", func(params []string) {})
|
||||
customRouter.Get("/users/[id]", func(params []string) {})
|
||||
customRouter.Get("/users/[id]/posts/[postId]", func(params []string) {})
|
||||
|
||||
// Standard mux setup
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {})
|
||||
mux.HandleFunc("/users/all", func(w http.ResponseWriter, r *http.Request) {})
|
||||
mux.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {}) // Best equivalent for dynamic routes
|
||||
|
||||
// Root path
|
||||
b.Run("root_path", func(b *testing.B) {
|
||||
b.Run("custom", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
customRouter.Lookup("GET", "/")
|
||||
}
|
||||
})
|
||||
b.Run("mux", func(b *testing.B) {
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
for i := 0; i < b.N; i++ {
|
||||
mux.ServeHTTP(nil, req)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Static path
|
||||
b.Run("static_path", func(b *testing.B) {
|
||||
b.Run("custom", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
customRouter.Lookup("GET", "/users/all")
|
||||
}
|
||||
})
|
||||
b.Run("mux", func(b *testing.B) {
|
||||
req, _ := http.NewRequest("GET", "/users/all", nil)
|
||||
for i := 0; i < b.N; i++ {
|
||||
mux.ServeHTTP(nil, req)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Dynamic path
|
||||
b.Run("dynamic_path", func(b *testing.B) {
|
||||
b.Run("custom", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
customRouter.Lookup("GET", "/users/123")
|
||||
}
|
||||
})
|
||||
b.Run("mux", func(b *testing.B) {
|
||||
req, _ := http.NewRequest("GET", "/users/123", nil)
|
||||
for i := 0; i < b.N; i++ {
|
||||
mux.ServeHTTP(nil, req)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Not found
|
||||
b.Run("not_found", func(b *testing.B) {
|
||||
b.Run("custom", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
customRouter.Lookup("GET", "/nonexistent/path")
|
||||
}
|
||||
})
|
||||
b.Run("mux", func(b *testing.B) {
|
||||
req, _ := http.NewRequest("GET", "/nonexistent/path", nil)
|
||||
for i := 0; i < b.N; i++ {
|
||||
mux.ServeHTTP(nil, req)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
|
@ -1,136 +0,0 @@
|
|||
package router_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
router "git.sharkk.net/Go/Router"
|
||||
)
|
||||
|
||||
func BenchmarkBlog(b *testing.B) {
|
||||
routes := routes("blog.txt")
|
||||
r := router.New[string]()
|
||||
|
||||
for _, route := range routes {
|
||||
r.Add(route.Method, route.Path, "")
|
||||
}
|
||||
|
||||
b.Run("Len1-Params0", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
r.Lookup("GET", "/")
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Len1-Params0-NoAlloc", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
r.LookupNoAlloc("GET", "/", noop)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Len1-Param1", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
r.Lookup("GET", "/:id")
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Len1-Param1-NoAlloc", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
r.LookupNoAlloc("GET", "/:id", noop)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkGithub(b *testing.B) {
|
||||
routes := routes("github.txt")
|
||||
r := router.New[string]()
|
||||
|
||||
for _, route := range routes {
|
||||
r.Add(route.Method, route.Path, "")
|
||||
}
|
||||
|
||||
b.Run("Len7-Params0", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
r.Lookup("GET", "/issues")
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Len7-Params0-NoAlloc", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
r.LookupNoAlloc("GET", "/issues", noop)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Len7-Params1", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
r.Lookup("GET", "/gists/:id")
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Len7-Params1-NoAlloc", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
r.LookupNoAlloc("GET", "/gists/:id", noop)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Len7-Params3", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
r.Lookup("GET", "/repos/:owner/:repo/issues")
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Len7-Params3-NoAlloc", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
r.LookupNoAlloc("GET", "/repos/:owner/:repo/issues", noop)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Route represents a single line in the router test file.
|
||||
type Route struct {
|
||||
Method string
|
||||
Path string
|
||||
}
|
||||
|
||||
// Loads all routes from a text file.
|
||||
func routes(fileName string) []Route {
|
||||
var routes []Route
|
||||
|
||||
for line := range lines(fileName) {
|
||||
line = strings.TrimSpace(line)
|
||||
parts := strings.Split(line, " ")
|
||||
routes = append(routes, Route{
|
||||
Method: parts[0],
|
||||
Path: parts[1],
|
||||
})
|
||||
}
|
||||
|
||||
return routes
|
||||
}
|
||||
|
||||
// Easily read every line in a text file.
|
||||
func lines(fileName string) <-chan string {
|
||||
lines := make(chan string)
|
||||
|
||||
go func() {
|
||||
defer close(lines)
|
||||
file, err := os.Open(fileName)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
scanner := bufio.NewScanner(file)
|
||||
|
||||
for scanner.Scan() {
|
||||
lines <- scanner.Text()
|
||||
}
|
||||
}()
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
// noop serves as an empty addParameter function.
|
||||
func noop(string, string) {}
|
|
@ -1,4 +0,0 @@
|
|||
GET /
|
||||
GET /[slug]
|
||||
GET /tags
|
||||
GET /tag/[tag]
|
203
tests/github.txt
203
tests/github.txt
|
@ -1,203 +0,0 @@
|
|||
GET /authorizations
|
||||
GET /authorizations/[id]
|
||||
POST /authorizations
|
||||
DELETE /authorizations/[id]
|
||||
GET /applications/[client_id]/tokens/[access_token]
|
||||
DELETE /applications/[client_id]/tokens
|
||||
DELETE /applications/[client_id]/tokens/[access_token]
|
||||
GET /events
|
||||
GET /repos/[owner]/[repo]/events
|
||||
GET /networks/[owner]/[repo]/events
|
||||
GET /orgs/[org]/events
|
||||
GET /users/[user]/received_events
|
||||
GET /users/[user]/received_events/public
|
||||
GET /users/[user]/events
|
||||
GET /users/[user]/events/public
|
||||
GET /users/[user]/events/orgs/[org]
|
||||
GET /feeds
|
||||
GET /notifications
|
||||
GET /repos/[owner]/[repo]/notifications
|
||||
PUT /notifications
|
||||
PUT /repos/[owner]/[repo]/notifications
|
||||
GET /notifications/threads/[id]
|
||||
GET /notifications/threads/[id]/subscription
|
||||
PUT /notifications/threads/[id]/subscription
|
||||
DELETE /notifications/threads/[id]/subscription
|
||||
GET /repos/[owner]/[repo]/stargazers
|
||||
GET /users/[user]/starred
|
||||
GET /user/starred
|
||||
GET /user/starred/[owner]/[repo]
|
||||
PUT /user/starred/[owner]/[repo]
|
||||
DELETE /user/starred/[owner]/[repo]
|
||||
GET /repos/[owner]/[repo]/subscribers
|
||||
GET /users/[user]/subscriptions
|
||||
GET /user/subscriptions
|
||||
GET /repos/[owner]/[repo]/subscription
|
||||
PUT /repos/[owner]/[repo]/subscription
|
||||
DELETE /repos/[owner]/[repo]/subscription
|
||||
GET /user/subscriptions/[owner]/[repo]
|
||||
PUT /user/subscriptions/[owner]/[repo]
|
||||
DELETE /user/subscriptions/[owner]/[repo]
|
||||
GET /users/[user]/gists
|
||||
GET /gists
|
||||
GET /gists/[id]
|
||||
POST /gists
|
||||
PUT /gists/[id]/star
|
||||
DELETE /gists/[id]/star
|
||||
GET /gists/[id]/star
|
||||
POST /gists/[id]/forks
|
||||
DELETE /gists/[id]
|
||||
GET /repos/[owner]/[repo]/git/blobs/[sha]
|
||||
POST /repos/[owner]/[repo]/git/blobs
|
||||
GET /repos/[owner]/[repo]/git/commits/[sha]
|
||||
POST /repos/[owner]/[repo]/git/commits
|
||||
GET /repos/[owner]/[repo]/git/refs
|
||||
POST /repos/[owner]/[repo]/git/refs
|
||||
GET /repos/[owner]/[repo]/git/tags/[sha]
|
||||
POST /repos/[owner]/[repo]/git/tags
|
||||
GET /repos/[owner]/[repo]/git/trees/[sha]
|
||||
POST /repos/[owner]/[repo]/git/trees
|
||||
GET /issues
|
||||
GET /user/issues
|
||||
GET /orgs/[org]/issues
|
||||
GET /repos/[owner]/[repo]/issues
|
||||
GET /repos/[owner]/[repo]/issues/[number]
|
||||
POST /repos/[owner]/[repo]/issues
|
||||
GET /repos/[owner]/[repo]/assignees
|
||||
GET /repos/[owner]/[repo]/assignees/[assignee]
|
||||
GET /repos/[owner]/[repo]/issues/[number]/comments
|
||||
POST /repos/[owner]/[repo]/issues/[number]/comments
|
||||
GET /repos/[owner]/[repo]/issues/[number]/events
|
||||
GET /repos/[owner]/[repo]/labels
|
||||
GET /repos/[owner]/[repo]/labels/[name]
|
||||
POST /repos/[owner]/[repo]/labels
|
||||
DELETE /repos/[owner]/[repo]/labels/[name]
|
||||
GET /repos/[owner]/[repo]/issues/[number]/labels
|
||||
POST /repos/[owner]/[repo]/issues/[number]/labels
|
||||
DELETE /repos/[owner]/[repo]/issues/[number]/labels/[name]
|
||||
PUT /repos/[owner]/[repo]/issues/[number]/labels
|
||||
DELETE /repos/[owner]/[repo]/issues/[number]/labels
|
||||
GET /repos/[owner]/[repo]/milestones/[number]/labels
|
||||
GET /repos/[owner]/[repo]/milestones
|
||||
GET /repos/[owner]/[repo]/milestones/[number]
|
||||
POST /repos/[owner]/[repo]/milestones
|
||||
DELETE /repos/[owner]/[repo]/milestones/[number]
|
||||
GET /emojis
|
||||
GET /gitignore/templates
|
||||
GET /gitignore/templates/[name]
|
||||
POST /markdown
|
||||
POST /markdown/raw
|
||||
GET /meta
|
||||
GET /rate_limit
|
||||
GET /users/[user]/orgs
|
||||
GET /user/orgs
|
||||
GET /orgs/[org]
|
||||
GET /orgs/[org]/members
|
||||
GET /orgs/[org]/members/[user]
|
||||
DELETE /orgs/[org]/members/[user]
|
||||
GET /orgs/[org]/public_members
|
||||
GET /orgs/[org]/public_members/[user]
|
||||
PUT /orgs/[org]/public_members/[user]
|
||||
DELETE /orgs/[org]/public_members/[user]
|
||||
GET /orgs/[org]/teams
|
||||
GET /teams/[id]
|
||||
POST /orgs/[org]/teams
|
||||
DELETE /teams/[id]
|
||||
GET /teams/[id]/members
|
||||
GET /teams/[id]/members/[user]
|
||||
PUT /teams/[id]/members/[user]
|
||||
DELETE /teams/[id]/members/[user]
|
||||
GET /teams/[id]/repos
|
||||
GET /teams/[id]/repos/[owner]/[repo]
|
||||
PUT /teams/[id]/repos/[owner]/[repo]
|
||||
DELETE /teams/[id]/repos/[owner]/[repo]
|
||||
GET /user/teams
|
||||
GET /repos/[owner]/[repo]/pulls
|
||||
GET /repos/[owner]/[repo]/pulls/[number]
|
||||
POST /repos/[owner]/[repo]/pulls
|
||||
GET /repos/[owner]/[repo]/pulls/[number]/commits
|
||||
GET /repos/[owner]/[repo]/pulls/[number]/files
|
||||
GET /repos/[owner]/[repo]/pulls/[number]/merge
|
||||
PUT /repos/[owner]/[repo]/pulls/[number]/merge
|
||||
GET /repos/[owner]/[repo]/pulls/[number]/comments
|
||||
PUT /repos/[owner]/[repo]/pulls/[number]/comments
|
||||
GET /user/repos
|
||||
GET /users/[user]/repos
|
||||
GET /orgs/[org]/repos
|
||||
GET /repositories
|
||||
POST /user/repos
|
||||
POST /orgs/[org]/repos
|
||||
GET /repos/[owner]/[repo]
|
||||
GET /repos/[owner]/[repo]/contributors
|
||||
GET /repos/[owner]/[repo]/languages
|
||||
GET /repos/[owner]/[repo]/teams
|
||||
GET /repos/[owner]/[repo]/tags
|
||||
GET /repos/[owner]/[repo]/branches
|
||||
GET /repos/[owner]/[repo]/branches/[branch]
|
||||
DELETE /repos/[owner]/[repo]
|
||||
GET /repos/[owner]/[repo]/collaborators
|
||||
GET /repos/[owner]/[repo]/collaborators/[user]
|
||||
PUT /repos/[owner]/[repo]/collaborators/[user]
|
||||
DELETE /repos/[owner]/[repo]/collaborators/[user]
|
||||
GET /repos/[owner]/[repo]/comments
|
||||
GET /repos/[owner]/[repo]/commits/[sha]/comments
|
||||
POST /repos/[owner]/[repo]/commits/[sha]/comments
|
||||
GET /repos/[owner]/[repo]/comments/[id]
|
||||
DELETE /repos/[owner]/[repo]/comments/[id]
|
||||
GET /repos/[owner]/[repo]/commits
|
||||
GET /repos/[owner]/[repo]/commits/[sha]
|
||||
GET /repos/[owner]/[repo]/readme
|
||||
GET /repos/[owner]/[repo]/keys
|
||||
GET /repos/[owner]/[repo]/keys/[id]
|
||||
POST /repos/[owner]/[repo]/keys
|
||||
DELETE /repos/[owner]/[repo]/keys/[id]
|
||||
GET /repos/[owner]/[repo]/downloads
|
||||
GET /repos/[owner]/[repo]/downloads/[id]
|
||||
DELETE /repos/[owner]/[repo]/downloads/[id]
|
||||
GET /repos/[owner]/[repo]/forks
|
||||
POST /repos/[owner]/[repo]/forks
|
||||
GET /repos/[owner]/[repo]/hooks
|
||||
GET /repos/[owner]/[repo]/hooks/[id]
|
||||
POST /repos/[owner]/[repo]/hooks
|
||||
POST /repos/[owner]/[repo]/hooks/[id]/tests
|
||||
DELETE /repos/[owner]/[repo]/hooks/[id]
|
||||
POST /repos/[owner]/[repo]/merges
|
||||
GET /repos/[owner]/[repo]/releases
|
||||
GET /repos/[owner]/[repo]/releases/[id]
|
||||
POST /repos/[owner]/[repo]/releases
|
||||
DELETE /repos/[owner]/[repo]/releases/[id]
|
||||
GET /repos/[owner]/[repo]/releases/[id]/assets
|
||||
GET /repos/[owner]/[repo]/stats/contributors
|
||||
GET /repos/[owner]/[repo]/stats/commit_activity
|
||||
GET /repos/[owner]/[repo]/stats/code_frequency
|
||||
GET /repos/[owner]/[repo]/stats/participation
|
||||
GET /repos/[owner]/[repo]/stats/punch_card
|
||||
GET /repos/[owner]/[repo]/statuses/[ref]
|
||||
POST /repos/[owner]/[repo]/statuses/[ref]
|
||||
GET /search/repositories
|
||||
GET /search/code
|
||||
GET /search/issues
|
||||
GET /search/users
|
||||
GET /legacy/issues/search/[owner]/[repository]/[state]/[keyword]
|
||||
GET /legacy/repos/search/[keyword]
|
||||
GET /legacy/user/search/[keyword]
|
||||
GET /legacy/user/email/[email]
|
||||
GET /users/[user]
|
||||
GET /user
|
||||
GET /users
|
||||
GET /user/emails
|
||||
POST /user/emails
|
||||
DELETE /user/emails
|
||||
GET /users/[user]/followers
|
||||
GET /user/followers
|
||||
GET /users/[user]/following
|
||||
GET /user/following
|
||||
GET /user/following/[user]
|
||||
GET /users/[user]/following/[target_user]
|
||||
PUT /user/following/[user]
|
||||
DELETE /user/following/[user]
|
||||
GET /users/[user]/keys
|
||||
GET /user/keys
|
||||
GET /user/keys/[id]
|
||||
POST /user/keys
|
||||
DELETE /user/keys/[id]
|
|
@ -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")
|
||||
}
|
||||
}
|
194
tree.go
194
tree.go
|
@ -1,194 +0,0 @@
|
|||
package router
|
||||
|
||||
// Representation of a node tree
|
||||
type Tree[T any] struct {
|
||||
root Node[T]
|
||||
}
|
||||
|
||||
// Adds a new path to the tree
|
||||
func (tree *Tree[T]) Add(path string, data T) {
|
||||
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
|
||||
}
|
||||
|
||||
// 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 *Node[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 *Node[T]) {
|
||||
node.data = transform(node.data)
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue
Block a user