Added trailing slash for static routes
This commit is contained in:
parent
3008940025
commit
dd98b11eea
@ -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
|
||||||
|
@ -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) {
|
||||||
|
29
Tree.go
29
Tree.go
@ -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,18 +26,9 @@ 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)
|
|
||||||
|
|
||||||
switch control {
|
|
||||||
case controlStop:
|
|
||||||
return
|
|
||||||
case controlBegin:
|
|
||||||
goto begin
|
|
||||||
case controlNext:
|
|
||||||
goto next
|
goto next
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
if i == len(path) {
|
if i == len(path) {
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
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 == "" {
|
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.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user