Improved route order flexibility

This commit is contained in:
Eduard Urbach 2023-07-10 14:48:28 +02:00
parent 9676708199
commit 46e1f07d6c
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
4 changed files with 99 additions and 5 deletions

View File

@ -2,9 +2,11 @@
HTTP router based on radix trees. HTTP router based on radix trees.
## Example ## Usage
We can save any type of data inside the router. Here is an example storing strings for each route: ### Static
We can save any type of data inside the router. Here is an example storing strings for static routes:
```go ```go
router := router.New[string]() router := router.New[string]()
@ -13,11 +15,34 @@ router.Add("GET", "/hello", "Hello")
router.Add("GET", "/world", "World") router.Add("GET", "/world", "World")
``` ```
### Parameters
The router supports parameters:
```go
router.Add("GET", "/users/:id", "...")
router.Add("GET", "/users/:id/comments", "...")
```
### Wildcards
The router can also fall back to a catch-all route which is useful for file servers:
```go
router.Add("GET", "/images/*path", "...")
```
## Benchmarks ## Benchmarks
Requesting every single route in [github.txt](testdata/github.txt): Requesting every single route in [github.txt](testdata/github.txt) (≈200 requests) in each iteration:
``` ```
BenchmarkLookup-12 33210 36134 ns/op 19488 B/op 337 allocs/op BenchmarkLookup-12 33210 36134 ns/op 19488 B/op 337 allocs/op
BenchmarkLookupNoAlloc-12 103437 11331 ns/op 0 B/op 0 allocs/op BenchmarkLookupNoAlloc-12 103437 11331 ns/op 0 B/op 0 allocs/op
``` ```
## Embedding
If you'd like to embed this router into your own framework, please use `LookupNoAlloc` because it's much faster than `Lookup`.
To build an http server you would of course store request handlers (functions), not strings.

View File

@ -1,5 +1,7 @@
package router package router
import "os"
// Router is a high-performance router. // Router is a high-performance router.
type Router[T comparable] struct { type Router[T comparable] struct {
get Tree[T] get Tree[T]
@ -57,6 +59,12 @@ func (router *Router[T]) Bind(transform func(T) T) {
router.options.Bind(transform) router.options.Bind(transform)
} }
// Print shows a pretty print of all the routes.
func (router *Router[T]) Print(method string) {
tree := router.selectTree(method)
tree.root.PrettyPrint(os.Stdout)
}
// selectTree returns the tree by the given HTTP method. // selectTree returns the tree by the given HTTP method.
func (router *Router[T]) selectTree(method string) *Tree[T] { func (router *Router[T]) selectTree(method string) *Tree[T] {
switch method { switch method {

View File

@ -59,12 +59,26 @@ func TestWildcard(t *testing.T) {
router := router.New[string]() router := router.New[string]()
router.Add("GET", "/", "Front page") router.Add("GET", "/", "Front page")
router.Add("GET", "/:slug", "Blog post")
router.Add("GET", "/users/:id", "Parameter")
router.Add("GET", "/images/*path", "Wildcard") router.Add("GET", "/images/*path", "Wildcard")
data, params := router.Lookup("GET", "/") data, params := router.Lookup("GET", "/")
assert.Equal(t, len(params), 0) assert.Equal(t, len(params), 0)
assert.Equal(t, data, "Front page") assert.Equal(t, data, "Front page")
data, params = router.Lookup("GET", "/blog-post")
assert.Equal(t, len(params), 1)
assert.Equal(t, params[0].Key, "slug")
assert.Equal(t, params[0].Value, "blog-post")
assert.Equal(t, data, "Blog post")
data, params = router.Lookup("GET", "/users/42")
assert.Equal(t, len(params), 1)
assert.Equal(t, params[0].Key, "id")
assert.Equal(t, params[0].Value, "42")
assert.Equal(t, data, "Parameter")
data, params = router.Lookup("GET", "/images/favicon/256.png") data, params = router.Lookup("GET", "/images/favicon/256.png")
assert.Equal(t, len(params), 1) assert.Equal(t, len(params), 1)
assert.Equal(t, params[0].Key, "path") assert.Equal(t, params[0].Key, "path")

View File

@ -1,6 +1,8 @@
package router package router
import ( import (
"fmt"
"io"
"strings" "strings"
) )
@ -228,6 +230,7 @@ func (node *treeNode[T]) append(path string, data T) {
// end is called when the node was fully parsed // end is called when the node was fully parsed
// and needs to decide the next control flow. // and needs to decide the next control flow.
// end is only called from `tree.Add`.
func (node *treeNode[T]) end(path string, data T, i int, offset int) (*treeNode[T], int, controlFlow) { func (node *treeNode[T]) end(path string, data T, i int, offset int) (*treeNode[T], int, controlFlow) {
char := path[i] char := path[i]
@ -250,7 +253,7 @@ func (node *treeNode[T]) end(path string, data T, i int, offset int) (*treeNode[
// node: /user/|:id // node: /user/|:id
// path: /user/|:id/profile // path: /user/|:id/profile
if node.parameter != nil { if node.parameter != nil && path[i] == parameter {
node = node.parameter node = node.parameter
offset = i offset = i
return node, offset, controlBegin return node, offset, controlBegin
@ -280,3 +283,47 @@ func (node *treeNode[T]) each(callback func(*treeNode[T])) {
node.wildcard.each(callback) node.wildcard.each(callback)
} }
} }
// PrettyPrint prints a human-readable form of the tree to the given writer.
func (node *treeNode[T]) PrettyPrint(writer io.Writer) {
node.prettyPrint(writer, -1)
}
// prettyPrint
func (node *treeNode[T]) prettyPrint(writer io.Writer, level int) {
prefix := ""
if level >= 0 {
prefix = strings.Repeat(" ", level) + "|_ "
}
switch node.kind {
case ':':
prefix += ":"
case '*':
prefix += "*"
}
fmt.Fprintf(writer, "%s%s [%v]\n", prefix, node.prefix, node.data)
for _, child := range node.children {
if child == nil {
continue
}
child.prettyPrint(writer, level+1)
}
if node.parameter != nil {
node.parameter.prettyPrint(writer, level+1)
}
if node.wildcard != nil {
node.wildcard.prettyPrint(writer, level+1)
}
}
// String returns the node prefix.
func (node *treeNode[T]) String() string {
return node.prefix
}