diff --git a/README.md b/README.md index 368d2f6..46dab4a 100644 --- a/README.md +++ b/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 -``` \ No newline at end of file +``` + +## 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. \ No newline at end of file diff --git a/Router.go b/Router.go index 0e53df1..1a3cb92 100644 --- a/Router.go +++ b/Router.go @@ -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 { diff --git a/Router_test.go b/Router_test.go index 51e68ee..09ac5b8 100644 --- a/Router_test.go +++ b/Router_test.go @@ -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") diff --git a/treeNode.go b/treeNode.go index 4b1eab4..9cdfc3b 100644 --- a/treeNode.go +++ b/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 +}