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) {
tree := router.Tree[string]{}
router := router.New[string]()
routes := loadRoutes("testdata/github.txt")
for _, route := range routes {
tree.Add(route, "")
router.Add(route.method, route.path, "")
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, route := range routes {
tree.Lookup(route)
router.Lookup(route.method, route.path)
}
}
}
func BenchmarkLookupNoAlloc(b *testing.B) {
tree := router.Tree[string]{}
router := router.New[string]()
routes := loadRoutes("testdata/github.txt")
addParameter := func(string, string) {}
for _, route := range routes {
tree.Add(route, "")
router.Add(route.method, route.path, "")
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
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.
## 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
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
BenchmarkLookupNoAlloc-12 148033 7993 ns/op 0 B/op 0 allocs/op
BenchmarkLookup-12 30147 39105 ns/op 19488 B/op 337 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"
)
type route struct {
method string
path string
}
func TestHello(t *testing.T) {
tree := router.Tree[string]{}
router := router.New[string]()
tree.Add("/hello", "Hello")
tree.Add("/world", "World")
router.Add("GET", "/hello", "Hello")
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, data, "Hello")
data, params = tree.Lookup("/world")
data, params = router.Lookup("GET", "/world")
assert.Equal(t, len(params), 0)
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, data, "")
}
func TestParams(t *testing.T) {
tree := router.Tree[string]{}
router := router.New[string]()
tree.Add("/blog/:slug", "Blog post")
tree.Add("/blog/:slug/comments/:id", "Comment")
router.Add("GET", "/blog/:slug", "Blog post")
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, params[0].Key, "slug")
assert.Equal(t, params[0].Value, "hello-world")
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, params[0].Key, "slug")
assert.Equal(t, params[0].Value, "hello-world")
@ -51,34 +56,38 @@ func TestParams(t *testing.T) {
}
func TestGitHub(t *testing.T) {
tree := router.Tree[string]{}
tree := router.New[string]()
routes := loadRoutes("testdata/github.txt")
for _, route := range routes {
tree.Add(route, "octocat")
tree.Add(route.method, route.path, "octocat")
}
for _, route := range routes {
data, _ := tree.Lookup(route)
data, _ := tree.Lookup(route.method, route.path)
assert.Equal(t, data, "octocat")
}
}
func loadRoutes(fileName string) []string {
var routes []string
func loadRoutes(fileName string) []route {
var routes []route
for route := range routesInFile(fileName) {
routes = append(routes, route)
for line := range linesInFile(fileName) {
parts := strings.Split(line, " ")
routes = append(routes, route{
method: parts[0],
path: parts[1],
})
}
return routes
}
func routesInFile(fileName string) <-chan string {
routes := make(chan string)
func linesInFile(fileName string) <-chan string {
lines := make(chan string)
go func() {
defer close(routes)
defer close(lines)
file, err := os.Open(fileName)
if err != nil {
@ -89,9 +98,9 @@ func routesInFile(fileName string) <-chan string {
scanner := bufio.NewScanner(file)
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
/authorizations/:id
/applications/:client_id/tokens/:access_token
/events
/repos/:owner/:repo/events
/networks/:owner/:repo/events
/orgs/:org/events
/users/:user/received_events
/users/:user/received_events/public
/users/:user/events
/users/:user/events/public
/users/:user/events/orgs/:org
/feeds
/notifications
/repos/:owner/:repo/notifications
/notifications/threads/:id
/notifications/threads/:id/subscription
/repos/:owner/:repo/stargazers
/users/:user/starred
/user/starred
/user/starred/:owner/:repo
/repos/:owner/:repo/subscribers
/users/:user/subscriptions
/user/subscriptions
/repos/:owner/:repo/subscription
/user/subscriptions/:owner/:repo
/users/:user/gists
/gists
/gists/:id
/gists/:id/star
/repos/:owner/:repo/git/blobs/:sha
/repos/:owner/:repo/git/commits/:sha
/repos/:owner/:repo/git/refs
/repos/:owner/:repo/git/tags/:sha
/repos/:owner/:repo/git/trees/:sha
/issues
/user/issues
/orgs/:org/issues
/repos/:owner/:repo/issues
/repos/:owner/:repo/issues/:number
/repos/:owner/:repo/assignees
/repos/:owner/:repo/assignees/:assignee
/repos/:owner/:repo/issues/:number/comments
/repos/:owner/:repo/issues/:number/events
/repos/:owner/:repo/labels
/repos/:owner/:repo/labels/:name
/repos/:owner/:repo/issues/:number/labels
/repos/:owner/:repo/milestones/:number/labels
/repos/:owner/:repo/milestones
/repos/:owner/:repo/milestones/:number
/emojis
/gitignore/templates
/gitignore/templates/:name
/meta
/rate_limit
/users/:user/orgs
/user/orgs
/orgs/:org
/orgs/:org/members
/orgs/:org/members/:user
/orgs/:org/public_members
/orgs/:org/public_members/:user
/orgs/:org/teams
/teams/:id
/teams/:id/members
/teams/:id/members/:user
/teams/:id/repos
/teams/:id/repos/:owner/:repo
/user/teams
/repos/:owner/:repo/pulls
/repos/:owner/:repo/pulls/:number
/repos/:owner/:repo/pulls/:number/commits
/repos/:owner/:repo/pulls/:number/files
/repos/:owner/:repo/pulls/:number/merge
/repos/:owner/:repo/pulls/:number/comments
/user/repos
/users/:user/repos
/orgs/:org/repos
/repositories
/repos/:owner/:repo
/repos/:owner/:repo/contributors
/repos/:owner/:repo/languages
/repos/:owner/:repo/teams
/repos/:owner/:repo/tags
/repos/:owner/:repo/branches
/repos/:owner/:repo/branches/:branch
/repos/:owner/:repo/collaborators
/repos/:owner/:repo/collaborators/:user
/repos/:owner/:repo/comments
/repos/:owner/:repo/commits/:sha/comments
/repos/:owner/:repo/comments/:id
/repos/:owner/:repo/commits
/repos/:owner/:repo/commits/:sha
/repos/:owner/:repo/readme
/repos/:owner/:repo/keys
/repos/:owner/:repo/keys/:id
/repos/:owner/:repo/downloads
/repos/:owner/:repo/downloads/:id
/repos/:owner/:repo/forks
/repos/:owner/:repo/hooks
/repos/:owner/:repo/hooks/:id
/repos/:owner/:repo/releases
/repos/:owner/:repo/releases/:id
/repos/:owner/:repo/releases/:id/assets
/repos/:owner/:repo/stats/contributors
/repos/:owner/:repo/stats/commit_activity
/repos/:owner/:repo/stats/code_frequency
/repos/:owner/:repo/stats/participation
/repos/:owner/:repo/stats/punch_card
/repos/:owner/:repo/statuses/:ref
/search/repositories
/search/code
/search/issues
/search/users
/legacy/issues/search/:owner/:repository/:state/:keyword
/legacy/repos/search/:keyword
/legacy/user/search/:keyword
/legacy/user/email/:email
/users/:user
/user
/users
/user/emails
/users/:user/followers
/user/followers
/users/:user/following
/user/following
/user/following/:user
/users/:user/following/:target_user
/users/:user/keys
/user/keys
/user/keys/:id
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