Improved route order flexibility
This commit is contained in:
parent
9676708199
commit
46e1f07d6c
33
README.md
33
README.md
@ -2,9 +2,11 @@
|
||||
|
||||
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
|
||||
router := router.New[string]()
|
||||
@ -13,11 +15,34 @@ router.Add("GET", "/hello", "Hello")
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
import "os"
|
||||
|
||||
// Router is a high-performance router.
|
||||
type Router[T comparable] struct {
|
||||
get Tree[T]
|
||||
@ -57,6 +59,12 @@ func (router *Router[T]) Bind(transform func(T) T) {
|
||||
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.
|
||||
func (router *Router[T]) selectTree(method string) *Tree[T] {
|
||||
switch method {
|
||||
|
@ -59,12 +59,26 @@ func TestWildcard(t *testing.T) {
|
||||
router := router.New[string]()
|
||||
|
||||
router.Add("GET", "/", "Front page")
|
||||
router.Add("GET", "/:slug", "Blog post")
|
||||
router.Add("GET", "/users/:id", "Parameter")
|
||||
router.Add("GET", "/images/*path", "Wildcard")
|
||||
|
||||
data, params := router.Lookup("GET", "/")
|
||||
assert.Equal(t, len(params), 0)
|
||||
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")
|
||||
assert.Equal(t, len(params), 1)
|
||||
assert.Equal(t, params[0].Key, "path")
|
||||
|
49
treeNode.go
49
treeNode.go
@ -1,6 +1,8 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@ -228,6 +230,7 @@ func (node *treeNode[T]) append(path string, data T) {
|
||||
|
||||
// end is called when the node was fully parsed
|
||||
// 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) {
|
||||
char := path[i]
|
||||
|
||||
@ -250,7 +253,7 @@ func (node *treeNode[T]) end(path string, data T, i int, offset int) (*treeNode[
|
||||
|
||||
// node: /user/|:id
|
||||
// path: /user/|:id/profile
|
||||
if node.parameter != nil {
|
||||
if node.parameter != nil && path[i] == parameter {
|
||||
node = node.parameter
|
||||
offset = i
|
||||
return node, offset, controlBegin
|
||||
@ -280,3 +283,47 @@ func (node *treeNode[T]) each(callback func(*treeNode[T])) {
|
||||
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