diff --git a/README.md b/README.md index 4847024..a5f075e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,56 @@ # Router -A radix-tree based no-allocation router in Go. \ No newline at end of file +A radix-tree based no-allocation router in Go. All credit to Eduard Urbach for his incredible work. + +## Features + +- Efficient lookup +- Generic data structure +- Zero dependencies + +## Installation + +```shell +go get git.sharkk.net/Go/Router +``` + +## Usage + +```go +router := router.New[string]() + +// Static routes +router.Add("GET", "/hello", "...") +router.Add("GET", "/world", "...") + +// 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) +}) +``` + +## Benchmarks +``` +goos: linux +goarch: amd64 +pkg: git.sharkk.net/Go/Router/tests +cpu: AMD Ryzen 9 7950X 16-Core Processor +BenchmarkBlog/Len1-Param0-32 384992013 3.337 ns/op 0 B/op 0 allocs/op +BenchmarkBlog/Len1-Param1-32 199599014 6.021 ns/op 0 B/op 0 allocs/op +BenchmarkGithub/Len7-Param0-32 256332994 4.678 ns/op 0 B/op 0 allocs/op +BenchmarkGithub/Len7-Param1-32 269038417 4.455 ns/op 0 B/op 0 allocs/op +BenchmarkGithub/Len7-Param2-32 256228226 4.673 ns/op 0 B/op 0 allocs/op +PASS +ok git.sharkk.net/Go/Router/tests 8.410s +``` \ No newline at end of file diff --git a/tests/benchmark_test.go b/tests/benchmark_test.go new file mode 100644 index 0000000..004da16 --- /dev/null +++ b/tests/benchmark_test.go @@ -0,0 +1,106 @@ +package router_test + +import ( + "bufio" + "os" + "strings" + "testing" + + router "git.sharkk.net/Go/Router" +) + +func BenchmarkBlog(b *testing.B) { + routes := routes("blog.txt") + r := router.New[string]() + + for _, route := range routes { + r.Add(route.Method, route.Path, "") + } + + b.Run("Len1-Param0", func(b *testing.B) { + for i := 0; i < b.N; i++ { + r.LookupNoAlloc("GET", "/", noop) + } + }) + + b.Run("Len1-Param1", func(b *testing.B) { + for i := 0; i < b.N; i++ { + r.LookupNoAlloc("GET", "/:id", noop) + } + }) +} + +func BenchmarkGithub(b *testing.B) { + routes := routes("routes.txt") + r := router.New[string]() + + for _, route := range routes { + r.Add(route.Method, route.Path, "") + } + + b.Run("Len7-Param0", func(b *testing.B) { + for i := 0; i < b.N; i++ { + r.LookupNoAlloc("GET", "/issues", noop) + } + }) + + b.Run("Len7-Param1", func(b *testing.B) { + for i := 0; i < b.N; i++ { + r.LookupNoAlloc("GET", "/gists/:id", noop) + } + }) + + b.Run("Len7-Param2", func(b *testing.B) { + for i := 0; i < b.N; i++ { + r.LookupNoAlloc("GET", "/repos/:owner/:repo/issues", noop) + } + }) +} + +// Route represents a single line in the router test file. +type Route struct { + Method string + Path string +} + +// Routes loads all routes from a text file. +func routes(fileName string) []Route { + var routes []Route + + for line := range lines(fileName) { + line = strings.TrimSpace(line) + parts := strings.Split(line, " ") + routes = append(routes, Route{ + Method: parts[0], + Path: parts[1], + }) + } + + return routes +} + +// Lines is a utility function to easily read every line in a text file. +func lines(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 +} + +// noop serves as an empty addParameter function. +func noop(string, string) {} diff --git a/tests/blog.txt b/tests/blog.txt new file mode 100644 index 0000000..755965f --- /dev/null +++ b/tests/blog.txt @@ -0,0 +1,4 @@ +GET / +GET /:slug +GET /tags +GET /tag/:tag \ No newline at end of file diff --git a/tests/github.txt b/tests/github.txt new file mode 100644 index 0000000..fc85456 --- /dev/null +++ b/tests/github.txt @@ -0,0 +1,203 @@ +GET /authorizations +GET /authorizations/:id +POST /authorizations +DELETE /authorizations/:id +GET /applications/:client_id/tokens/:access_token +DELETE /applications/:client_id/tokens +DELETE /applications/:client_id/tokens/:access_token +GET /events +GET /repos/:owner/:repo/events +GET /networks/:owner/:repo/events +GET /orgs/:org/events +GET /users/:user/received_events +GET /users/:user/received_events/public +GET /users/:user/events +GET /users/:user/events/public +GET /users/:user/events/orgs/:org +GET /feeds +GET /notifications +GET /repos/:owner/:repo/notifications +PUT /notifications +PUT /repos/:owner/:repo/notifications +GET /notifications/threads/:id +GET /notifications/threads/:id/subscription +PUT /notifications/threads/:id/subscription +DELETE /notifications/threads/:id/subscription +GET /repos/:owner/:repo/stargazers +GET /users/:user/starred +GET /user/starred +GET /user/starred/:owner/:repo +PUT /user/starred/:owner/:repo +DELETE /user/starred/:owner/:repo +GET /repos/:owner/:repo/subscribers +GET /users/:user/subscriptions +GET /user/subscriptions +GET /repos/:owner/:repo/subscription +PUT /repos/:owner/:repo/subscription +DELETE /repos/:owner/:repo/subscription +GET /user/subscriptions/:owner/:repo +PUT /user/subscriptions/:owner/:repo +DELETE /user/subscriptions/:owner/:repo +GET /users/:user/gists +GET /gists +GET /gists/:id +POST /gists +PUT /gists/:id/star +DELETE /gists/:id/star +GET /gists/:id/star +POST /gists/:id/forks +DELETE /gists/:id +GET /repos/:owner/:repo/git/blobs/:sha +POST /repos/:owner/:repo/git/blobs +GET /repos/:owner/:repo/git/commits/:sha +POST /repos/:owner/:repo/git/commits +GET /repos/:owner/:repo/git/refs +POST /repos/:owner/:repo/git/refs +GET /repos/:owner/:repo/git/tags/:sha +POST /repos/:owner/:repo/git/tags +GET /repos/:owner/:repo/git/trees/:sha +POST /repos/:owner/:repo/git/trees +GET /issues +GET /user/issues +GET /orgs/:org/issues +GET /repos/:owner/:repo/issues +GET /repos/:owner/:repo/issues/:number +POST /repos/:owner/:repo/issues +GET /repos/:owner/:repo/assignees +GET /repos/:owner/:repo/assignees/:assignee +GET /repos/:owner/:repo/issues/:number/comments +POST /repos/:owner/:repo/issues/:number/comments +GET /repos/:owner/:repo/issues/:number/events +GET /repos/:owner/:repo/labels +GET /repos/:owner/:repo/labels/:name +POST /repos/:owner/:repo/labels +DELETE /repos/:owner/:repo/labels/:name +GET /repos/:owner/:repo/issues/:number/labels +POST /repos/:owner/:repo/issues/:number/labels +DELETE /repos/:owner/:repo/issues/:number/labels/:name +PUT /repos/:owner/:repo/issues/:number/labels +DELETE /repos/:owner/:repo/issues/:number/labels +GET /repos/:owner/:repo/milestones/:number/labels +GET /repos/:owner/:repo/milestones +GET /repos/:owner/:repo/milestones/:number +POST /repos/:owner/:repo/milestones +DELETE /repos/:owner/:repo/milestones/:number +GET /emojis +GET /gitignore/templates +GET /gitignore/templates/:name +POST /markdown +POST /markdown/raw +GET /meta +GET /rate_limit +GET /users/:user/orgs +GET /user/orgs +GET /orgs/:org +GET /orgs/:org/members +GET /orgs/:org/members/:user +DELETE /orgs/:org/members/:user +GET /orgs/:org/public_members +GET /orgs/:org/public_members/:user +PUT /orgs/:org/public_members/:user +DELETE /orgs/:org/public_members/:user +GET /orgs/:org/teams +GET /teams/:id +POST /orgs/:org/teams +DELETE /teams/:id +GET /teams/:id/members +GET /teams/:id/members/:user +PUT /teams/:id/members/:user +DELETE /teams/:id/members/:user +GET /teams/:id/repos +GET /teams/:id/repos/:owner/:repo +PUT /teams/:id/repos/:owner/:repo +DELETE /teams/:id/repos/:owner/:repo +GET /user/teams +GET /repos/:owner/:repo/pulls +GET /repos/:owner/:repo/pulls/:number +POST /repos/:owner/:repo/pulls +GET /repos/:owner/:repo/pulls/:number/commits +GET /repos/:owner/:repo/pulls/:number/files +GET /repos/:owner/:repo/pulls/:number/merge +PUT /repos/:owner/:repo/pulls/:number/merge +GET /repos/:owner/:repo/pulls/:number/comments +PUT /repos/:owner/:repo/pulls/:number/comments +GET /user/repos +GET /users/:user/repos +GET /orgs/:org/repos +GET /repositories +POST /user/repos +POST /orgs/:org/repos +GET /repos/:owner/:repo +GET /repos/:owner/:repo/contributors +GET /repos/:owner/:repo/languages +GET /repos/:owner/:repo/teams +GET /repos/:owner/:repo/tags +GET /repos/:owner/:repo/branches +GET /repos/:owner/:repo/branches/:branch +DELETE /repos/:owner/:repo +GET /repos/:owner/:repo/collaborators +GET /repos/:owner/:repo/collaborators/:user +PUT /repos/:owner/:repo/collaborators/:user +DELETE /repos/:owner/:repo/collaborators/:user +GET /repos/:owner/:repo/comments +GET /repos/:owner/:repo/commits/:sha/comments +POST /repos/:owner/:repo/commits/:sha/comments +GET /repos/:owner/:repo/comments/:id +DELETE /repos/:owner/:repo/comments/:id +GET /repos/:owner/:repo/commits +GET /repos/:owner/:repo/commits/:sha +GET /repos/:owner/:repo/readme +GET /repos/:owner/:repo/keys +GET /repos/:owner/:repo/keys/:id +POST /repos/:owner/:repo/keys +DELETE /repos/:owner/:repo/keys/:id +GET /repos/:owner/:repo/downloads +GET /repos/:owner/:repo/downloads/:id +DELETE /repos/:owner/:repo/downloads/:id +GET /repos/:owner/:repo/forks +POST /repos/:owner/:repo/forks +GET /repos/:owner/:repo/hooks +GET /repos/:owner/:repo/hooks/:id +POST /repos/:owner/:repo/hooks +POST /repos/:owner/:repo/hooks/:id/tests +DELETE /repos/:owner/:repo/hooks/:id +POST /repos/:owner/:repo/merges +GET /repos/:owner/:repo/releases +GET /repos/:owner/:repo/releases/:id +POST /repos/:owner/:repo/releases +DELETE /repos/:owner/:repo/releases/:id +GET /repos/:owner/:repo/releases/:id/assets +GET /repos/:owner/:repo/stats/contributors +GET /repos/:owner/:repo/stats/commit_activity +GET /repos/:owner/:repo/stats/code_frequency +GET /repos/:owner/:repo/stats/participation +GET /repos/:owner/:repo/stats/punch_card +GET /repos/:owner/:repo/statuses/:ref +POST /repos/:owner/:repo/statuses/:ref +GET /search/repositories +GET /search/code +GET /search/issues +GET /search/users +GET /legacy/issues/search/:owner/:repository/:state/:keyword +GET /legacy/repos/search/:keyword +GET /legacy/user/search/:keyword +GET /legacy/user/email/:email +GET /users/:user +GET /user +GET /users +GET /user/emails +POST /user/emails +DELETE /user/emails +GET /users/:user/followers +GET /user/followers +GET /users/:user/following +GET /user/following +GET /user/following/:user +GET /users/:user/following/:target_user +PUT /user/following/:user +DELETE /user/following/:user +GET /users/:user/keys +GET /user/keys +GET /user/keys/:id +POST /user/keys +DELETE /user/keys/:id \ No newline at end of file diff --git a/tests/Router_test.go b/tests/router_test.go similarity index 100% rename from tests/Router_test.go rename to tests/router_test.go