Added Router type

This commit is contained in:
Eduard Urbach 2023-07-09 21:24:24 +02:00
parent a05f4b2e36
commit 12b5b4a799
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
5 changed files with 343 additions and 163 deletions

View File

@ -7,32 +7,36 @@ import (
) )
func BenchmarkLookup(b *testing.B) { func BenchmarkLookup(b *testing.B) {
tree := router.Tree[string]{} router := router.New[string]()
routes := loadRoutes("testdata/github.txt") routes := loadRoutes("testdata/github.txt")
for _, route := range routes { for _, route := range routes {
tree.Add(route, "") router.Add(route.method, route.path, "")
} }
b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
for _, route := range routes { for _, route := range routes {
tree.Lookup(route) router.Lookup(route.method, route.path)
} }
} }
} }
func BenchmarkLookupNoAlloc(b *testing.B) { func BenchmarkLookupNoAlloc(b *testing.B) {
tree := router.Tree[string]{} router := router.New[string]()
routes := loadRoutes("testdata/github.txt") routes := loadRoutes("testdata/github.txt")
addParameter := func(string, string) {} addParameter := func(string, string) {}
for _, route := range routes { for _, route := range routes {
tree.Add(route, "") router.Add(route.method, route.path, "")
} }
b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
for _, route := range routes { for _, route := range routes {
tree.LookupNoAlloc(route, addParameter) router.LookupNoAlloc(route.method, route.path, addParameter)
} }
} }
} }

View File

@ -2,11 +2,22 @@
HTTP router based on radix trees. HTTP router based on radix trees.
## Example
We can save any type of data inside the router. Here is an example storing strings for each route:
```go
router := router.New[string]()
router.Add("GET", "/hello", "Hello")
router.Add("GET", "/world", "World")
```
## Benchmarks ## Benchmarks
Loading and requesting every single route in [github.txt](testdata/github.txt): Requesting every single route in [github.txt](testdata/github.txt):
``` ```
BenchmarkLookup-12 50715 23207 ns/op 11649 B/op 204 allocs/op BenchmarkLookup-12 30147 39105 ns/op 19488 B/op 337 allocs/op
BenchmarkLookupNoAlloc-12 148033 7993 ns/op 0 B/op 0 allocs/op BenchmarkLookupNoAlloc-12 85166 14411 ns/op 0 B/op 0 allocs/op
``` ```

84
Router.go Normal file
View File

@ -0,0 +1,84 @@
package router
// Router is a high-performance router.
type Router[T comparable] struct {
get Tree[T]
post Tree[T]
delete Tree[T]
put Tree[T]
patch Tree[T]
head Tree[T]
connect Tree[T]
trace Tree[T]
options Tree[T]
}
// New creates a new router containing trees for every HTTP method.
func New[T comparable]() *Router[T] {
return &Router[T]{}
}
// Add registers a new handler for the given method and path.
func (router *Router[T]) Add(method string, path string, handler T) {
tree := router.selectTree(method)
tree.Add(path, handler)
}
// Lookup finds the handler and parameters for the given route.
func (router *Router[T]) Lookup(method string, path string) (T, []Parameter) {
if method[0] == 'G' {
return router.get.Lookup(path)
}
tree := router.selectTree(method)
return tree.Lookup(path)
}
// LookupNoAlloc finds the handler and parameters for the given route without using any memory allocations.
func (router *Router[T]) LookupNoAlloc(method string, path string, addParameter func(string, string)) T {
if method[0] == 'G' {
return router.get.LookupNoAlloc(path, addParameter)
}
tree := router.selectTree(method)
return tree.LookupNoAlloc(path, addParameter)
}
// Bind traverses all trees and calls the given function on every node.
func (router *Router[T]) Bind(transform func(T) T) {
router.get.Bind(transform)
router.post.Bind(transform)
router.delete.Bind(transform)
router.put.Bind(transform)
router.patch.Bind(transform)
router.head.Bind(transform)
router.connect.Bind(transform)
router.trace.Bind(transform)
router.options.Bind(transform)
}
// selectTree returns the tree by the given HTTP method.
func (router *Router[T]) selectTree(method string) *Tree[T] {
switch method {
case "GET":
return &router.get
case "POST":
return &router.post
case "DELETE":
return &router.delete
case "PUT":
return &router.put
case "PATCH":
return &router.patch
case "HEAD":
return &router.head
case "CONNECT":
return &router.connect
case "TRACE":
return &router.trace
case "OPTIONS":
return &router.options
default:
return nil
}
}

