From e5b0eb443aa3d0c9c3079368e4ce0217c2166124 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 1 Sep 2023 12:43:36 +0200 Subject: [PATCH] Improved code quality --- .editorconfig | 9 --- .gitignore | 21 ++---- LICENSE | 9 --- README.md | 61 +++++++++++------- Router_test.go | 81 +++++++++--------------- Benchmarks_test.go => benchmarks_test.go | 8 +-- go.mod | 4 +- go.sum | 4 +- helper_test.go | 52 +++++++++++++++ treeNode.go | 2 +- 10 files changed, 131 insertions(+), 120 deletions(-) delete mode 100644 .editorconfig delete mode 100644 LICENSE rename Benchmarks_test.go => benchmarks_test.go (77%) create mode 100644 helper_test.go diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index c3cec26..0000000 --- a/.editorconfig +++ /dev/null @@ -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 \ No newline at end of file diff --git a/.gitignore b/.gitignore index c6b8d74..56312f2 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 40d5bcd..0000000 --- a/LICENSE +++ /dev/null @@ -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. diff --git a/README.md b/README.md index ede58d0..9854c92 100644 --- a/README.md +++ b/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. \ No newline at end of file +## Copyright + +© 2023 Eduard Urbach diff --git a/Router_test.go b/Router_test.go index 09ac5b8..e88afe6 100644 --- a/Router_test.go +++ b/Router_test.go @@ -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 -} diff --git a/Benchmarks_test.go b/benchmarks_test.go similarity index 77% rename from Benchmarks_test.go rename to benchmarks_test.go index 2170ae0..5182c3b 100644 --- a/Benchmarks_test.go +++ b/benchmarks_test.go @@ -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) } } diff --git a/go.mod b/go.mod index d40e368..5879d15 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 33f82ea..9fc2547 100644 --- a/go.sum +++ b/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= diff --git a/helper_test.go b/helper_test.go new file mode 100644 index 0000000..1ad5d3c --- /dev/null +++ b/helper_test.go @@ -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 +} diff --git a/treeNode.go b/treeNode.go index 7857ca2..18af73c 100644 --- a/treeNode.go +++ b/treeNode.go @@ -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 := ""