diff --git a/README.md b/README.md index 382017a..724d820 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/Router_test.go b/Router_test.go index cc93b46..66230c6 100644 --- a/Router_test.go +++ b/Router_test.go @@ -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) { diff --git a/Tree.go b/Tree.go index 3193d8a..5dfe82b 100644 --- a/Tree.go +++ b/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 } } diff --git a/benchmarks_test.go b/benchmarks_test.go index d72791e..55caf1f 100644 --- a/benchmarks_test.go +++ b/benchmarks_test.go @@ -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) {} diff --git a/flow.go b/flow.go new file mode 100644 index 0000000..d84bcaa --- /dev/null +++ b/flow.go @@ -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 +) diff --git a/helper_test.go b/helper_test.go deleted file mode 100644 index 92b14b1..0000000 --- a/helper_test.go +++ /dev/null @@ -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) {} diff --git a/testdata/Route.go b/testdata/Route.go new file mode 100644 index 0000000..2a2231f --- /dev/null +++ b/testdata/Route.go @@ -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 +} diff --git a/treeNode.go b/treeNode.go index ab430a6..c500939 100644 --- a/treeNode.go +++ b/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.