Improved code quality

This commit is contained in:
Eduard Urbach 2023-09-01 12:43:36 +02:00
parent 48545fdf01
commit e5b0eb443a
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
10 changed files with 131 additions and 120 deletions

View File

@ -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
View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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
}

View File

@ -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
View File

@ -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
View File

@ -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
View 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
}

View File

@ -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 := ""