diff --git a/Parameter.go b/Parameter.go new file mode 100644 index 0000000..de4f0a5 --- /dev/null +++ b/Parameter.go @@ -0,0 +1,7 @@ +package router + +// Parameter represents a URL parameter. +type Parameter struct { + Key string + Value string +} diff --git a/README.md b/README.md index 724d820..116f444 100644 --- a/README.md +++ b/README.md @@ -59,11 +59,11 @@ coverage: 100.0% of statements ## Benchmarks ``` -BenchmarkBlog/Len1-Param0-12 182590244 6.57 ns/op 0 B/op 0 allocs/op -BenchmarkBlog/Len1-Param1-12 100000000 10.95 ns/op 0 B/op 0 allocs/op -BenchmarkGitHub/Len7-Param0-12 67053636 17.95 ns/op 0 B/op 0 allocs/op -BenchmarkGitHub/Len7-Param1-12 49371550 24.12 ns/op 0 B/op 0 allocs/op -BenchmarkGitHub/Len7-Param2-12 24562465 48.83 ns/op 0 B/op 0 allocs/op +BenchmarkBlog/Len1-Param0-12 200621386 5.963 ns/op 0 B/op 0 allocs/op +BenchmarkBlog/Len1-Param1-12 129550375 9.224 ns/op 0 B/op 0 allocs/op +BenchmarkGitHub/Len7-Param0-12 70562060 16.98 ns/op 0 B/op 0 allocs/op +BenchmarkGitHub/Len7-Param1-12 49366180 22.56 ns/op 0 B/op 0 allocs/op +BenchmarkGitHub/Len7-Param2-12 24332162 47.91 ns/op 0 B/op 0 allocs/op ``` ## License diff --git a/Router.go b/Router.go index d3116a5..3db0647 100644 --- a/Router.go +++ b/Router.go @@ -25,7 +25,7 @@ func (router *Router[T]) Add(method string, path string, handler T) { } // Lookup finds the handler and parameters for the given route. -func (router *Router[T]) Lookup(method string, path string) (T, []keyValue) { +func (router *Router[T]) Lookup(method string, path string) (T, []Parameter) { if method[0] == 'G' { return router.get.Lookup(path) } diff --git a/Router_test.go b/Router_test.go index 66230c6..3f1c2fe 100644 --- a/Router_test.go +++ b/Router_test.go @@ -9,6 +9,20 @@ import ( "git.akyoto.dev/go/router/testdata" ) +func TestHello(t *testing.T) { + r := router.New[string]() + r.Add("GET", "/blog", "Blog") + r.Add("GET", "/blog/post", "Blog post") + + data, params := r.Lookup("GET", "/blog") + assert.Equal(t, len(params), 0) + assert.Equal(t, data, "Blog") + + data, params = r.Lookup("GET", "/blog/post") + assert.Equal(t, len(params), 0) + assert.Equal(t, data, "Blog post") +} + func TestStatic(t *testing.T) { r := router.New[string]() r.Add("GET", "/hello", "Hello") @@ -61,11 +75,12 @@ func TestParameter(t *testing.T) { func TestWildcard(t *testing.T) { r := router.New[string]() r.Add("GET", "/", "Front page") - r.Add("GET", "/:post", "Blog post") - r.Add("GET", "/*any", "Wildcard") r.Add("GET", "/users/:id", "Parameter") r.Add("GET", "/images/static", "Static") r.Add("GET", "/images/*path", "Wildcard") + r.Add("GET", "/:post", "Blog post") + r.Add("GET", "/*any", "Wildcard") + r.Add("GET", "*root", "Root wildcard") data, params := r.Lookup("GET", "/") assert.Equal(t, len(params), 0) @@ -107,6 +122,12 @@ func TestWildcard(t *testing.T) { assert.Equal(t, params[0].Key, "path") assert.Equal(t, params[0].Value, "favicon/256.png") assert.Equal(t, data, "Wildcard") + + data, params = r.Lookup("GET", "not-a-path") + assert.Equal(t, len(params), 1) + assert.Equal(t, params[0].Key, "root") + assert.Equal(t, params[0].Value, "not-a-path") + assert.Equal(t, data, "Root wildcard") } func TestMap(t *testing.T) { @@ -115,6 +136,7 @@ func TestMap(t *testing.T) { r.Add("GET", "/world", "World") r.Add("GET", "/user/:user", "User") r.Add("GET", "/*path", "Path") + r.Add("GET", "*root", "Root") r.Map(func(data string) string { return strings.Repeat(data, 2) @@ -135,6 +157,10 @@ func TestMap(t *testing.T) { data, params = r.Lookup("GET", "/test.txt") assert.Equal(t, len(params), 1) assert.Equal(t, data, "PathPath") + + data, params = r.Lookup("GET", "test.txt") + assert.Equal(t, len(params), 1) + assert.Equal(t, data, "RootRoot") } func TestMethods(t *testing.T) { diff --git a/Tree.go b/Tree.go index 5dfe82b..5f33073 100644 --- a/Tree.go +++ b/Tree.go @@ -79,11 +79,11 @@ func (tree *Tree[T]) Add(path string, data T) { } // Lookup finds the data for the given path. -func (tree *Tree[T]) Lookup(path string) (T, []keyValue) { - var params []keyValue +func (tree *Tree[T]) Lookup(path string) (T, []Parameter) { + var params []Parameter data := tree.LookupNoAlloc(path, func(key string, value string) { - params = append(params, keyValue{key, value}) + params = append(params, Parameter{key, value}) }) return data, params @@ -92,44 +92,28 @@ func (tree *Tree[T]) Lookup(path string) (T, []keyValue) { // LookupNoAlloc finds the data for the given path without using any memory allocations. func (tree *Tree[T]) LookupNoAlloc(path string, addParameter func(key string, value string)) T { var ( - i uint - offset uint - lastWildcardOffset uint - lastWildcard *treeNode[T] - empty T - node = &tree.root + i uint + offset uint + wildcardOffset uint + wildcard *treeNode[T] + node = &tree.root ) + // Skip the first loop iteration if the starting characters are equal + if len(path) > 0 && len(node.prefix) > 0 && path[0] == node.prefix[0] { + i = 1 + } + begin: // Search tree for equal parts until we can no longer proceed - for { - // We reached the end. - if i == uint(len(path)) { - // node: /blog| - // path: /blog| - if i-offset == uint(len(node.prefix)) { - return node.data - } - - // node: /|*any - // path: /|image.png - if lastWildcard != nil { - addParameter(lastWildcard.prefix, path[lastWildcardOffset:]) - return lastWildcard.data - } - - // node: /blog|feed - // path: /blog| - return empty - } - + for i < uint(len(path)) { // The node we just checked is entirely included in our path. // node: /| // path: /|blog if i-offset == uint(len(node.prefix)) { if node.wildcard != nil { - lastWildcard = node.wildcard - lastWildcardOffset = i + wildcard = node.wildcard + wildcardOffset = i } char := path[i] @@ -176,28 +160,35 @@ begin: // node: /|*any // path: /|image.png - if lastWildcard != nil { - addParameter(lastWildcard.prefix, path[lastWildcardOffset:]) - return lastWildcard.data - } - - return empty + goto notFound } // We got a conflict. // node: /b|ag // path: /b|riefcase if path[i] != node.prefix[i-offset] { - if lastWildcard != nil { - addParameter(lastWildcard.prefix, path[lastWildcardOffset:]) - return lastWildcard.data - } - - return empty + goto notFound } i++ } + + // node: /blog| + // path: /blog| + if i-offset == uint(len(node.prefix)) { + return node.data + } + + // node: /|*any + // path: /|image.png +notFound: + if wildcard != nil { + addParameter(wildcard.prefix, path[wildcardOffset:]) + return wildcard.data + } + + var empty T + return empty } // Map binds all handlers to a new one provided by the callback. diff --git a/go.mod b/go.mod index 5879d15..9f71fa8 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,5 @@ module git.akyoto.dev/go/router -go 1.21 +go 1.22 require git.akyoto.dev/go/assert v0.1.3 diff --git a/keyValue.go b/keyValue.go deleted file mode 100644 index def3969..0000000 --- a/keyValue.go +++ /dev/null @@ -1,7 +0,0 @@ -package router - -// keyValue represents a URL parameter. -type keyValue struct { - Key string - Value string -}