Added trailing slash for static routes

This commit is contained in:
Eduard Urbach 2024-03-13 13:04:03 +01:00
parent 3008940025
commit dd98b11eea
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
8 changed files with 119 additions and 96 deletions

View File

@ -50,9 +50,10 @@ PASS: TestMap
PASS: TestMethods
PASS: TestGitHub
PASS: TestTrailingSlash
PASS: TestTrailingSlashOverwrite
PASS: TestOverwrite
PASS: TestInvalidMethod
coverage: 99.1% of statements
coverage: 100.0% of statements
```
## Benchmarks

View File

@ -6,6 +6,7 @@ import (
"git.akyoto.dev/go/assert"
"git.akyoto.dev/go/router"
"git.akyoto.dev/go/router/testdata"
)
func TestStatic(t *testing.T) {
@ -162,18 +163,18 @@ func TestMethods(t *testing.T) {
}
func TestGitHub(t *testing.T) {
routes := loadRoutes("testdata/github.txt")
routes := testdata.Routes("testdata/github.txt")
r := router.New[string]()
for _, route := range routes {
r.Add(route.method, route.path, "octocat")
r.Add(route.Method, route.Path, "octocat")
}
for _, route := range routes {
data, _ := r.Lookup(route.method, route.path)
data, _ := r.Lookup(route.Method, route.Path)
assert.Equal(t, data, "octocat")
data = r.LookupNoAlloc(route.method, route.path, func(string, string) {})
data = r.LookupNoAlloc(route.Method, route.Path, func(string, string) {})
assert.Equal(t, data, "octocat")
}
}
@ -181,7 +182,6 @@ func TestGitHub(t *testing.T) {
func TestTrailingSlash(t *testing.T) {
r := router.New[string]()
r.Add("GET", "/hello", "Hello 1")
r.Add("GET", "/hello/", "Hello 2")
data, params := r.Lookup("GET", "/hello")
assert.Equal(t, len(params), 0)
@ -189,7 +189,35 @@ func TestTrailingSlash(t *testing.T) {
data, params = r.Lookup("GET", "/hello/")
assert.Equal(t, len(params), 0)
assert.Equal(t, data, "Hello 2")
assert.Equal(t, data, "Hello 1")
}
func TestTrailingSlashOverwrite(t *testing.T) {
r := router.New[string]()
r.Add("GET", "/hello", "route 1")
r.Add("GET", "/hello/", "route 2")
r.Add("GET", "/:param", "route 3")
r.Add("GET", "/:param/", "route 4")
r.Add("GET", "/*any", "route 5")
data, params := r.Lookup("GET", "/hello")
assert.Equal(t, len(params), 0)
assert.Equal(t, data, "route 1")
data, params = r.Lookup("GET", "/hello/")
assert.Equal(t, len(params), 0)
assert.Equal(t, data, "route 2")
data, params = r.Lookup("GET", "/param")
assert.Equal(t, len(params), 1)
assert.Equal(t, data, "route 3")
data, params = r.Lookup("GET", "/param/")
assert.Equal(t, len(params), 1)
assert.Equal(t, data, "route 4")
data, _ = r.Lookup("GET", "/wild/card/")
assert.Equal(t, data, "route 5")
}
func TestOverwrite(t *testing.T) {

31
Tree.go
View File

@ -1,15 +1,5 @@
package router
// controlFlow tells the main loop what it should do next.
type controlFlow int
// controlFlow values.
const (
controlStop controlFlow = 0
controlBegin controlFlow = 1
controlNext controlFlow = 2
)
// Tree represents a radix tree.
type Tree[T any] struct {
root treeNode[T]
@ -36,17 +26,8 @@ func (tree *Tree[T]) Add(path string, data T) {
// When we hit a separator, we'll search for a fitting child.
if path[i] == separator {
var control controlFlow
node, offset, control = node.end(path, data, i, offset)
switch control {
case controlStop:
return
case controlBegin:
goto begin
case controlNext:
goto next
}
node, offset, _ = node.end(path, data, i, offset)
goto next
}
default:
@ -70,15 +51,15 @@ func (tree *Tree[T]) Add(path string, data T) {
// node: /|
// path: /|blog
if i-offset == len(node.prefix) {
var control controlFlow
var control flow
node, offset, control = node.end(path, data, i, offset)
switch control {
case controlStop:
case flowStop:
return
case controlBegin:
case flowBegin:
goto begin
case controlNext:
case flowNext:
goto next
}
}

View File

@ -4,14 +4,15 @@ import (
"testing"
"git.akyoto.dev/go/router"
"git.akyoto.dev/go/router/testdata"
)
func BenchmarkBlog(b *testing.B) {
routes := loadRoutes("testdata/blog.txt")
routes := testdata.Routes("testdata/blog.txt")
r := router.New[string]()
for _, route := range routes {
r.Add(route.method, route.path, "")
r.Add(route.Method, route.Path, "")
}
b.Run("Len1-Param0", func(b *testing.B) {
@ -28,11 +29,11 @@ func BenchmarkBlog(b *testing.B) {
}
func BenchmarkGitHub(b *testing.B) {
routes := loadRoutes("testdata/github.txt")
routes := testdata.Routes("testdata/github.txt")
r := router.New[string]()
for _, route := range routes {
r.Add(route.method, route.path, "")
r.Add(route.Method, route.Path, "")
}
b.Run("Len7-Param0", func(b *testing.B) {
@ -53,3 +54,6 @@ func BenchmarkGitHub(b *testing.B) {
}
})
}
// noop serves as an empty addParameter function.
func noop(string, string) {}

11
flow.go Normal file
View File

@ -0,0 +1,11 @@
package router
// flow tells the main loop what it should do next.
type flow int
// Control flow values.
const (
flowStop flow = iota
flowBegin
flowNext
)

View File

@ -1,55 +0,0 @@
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
}
// noop serves as an empty addParameter function.
func noop(string, string) {}

52
testdata/Route.go vendored Normal file
View File

@ -0,0 +1,52 @@
package testdata
import (
"bufio"
"os"
"strings"
)
// 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
}

View File

@ -157,6 +157,7 @@ func (node *treeNode[T]) append(path string, data T) {
if node.prefix == "" {
node.prefix = path
node.data = data
node.addTrailingSlash(data)
return
}
@ -229,7 +230,7 @@ func (node *treeNode[T]) append(path string, data T) {
// end is called when the node was fully parsed
// and needs to decide the next control flow.
// end is only called from `tree.Add`.
func (node *treeNode[T]) end(path string, data T, i int, offset int) (*treeNode[T], int, controlFlow) {
func (node *treeNode[T]) end(path string, data T, i int, offset int) (*treeNode[T], int, flow) {
char := path[i]
if char >= node.startIndex && char < node.endIndex {
@ -238,7 +239,7 @@ func (node *treeNode[T]) end(path string, data T, i int, offset int) (*treeNode[
if index != 0 {
node = node.children[index]
offset = i
return node, offset, controlNext
return node, offset, flowNext
}
}
@ -246,7 +247,7 @@ func (node *treeNode[T]) end(path string, data T, i int, offset int) (*treeNode[
// If no prefix is set, this is the starting node.
if node.prefix == "" {
node.append(path[i:], data)
return node, offset, controlStop
return node, offset, flowStop
}
// node: /user/|:id
@ -254,11 +255,11 @@ func (node *treeNode[T]) end(path string, data T, i int, offset int) (*treeNode[
if node.parameter != nil && path[i] == parameter {
node = node.parameter
offset = i
return node, offset, controlBegin
return node, offset, flowBegin
}
node.append(path[i:], data)
return node, offset, controlStop
return node, offset, flowStop
}
// each traverses the tree and calls the given function on every node.