Repository: hummingbird-project/hummingbird (swift)

Issue:
Endpoint path incorrect for router group index routes
When using a router group in combination with index routes, the endpoint path for those requests incorrectly (IMO) has a trailing slash:

```swift
let group = router.group("/users")
group.get(use: list)

func list(request: HBRequest) {
  // here I'd expect request.endpointPath to be "/users", but it is "/users/"
}
```

This might be a regression from https://github.com/hummingbird-project/hummingbird/pull/164.

Pull request:
It should consistently end without a slash

Patch:
diff --git a/Sources/Hummingbird/Router/RouterBuilder.swift b/Sources/Hummingbird/Router/RouterBuilder.swift
index bf285d803..34292b94f 100644
--- a/Sources/Hummingbird/Router/RouterBuilder.swift
+++ b/Sources/Hummingbird/Router/RouterBuilder.swift
@@ -60,8 +60,8 @@ public final class HBRouterBuilder: HBRouterMethods {
     ///   - method: http method
     ///   - responder: handler to call
     public func add(_ path: String, method: HTTPMethod, responder: HBResponder) {
-        // ensure path starts with a "/"
-        let path = path.starts(with: "/") ? path : "/\(path)"
+        // ensure path starts with a "/" and doesn't end with a "/"
+        let path = "/\(path.dropSuffix("/").dropPrefix("/"))"
         self.trie.addEntry(.init(path), value: HBEndpointResponders(path: path)) { node in
             node.value!.addResponder(for: method, responder: middlewares.constructResponder(finalResponder: responder))
         }
diff --git a/Sources/Hummingbird/Router/RouterGroup.swift b/Sources/Hummingbird/Router/RouterGroup.swift
index c02338f46..a3de82895 100644
--- a/Sources/Hummingbird/Router/RouterGroup.swift
+++ b/Sources/Hummingbird/Router/RouterGroup.swift
@@ -89,21 +89,3 @@ public struct HBRouterGroup: HBRouterMethods {
         return "\(path1)/\(path2)"
     }
 }
-
-private extension String {
-    func dropPrefix(_ prefix: String) -> Substring {
-        if hasPrefix(prefix) {
-            return self.dropFirst(prefix.count)
-        } else {
-            return self[...]
-        }
-    }
-
-    func dropSuffix(_ suffix: String) -> Substring {
-        if hasSuffix(suffix) {
-            return self.dropLast(suffix.count)
-        } else {
-            return self[...]
-        }
-    }
-}
diff --git a/Sources/Hummingbird/Utils/StringProtocol.swift b/Sources/Hummingbird/Utils/StringProtocol.swift
new file mode 100644
index 000000000..ea96892c3
--- /dev/null
+++ b/Sources/Hummingbird/Utils/StringProtocol.swift
@@ -0,0 +1,31 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Hummingbird server framework project
+//
+// Copyright (c) 2021-2023 the Hummingbird authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+extension StringProtocol {
+    func dropPrefix(_ prefix: String) -> Self.SubSequence {
+        if hasPrefix(prefix) {
+            return self.dropFirst(prefix.count)
+        } else {
+            return self[...]
+        }
+    }
+
+    func dropSuffix(_ suffix: String) -> Self.SubSequence {
+        if hasSuffix(suffix) {
+            return self.dropLast(suffix.count)
+        } else {
+            return self[...]
+        }
+    }
+}


Test patch:
diff --git a/Tests/HummingbirdTests/RouterTests.swift b/Tests/HummingbirdTests/RouterTests.swift
index a1918111f..0a70d1309 100644
--- a/Tests/HummingbirdTests/RouterTests.swift
+++ b/Tests/HummingbirdTests/RouterTests.swift
@@ -69,6 +69,9 @@ final class RouterTests: XCTestCase {
         app.router.get("test") {
             $0.endpointPath
         }
+        app.router.get {
+            $0.endpointPath
+        }
         app.router.post("/test2") {
             $0.endpointPath
         }
@@ -76,6 +79,50 @@ final class RouterTests: XCTestCase {
         try app.XCTStart()
         defer { app.XCTStop() }
 
+        try app.XCTExecute(uri: "/", method: .GET) { response in
+            let body = try XCTUnwrap(response.body)
+            XCTAssertEqual(String(buffer: body), "/")
+        }
+        try app.XCTExecute(uri: "/test/", method: .GET) { response in
+            let body = try XCTUnwrap(response.body)
+            XCTAssertEqual(String(buffer: body), "/test")
+        }
+        try app.XCTExecute(uri: "/test2/", method: .POST) { response in
+            let body = try XCTUnwrap(response.body)
+            XCTAssertEqual(String(buffer: body), "/test2")
+        }
+    }
+
+    /// Test endpointPath doesn't have "/" at end
+    func testEndpointPathSuffix() throws {
+        struct TestEndpointMiddleware: HBMiddleware {
+            func apply(to request: HBRequest, next: HBResponder) -> EventLoopFuture<HBResponse> {
+                guard let endpointPath = request.endpointPath else { return next.respond(to: request) }
+                return request.success(.init(status: .ok, body: .byteBuffer(ByteBuffer(string: endpointPath))))
+            }
+        }
+
+        let app = HBApplication(testing: .embedded)
+        app.middleware.add(TestEndpointMiddleware())
+        app.router.get("test/") {
+            $0.endpointPath
+        }
+        app.router.post("test2") {
+            $0.endpointPath
+        }
+        app.router
+            .group("testGroup")
+            .get {
+                $0.endpointPath
+            }
+        app.router
+            .group("testGroup2")
+            .get("/") {
+                $0.endpointPath
+            }
+        try app.XCTStart()
+        defer { app.XCTStop() }
+
         try app.XCTExecute(uri: "/test/", method: .GET) { response in
             let body = try XCTUnwrap(response.body)
             XCTAssertEqual(String(buffer: body), "/test")
@@ -85,6 +132,16 @@ final class RouterTests: XCTestCase {
             let body = try XCTUnwrap(response.body)
             XCTAssertEqual(String(buffer: body), "/test2")
         }
+
+        try app.XCTExecute(uri: "/testGroup/", method: .GET) { response in
+            let body = try XCTUnwrap(response.body)
+            XCTAssertEqual(String(buffer: body), "/testGroup")
+        }
+
+        try app.XCTExecute(uri: "/testGroup2", method: .GET) { response in
+            let body = try XCTUnwrap(response.body)
+            XCTAssertEqual(String(buffer: body), "/testGroup2")
+        }
     }
 
     /// Test correct endpoints are called from group