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: TestMethods
PASS: TestGitHub PASS: TestGitHub
PASS: TestTrailingSlash PASS: TestTrailingSlash
PASS: TestTrailingSlashOverwrite
PASS: TestOverwrite PASS: TestOverwrite
PASS: TestInvalidMethod PASS: TestInvalidMethod
coverage: 99.1% of statements coverage: 100.0% of statements
``` ```
## Benchmarks ## Benchmarks

View File

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

31
Tree.go
View File

@ -1,15 +1,5 @@
package router 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. // Tree represents a radix tree.
type Tree[T any] struct { type Tree[T any] struct {
root treeNode[T] 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. // When we hit a separator, we'll search for a fitting child.
if path[i] == separator { if path[i] == separator {
var control controlFlow node, offset, _ = node.end(path, data, i, offset)
node, offset, control = node.end(path, data, i, offset) goto next
switch control {
case controlStop:
return
case controlBegin:
goto begin
case controlNext:
goto next
}
} }
default: default:
@ -70,15 +51,15 @@ func (tree *Tree[T]) Add(path string, data T) {
// node: /| // node: /|
// path: /|blog // path: /|blog
if i-offset == len(node.prefix) { if i-offset == len(node.prefix) {
var control controlFlow var control flow
node, offset, control = node.end(path, data, i, offset) node, offset, control = node.end(path, data, i, offset)
switch control { switch control {
case controlStop: case flowStop:
return return
case controlBegin: case flowBegin:
goto begin goto begin
case controlNext: case flowNext:
goto next goto next
} }
} }

View File

@ -4,14 +4,15 @@ import (
"testing" "testing"
"git.akyoto.dev/go/router" "git.akyoto.dev/go/router"
"git.akyoto.dev/go/router/testdata"
) )
func BenchmarkBlog(b *testing.B) { func BenchmarkBlog(b *testing.B) {
routes := loadRoutes("testdata/blog.txt") routes := testdata.Routes("testdata/blog.txt")
r := router.New[string]() r := router.New[string]()
for _, route := range routes { for _, route := range routes {
r.Add(route.method, route.path, "") r.Add(route.Method, route.Path, "")
} }
b.Run("Len1-Param0", func(b *testing.B) { b.Run("Len1-Param0", func(b *testing.B) {
@ -28,11 +29,11 @@ func BenchmarkBlog(b *testing.B) {
} }
func BenchmarkGitHub(b *testing.B) { func BenchmarkGitHub(b *testing.B) {
routes := loadRoutes("testdata/github.txt") routes := testdata.Routes("testdata/github.txt")
r := router.New[string]() r := router.New[string]()
for _, route := range routes { for _, route := range routes {
r.Add(route.method, route.path, "") r.Add(route.Method, route.Path, "")
} }
b.Run("Len7-Param0", func(b *testing.B) { 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 == "" { if node.prefix == "" {
node.prefix = path node.prefix = path
node.data = data node.data = data
node.addTrailingSlash(data)
return return
} }
@ -229,7 +230,7 @@ func (node *treeNode[T]) append(path string, data T) {
// end is called when the node was fully parsed // end is called when the node was fully parsed
// and needs to decide the next control flow. // and needs to decide the next control flow.
// end is only called from `tree.Add`. // 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] char := path[i]
if char >= node.startIndex && char < node.endIndex { 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 { if index != 0 {
node = node.children[index] node = node.children[index]
offset = i 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 no prefix is set, this is the starting node.
if node.prefix == "" { if node.prefix == "" {
node.append(path[i:], data) node.append(path[i:], data)
return node, offset, controlStop return node, offset, flowStop
} }
// node: /user/|:id // 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 { if node.parameter != nil && path[i] == parameter {
node = node.parameter node = node.parameter
offset = i offset = i
return node, offset, controlBegin return node, offset, flowBegin
} }
node.append(path[i:], data) 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. // each traverses the tree and calls the given function on every node.