View File

@ -10,38 +10,43 @@ import (
"git.akyoto.dev/go/router" "git.akyoto.dev/go/router"
) )
type route struct {
method string
path string
}
func TestHello(t *testing.T) { func TestHello(t *testing.T) {
tree := router.Tree[string]{} router := router.New[string]()
tree.Add("/hello", "Hello") router.Add("GET", "/hello", "Hello")
tree.Add("/world", "World") router.Add("GET", "/world", "World")
data, params := tree.Lookup("/hello") data, params := router.Lookup("GET", "/hello")
assert.Equal(t, len(params), 0) assert.Equal(t, len(params), 0)
assert.Equal(t, data, "Hello") assert.Equal(t, data, "Hello")
data, params = tree.Lookup("/world") data, params = router.Lookup("GET", "/world")
assert.Equal(t, len(params), 0) assert.Equal(t, len(params), 0)
assert.Equal(t, data, "World") assert.Equal(t, data, "World")
data, params = tree.Lookup("/404") data, params = router.Lookup("GET", "/404")
assert.Equal(t, len(params), 0) assert.Equal(t, len(params), 0)
assert.Equal(t, data, "") assert.Equal(t, data, "")
} }
func TestParams(t *testing.T) { func TestParams(t *testing.T) {
tree := router.Tree[string]{} router := router.New[string]()
tree.Add("/blog/:slug", "Blog post") router.Add("GET", "/blog/:slug", "Blog post")
tree.Add("/blog/:slug/comments/:id", "Comment") router.Add("GET", "/blog/:slug/comments/:id", "Comment")
data, params := tree.Lookup("/blog/hello-world") data, params := router.Lookup("GET", "/blog/hello-world")
assert.Equal(t, len(params), 1) assert.Equal(t, len(params), 1)
assert.Equal(t, params[0].Key, "slug") assert.Equal(t, params[0].Key, "slug")
assert.Equal(t, params[0].Value, "hello-world") assert.Equal(t, params[0].Value, "hello-world")
assert.Equal(t, data, "Blog post") assert.Equal(t, data, "Blog post")
data, params = tree.Lookup("/blog/hello-world/comments/123") data, params = router.Lookup("GET", "/blog/hello-world/comments/123")
assert.Equal(t, len(params), 2) assert.Equal(t, len(params), 2)
assert.Equal(t, params[0].Key, "slug") assert.Equal(t, params[0].Key, "slug")
assert.Equal(t, params[0].Value, "hello-world") assert.Equal(t, params[0].Value, "hello-world")
@ -51,34 +56,38 @@ func TestParams(t *testing.T) {
} }
func TestGitHub(t *testing.T) { func TestGitHub(t *testing.T) {
tree := router.Tree[string]{} tree := router.New[string]()
routes := loadRoutes("testdata/github.txt") routes := loadRoutes("testdata/github.txt")
for _, route := range routes { for _, route := range routes {
tree.Add(route, "octocat") tree.Add(route.method, route.path, "octocat")
} }
for _, route := range routes { for _, route := range routes {
data, _ := tree.Lookup(route) data, _ := tree.Lookup(route.method, route.path)
assert.Equal(t, data, "octocat") assert.Equal(t, data, "octocat")
} }
} }
func loadRoutes(fileName string) []string { func loadRoutes(fileName string) []route {
var routes []string var routes []route
for route := range routesInFile(fileName) { for line := range linesInFile(fileName) {
routes = append(routes, route) parts := strings.Split(line, " ")
routes = append(routes, route{
method: parts[0],
path: parts[1],
})
} }
return routes return routes
} }
func routesInFile(fileName string) <-chan string { func linesInFile(fileName string) <-chan string {
routes := make(chan string) lines := make(chan string)
go func() { go func() {
defer close(routes) defer close(lines)
file, err := os.Open(fileName) file, err := os.Open(fileName)
if err != nil { if err != nil {
@ -89,9 +98,9 @@ func routesInFile(fileName string) <-chan string {
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
for scanner.Scan() { for scanner.Scan() {
routes <- strings.TrimSpace(scanner.Text()) lines <- strings.TrimSpace(scanner.Text())
} }
}() }()
return routes return lines
} }

334
testdata/github.txt vendored
View File

@ -1,131 +1,203 @@
/authorizations GET /authorizations
/authorizations/:id GET /authorizations/:id
/applications/:client_id/tokens/:access_token POST /authorizations
/events DELETE /authorizations/:id
/repos/:owner/:repo/events GET /applications/:client_id/tokens/:access_token
/networks/:owner/:repo/events DELETE /applications/:client_id/tokens
/orgs/:org/events DELETE /applications/:client_id/tokens/:access_token
/users/:user/received_events GET /events
/users/:user/received_events/public GET /repos/:owner/:repo/events
/users/:user/events GET /networks/:owner/:repo/events
/users/:user/events/public GET /orgs/:org/events
/users/:user/events/orgs/:org GET /users/:user/received_events
/feeds GET /users/:user/received_events/public
/notifications GET /users/:user/events
/repos/:owner/:repo/notifications GET /users/:user/events/public
/notifications/threads/:id GET /users/:user/events/orgs/:org
/notifications/threads/:id/subscription GET /feeds
/repos/:owner/:repo/stargazers GET /notifications
/users/:user/starred GET /repos/:owner/:repo/notifications
/user/starred PUT /notifications
/user/starred/:owner/:repo PUT /repos/:owner/:repo/notifications
/repos/:owner/:repo/subscribers GET /notifications/threads/:id
/users/:user/subscriptions GET /notifications/threads/:id/subscription
/user/subscriptions PUT /notifications/threads/:id/subscription
/repos/:owner/:repo/subscription DELETE /notifications/threads/:id/subscription
/user/subscriptions/:owner/:repo GET /repos/:owner/:repo/stargazers
/users/:user/gists GET /users/:user/starred
/gists GET /user/starred
/gists/:id GET /user/starred/:owner/:repo
/gists/:id/star PUT /user/starred/:owner/:repo
/repos/:owner/:repo/git/blobs/:sha DELETE /user/starred/:owner/:repo
/repos/:owner/:repo/git/commits/:sha GET /repos/:owner/:repo/subscribers
/repos/:owner/:repo/git/refs GET /users/:user/subscriptions
/repos/:owner/:repo/git/tags/:sha GET /user/subscriptions
/repos/:owner/:repo/git/trees/:sha GET /repos/:owner/:repo/subscription
/issues PUT /repos/:owner/:repo/subscription
/user/issues DELETE /repos/:owner/:repo/subscription
/orgs/:org/issues GET /user/subscriptions/:owner/:repo
/repos/:owner/:repo/issues PUT /user/subscriptions/:owner/:repo
/repos/:owner/:repo/issues/:number DELETE /user/subscriptions/:owner/:repo
/repos/:owner/:repo/assignees GET /users/:user/gists
/repos/:owner/:repo/assignees/:assignee GET /gists
/repos/:owner/:repo/issues/:number/comments GET /gists/:id
/repos/:owner/:repo/issues/:number/events POST /gists
/repos/:owner/:repo/labels PUT /gists/:id/star
/repos/:owner/:repo/labels/:name DELETE /gists/:id/star
/repos/:owner/:repo/issues/:number/labels GET /gists/:id/star
/repos/:owner/:repo/milestones/:number/labels POST /gists/:id/forks
/repos/:owner/:repo/milestones DELETE /gists/:id
/repos/:owner/:repo/milestones/:number GET /repos/:owner/:repo/git/blobs/:sha
/emojis POST /repos/:owner/:repo/git/blobs
/gitignore/templates GET /repos/:owner/:repo/git/commits/:sha
/gitignore/templates/:name POST /repos/:owner/:repo/git/commits
/meta GET /repos/:owner/:repo/git/refs
/rate_limit POST /repos/:owner/:repo/git/refs
/users/:user/orgs GET /repos/:owner/:repo/git/tags/:sha
/user/orgs POST /repos/:owner/:repo/git/tags
/orgs/:org GET /repos/:owner/:repo/git/trees/:sha
/orgs/:org/members POST /repos/:owner/:repo/git/trees
/orgs/:org/members/:user GET /issues
/orgs/:org/public_members GET /user/issues
/orgs/:org/public_members/:user GET /orgs/:org/issues
/orgs/:org/teams GET /repos/:owner/:repo/issues
/teams/:id GET /repos/:owner/:repo/issues/:number
/teams/:id/members POST /repos/:owner/:repo/issues
/teams/:id/members/:user GET /repos/:owner/:repo/assignees
/teams/:id/repos GET /repos/:owner/:repo/assignees/:assignee
/teams/:id/repos/:owner/:repo GET /repos/:owner/:repo/issues/:number/comments
/user/teams POST /repos/:owner/:repo/issues/:number/comments
/repos/:owner/:repo/pulls GET /repos/:owner/:repo/issues/:number/events
/repos/:owner/:repo/pulls/:number GET /repos/:owner/:repo/labels
/repos/:owner/:repo/pulls/:number/commits GET /repos/:owner/:repo/labels/:name
/repos/:owner/:repo/pulls/:number/files POST /repos/:owner/:repo/labels
/repos/:owner/:repo/pulls/:number/merge DELETE /repos/:owner/:repo/labels/:name
/repos/:owner/:repo/pulls/:number/comments GET /repos/:owner/:repo/issues/:number/labels
/user/repos POST /repos/:owner/:repo/issues/:number/labels
/users/:user/repos DELETE /repos/:owner/:repo/issues/:number/labels/:name
/orgs/:org/repos PUT /repos/:owner/:repo/issues/:number/labels
/repositories DELETE /repos/:owner/:repo/issues/:number/labels
/repos/:owner/:repo GET /repos/:owner/:repo/milestones/:number/labels
/repos/:owner/:repo/contributors GET /repos/:owner/:repo/milestones
/repos/:owner/:repo/languages GET /repos/:owner/:repo/milestones/:number
/repos/:owner/:repo/teams POST /repos/:owner/:repo/milestones
/repos/:owner/:repo/tags DELETE /repos/:owner/:repo/milestones/:number
/repos/:owner/:repo/branches GET /emojis
/repos/:owner/:repo/branches/:branch GET /gitignore/templates
/repos/:owner/:repo/collaborators GET /gitignore/templates/:name
/repos/:owner/:repo/collaborators/:user POST /markdown
/repos/:owner/:repo/comments POST /markdown/raw
/repos/:owner/:repo/commits/:sha/comments GET /meta
/repos/:owner/:repo/comments/:id GET /rate_limit
/repos/:owner/:repo/commits GET /users/:user/orgs
/repos/:owner/:repo/commits/:sha GET /user/orgs
/repos/:owner/:repo/readme GET /orgs/:org
/repos/:owner/:repo/keys GET /orgs/:org/members
/repos/:owner/:repo/keys/:id GET /orgs/:org/members/:user
/repos/:owner/:repo/downloads DELETE /orgs/:org/members/:user
/repos/:owner/:repo/downloads/:id GET /orgs/:org/public_members
/repos/:owner/:repo/forks GET /orgs/:org/public_members/:user
/repos/:owner/:repo/hooks PUT /orgs/:org/public_members/:user
/repos/:owner/:repo/hooks/:id DELETE /orgs/:org/public_members/:user
/repos/:owner/:repo/releases GET /orgs/:org/teams
/repos/:owner/:repo/releases/:id GET /teams/:id
/repos/:owner/:repo/releases/:id/assets POST /orgs/:org/teams
/repos/:owner/:repo/stats/contributors DELETE /teams/:id
/repos/:owner/:repo/stats/commit_activity GET /teams/:id/members
/repos/:owner/:repo/stats/code_frequency GET /teams/:id/members/:user
/repos/:owner/:repo/stats/participation PUT /teams/:id/members/:user
/repos/:owner/:repo/stats/punch_card DELETE /teams/:id/members/:user
/repos/:owner/:repo/statuses/:ref GET /teams/:id/repos
/search/repositories GET /teams/:id/repos/:owner/:repo
/search/code PUT /teams/:id/repos/:owner/:repo
/search/issues DELETE /teams/:id/repos/:owner/:repo
/search/users GET /user/teams
/legacy/issues/search/:owner/:repository/:state/:keyword GET /repos/:owner/:repo/pulls
/legacy/repos/search/:keyword GET /repos/:owner/:repo/pulls/:number
/legacy/user/search/:keyword POST /repos/:owner/:repo/pulls
/legacy/user/email/:email GET /repos/:owner/:repo/pulls/:number/commits
/users/:user GET /repos/:owner/:repo/pulls/:number/files
/user GET /repos/:owner/:repo/pulls/:number/merge
/users PUT /repos/:owner/:repo/pulls/:number/merge
/user/emails GET /repos/:owner/:repo/pulls/:number/comments
/users/:user/followers PUT /repos/:owner/:repo/pulls/:number/comments
/user/followers GET /user/repos
/users/:user/following GET /users/:user/repos
/user/following GET /orgs/:org/repos
/user/following/:user GET /repositories
/users/:user/following/:target_user POST /user/repos
/users/:user/keys POST /orgs/:org/repos
/user/keys GET /repos/:owner/:repo
/user/keys/:id 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