Improved route order flexibility
This commit is contained in:
parent
9676708199
commit
46e1f07d6c
31
README.md
31
README.md
@ -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.
|
@ -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 {
|
||||||
|
@ -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")
|
||||||
|
49
treeNode.go
49
treeNode.go
@ -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
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user