Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Sources/RoutingKit/PathComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,22 @@ public enum PathComponent: ExpressibleByStringInterpolation, CustomStringConvert
}
}

extension PathComponent {
/// The components this one spells out when it contains `/`, or just itself when it doesn't.
var splitAtSlashes: [PathComponent] {
switch self {
case .constant(let constant) where constant.contains("/"):
constant.pathComponents
case .parameter(let name) where name.contains("/"):
(":" + name).pathComponents
case .partialParameter(let template, _, _) where template.contains("/"):
(":" + template).pathComponents
default:
[self]
}
}
}

extension String {
/// Converts a string into an array of ``PathComponent``.
public var pathComponents: [PathComponent] {
Expand Down
5 changes: 5 additions & 0 deletions Sources/RoutingKit/TrieRouter/TrieRouterBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,15 @@ public struct TrieRouterBuilder<Output: Sendable>: RouterBuilder {
/// let router = TrieRouter<Route>()
/// router.register(route, at: [.constant("users"), User.parameter])
///
/// A component containing `/`, such as `"users/:id"`, is registered as the components it spells out,
/// the same as `["users", ":id"]`.
///
/// - Parameters:
/// - output: Output to register.
/// - path: Path to register output at.
public mutating func register(_ output: Output, at path: [PathComponent]) {
// Request paths are split on `/` before routing, so a component that still contains one could never match.
let path = path.flatMap(\.splitAtSlashes)
assert(!path.isEmpty, "Cannot register a route with an empty path.")
root = insertRoute(node: root, path: path[...], output: output)
}
Expand Down
26 changes: 26 additions & 0 deletions Tests/RoutingKitTests/RouterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,32 @@ struct RouterTests {
#expect(p.get("x") == "a")
}

@Test(
"Slashes in a path component separate components",
.bug("https://github.com/vapor/vapor/issues/3349"),
arguments: [
("te/st", ["te", "st"], [:]),
("/users/:id/", ["users", "42"], ["id": "42"]),
(":id/edit", ["42", "edit"], ["id": "42"]),
("files/**", ["files", "a", "b"], [:]),
("docs/:{name}.md", ["docs", "intro.md"], ["name": "intro"]),
(":{name}.md/raw", ["intro.md", "raw"], ["name": "intro"]),
] as [(String, [String], [String: String])]
)
func slashesSeparateComponents(registered: String, request: [String], expectedParameters: [String: String]) {
var b = TrieRouterBuilder<Int>()
b.register(1, at: [.init(stringLiteral: registered)])
let r = b.build()

var p = Parameters()
#expect(r.route(path: request, parameters: &p) == 1)
for (name, value) in expectedParameters {
#expect(p.get(name) == value)
}
// The slashed text is no longer registered as a single component.
#expect(r.route(path: [request.joined(separator: "/")], parameters: &p) == nil)
}

@Test("Multiple wildcard alternatives")
func multipleWildcards() throws {
var b = TrieRouterBuilder<Int>()
Expand Down