Added trailing slash for static routes
This commit is contained in:
parent
3008940025
commit
dd98b11eea
@ -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
|
||||
|
@ -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
31
Tree.go
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
11
flow.go
Normal 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
|
||||
)
|
@ -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
52
testdata/Route.go
vendored
Normal 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
|
||||
}
|
11
treeNode.go
11
treeNode.go
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user