Improved code quality
This commit is contained in:
parent
48545fdf01
commit
e5b0eb443a
@ -1,9 +0,0 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
21
.gitignore
vendored
21
.gitignore
vendored
@ -1,17 +1,8 @@
|
||||
# Ignore everything
|
||||
*
|
||||
|
||||
# But not these files...
|
||||
!/.gitignore
|
||||
!/.editorconfig
|
||||
|
||||
!*.go
|
||||
!*.txt
|
||||
!go.sum
|
||||
!go.mod
|
||||
|
||||
!README.md
|
||||
!LICENSE
|
||||
|
||||
# ...even if they are in subdirectories
|
||||
!*/
|
||||
!.gitignore
|
||||
!*.go
|
||||
!*.md
|
||||
!*.mod
|
||||
!*.sum
|
||||
!*.txt
|
||||
|
9
LICENSE
9
LICENSE
@ -1,9 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Eduard Urbach
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
61
README.md
61
README.md
@ -2,6 +2,12 @@
|
||||
|
||||
HTTP router based on radix trees.
|
||||
|
||||
## Features
|
||||
|
||||
- Efficient lookup
|
||||
- Generic data structure
|
||||
- Zero dependencies (excluding tests)
|
||||
|
||||
## Installation
|
||||
|
||||
```shell
|
||||
@ -10,45 +16,52 @@ go get git.akyoto.dev/go/router
|
||||
|
||||
## Usage
|
||||
|
||||
### 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]()
|
||||
|
||||
router.Add("GET", "/hello", "Hello")
|
||||
router.Add("GET", "/world", "World")
|
||||
```
|
||||
// Static routes
|
||||
router.Add("GET", "/hello", "...")
|
||||
router.Add("GET", "/world", "...")
|
||||
|
||||
### Parameters
|
||||
|
||||
The router supports parameters:
|
||||
|
||||
```go
|
||||
// Parameter routes
|
||||
router.Add("GET", "/users/:id", "...")
|
||||
router.Add("GET", "/users/:id/comments", "...")
|
||||
|
||||
// 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)
|
||||
})
|
||||
```
|
||||
|
||||
### Wildcards
|
||||
## Tests
|
||||
|
||||
The router can also fall back to a catch-all route which is useful for file servers:
|
||||
|
||||
```go
|
||||
router.Add("GET", "/images/*path", "...")
|
||||
```
|
||||
PASS: TestStatic
|
||||
PASS: TestParameter
|
||||
PASS: TestWildcard
|
||||
PASS: TestMethods
|
||||
PASS: TestGitHub
|
||||
coverage: 76.9% of statements
|
||||
```
|
||||
|
||||
## Benchmarks
|
||||
|
||||
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
|
||||
BenchmarkLookup-12 6965749 171.2 ns/op 96 B/op 2 allocs/op
|
||||
BenchmarkLookupNoAlloc-12 24243546 48.5 ns/op 0 B/op 0 allocs/op
|
||||
```
|
||||
|
||||
## Embedding
|
||||
## License
|
||||
|
||||
If you'd like to embed this router into your own framework, please use `LookupNoAlloc` because it's much faster than `Lookup`.
|
||||
Please see the [license documentation](https://akyoto.dev/license).
|
||||
|
||||
To build an http server you would of course store request handlers (functions), not strings.
|
||||
## Copyright
|
||||
|
||||
© 2023 Eduard Urbach
|
||||
|
@ -1,23 +1,14 @@
|
||||
package router_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.akyoto.dev/go/assert"
|
||||
"git.akyoto.dev/go/router"
|
||||
)
|
||||
|
||||
type route struct {
|
||||
method string
|
||||
path string
|
||||
}
|
||||
|
||||
func TestHello(t *testing.T) {
|
||||
func TestStatic(t *testing.T) {
|
||||
router := router.New[string]()
|
||||
|
||||
router.Add("GET", "/hello", "Hello")
|
||||
router.Add("GET", "/world", "World")
|
||||
|
||||
@ -34,9 +25,8 @@ func TestHello(t *testing.T) {
|
||||
assert.Equal(t, data, "")
|
||||
}
|
||||
|
||||
func TestParam(t *testing.T) {
|
||||
func TestParameter(t *testing.T) {
|
||||
router := router.New[string]()
|
||||
|
||||
router.Add("GET", "/blog/:slug", "Blog post")
|
||||
router.Add("GET", "/blog/:slug/comments/:id", "Comment")
|
||||
|
||||
@ -57,7 +47,6 @@ func TestParam(t *testing.T) {
|
||||
|
||||
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")
|
||||
@ -86,52 +75,40 @@ func TestWildcard(t *testing.T) {
|
||||
assert.Equal(t, data, "Wildcard")
|
||||
}
|
||||
|
||||
func TestMethods(t *testing.T) {
|
||||
router := router.New[string]()
|
||||
methods := []string{
|
||||
"GET",
|
||||
"POST",
|
||||
"DELETE",
|
||||
"PUT",
|
||||
"PATCH",
|
||||
"HEAD",
|
||||
"CONNECT",
|
||||
"TRACE",
|
||||
"OPTIONS",
|
||||
}
|
||||
|
||||
for _, method := range methods {
|
||||
router.Add(method, "/", method)
|
||||
}
|
||||
|
||||
for _, method := range methods {
|
||||
data, _ := router.Lookup(method, "/")
|
||||
assert.Equal(t, data, method)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitHub(t *testing.T) {
|
||||
tree := router.New[string]()
|
||||
router := router.New[string]()
|
||||
routes := loadRoutes("testdata/github.txt")
|
||||
|
||||
for _, route := range routes {
|
||||
tree.Add(route.method, route.path, "octocat")
|
||||
router.Add(route.method, route.path, "octocat")
|
||||
}
|
||||
|
||||
for _, route := range routes {
|
||||
data, _ := tree.Lookup(route.method, route.path)
|
||||
data, _ := router.Lookup(route.method, route.path)
|
||||
assert.Equal(t, data, "octocat")
|
||||
}
|
||||
}
|
||||
|
||||
func loadRoutes(fileName string) []route {
|
||||
var routes []route
|
||||
|
||||
for line := range linesInFile(fileName) {
|
||||
parts := strings.Split(line, " ")
|
||||
routes = append(routes, route{
|
||||
method: parts[0],
|
||||
path: parts[1],
|
||||
})
|
||||
}
|
||||
|
||||
return routes
|
||||
}
|
||||
|
||||
func linesInFile(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 <- strings.TrimSpace(scanner.Text())
|
||||
}
|
||||
}()
|
||||
|
||||
return lines
|
||||
}
|
||||
|
@ -17,9 +17,7 @@ func BenchmarkLookup(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, route := range routes {
|
||||
router.Lookup(route.method, route.path)
|
||||
}
|
||||
router.Lookup("GET", "/repos/:owner/:repo/issues")
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,8 +33,6 @@ func BenchmarkLookupNoAlloc(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, route := range routes {
|
||||
router.LookupNoAlloc(route.method, route.path, addParameter)
|
||||
}
|
||||
router.LookupNoAlloc("GET", "/repos/:owner/:repo/issues", addParameter)
|
||||
}
|
||||
}
|
4
go.mod
4
go.mod
@ -1,5 +1,5 @@
|
||||
module git.akyoto.dev/go/router
|
||||
|
||||
go 1.20
|
||||
go 1.21
|
||||
|
||||
require git.akyoto.dev/go/assert v0.1.2
|
||||
require git.akyoto.dev/go/assert v0.1.3
|
||||
|
4
go.sum
4
go.sum
@ -1,2 +1,2 @@
|
||||
git.akyoto.dev/go/assert v0.1.2 h1:3paz/5z/JcGK/2K9J+pVh5Jwt2gYfJQG+P5OE9/jB7Y=
|
||||
git.akyoto.dev/go/assert v0.1.2/go.mod h1:Zr/UFuiqmqRmFFgpBGwF71jbzb6iYJfXFeePYHGtWsg=
|
||||
git.akyoto.dev/go/assert v0.1.3 h1:QwCUbmG4aZYsNk/OuRBz1zWVKmGlDUHhOnnDBfn8Qw8=
|
||||
git.akyoto.dev/go/assert v0.1.3/go.mod h1:0GzMaM0eURuDwtGkJJkCsI7r2aUKr+5GmWNTFPgDocM=
|
||||
|
52
helper_test.go
Normal file
52
helper_test.go
Normal file
@ -0,0 +1,52 @@
|
||||
package router_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// route represents a single line in the router test file.
|
||||
type route struct {
|
||||
method string
|
||||
path string
|
||||
}
|
||||
|
||||
// loadRoutes loads all routes from a text file.
|
||||
func loadRoutes(fileName string) []route {
|
||||
var routes []route
|
||||
|
||||
for line := range linesInFile(fileName) {
|
||||
line = strings.TrimSpace(line)
|
||||
parts := strings.Split(line, " ")
|
||||
routes = append(routes, route{
|
||||
method: parts[0],
|
||||
path: parts[1],
|
||||
})
|
||||
}
|
||||
|
||||
return routes
|
||||
}
|
||||
|
||||
// linesInFile is a utility function to easily read every line in a text file.
|
||||
func linesInFile(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
|
||||
}
|
@ -289,7 +289,7 @@ func (node *treeNode[T]) PrettyPrint(writer io.Writer) {
|
||||
node.prettyPrint(writer, -1)
|
||||
}
|
||||
|
||||
// prettyPrint
|
||||
// prettyPrint is the underlying pretty printer.
|
||||
func (node *treeNode[T]) prettyPrint(writer io.Writer, level int) {
|
||||
prefix := ""
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user