Skip to content

Commit

Permalink
Add access to components matched by catchall ('**') (#94)
Browse files Browse the repository at this point in the history
* store catchall matched path in parameters

* refactor Parameters.catchall
  • Loading branch information
stevapple authored Apr 21, 2020
1 parent 60c2a16 commit e7f2d5b
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 9 deletions.
40 changes: 40 additions & 0 deletions Sources/RoutingKit/Parameters.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import Foundation
public struct Parameters {
/// Internal storage.
private var values: [String: String]
private var catchall: Catchall

/// Creates a new `Parameters`.
///
/// Pass this into the `Router.route(path:parameters:)` method to fill with values.
public init() {
self.values = [:]
self.catchall = Catchall()
}

/// Grabs the named parameter from the parameter bag.
Expand Down Expand Up @@ -55,4 +57,42 @@ public struct Parameters {
public mutating func set(_ name: String, to value: String?) {
self.values[name] = value?.removingPercentEncoding
}

/// Fetches the components matched by `catchall` (`**`).
///
/// If the route doen't hit `catchall`, it'll return `[]`.
///
/// You can judge whether `catchall` is hit using:
///
/// let matched = parameters.getCatchall()
/// guard matched.count != 0 else {
/// // not hit
/// }
///
/// - note: The value will be percent-decoded.
///
/// - returns: The path components matched
public mutating func getCatchall() -> [String] {
if self.catchall.isPercentEncoded {
self.catchall.values = self.catchall.values.map { $0.removingPercentEncoding ?? $0 }
self.catchall.isPercentEncoded = false
}
return self.catchall.values
}

/// Stores the components matched by `catchall` (`**`).
///
/// - parameters:
/// - matched: The subpaths matched (percent-encoded if necessary)
public mutating func setCatchall(matched: [String]) {
self.catchall = Catchall(values: matched)
}

/// Holds path components that were matched by `catchall` (`**`).
///
/// Used internally.
private struct Catchall {
var values: [String] = []
var isPercentEncoded: Bool = true
}
}
2 changes: 2 additions & 0 deletions Sources/RoutingKit/PathComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public enum PathComponent: ExpressibleByStringLiteral, CustomStringConvertible {
/// Catch alls have the lowest precedence, and will only be matched
/// if no more specific path components are found.
///
/// The matched subpath will be stored into `Parameters.catchall`.
///
/// Represented as `**`
case catchall

Expand Down
16 changes: 9 additions & 7 deletions Sources/RoutingKit/TrieRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,17 +76,17 @@ public final class TrieRouter<Output>: Router, CustomStringConvertible {

let isCaseInsensitive = self.options.contains(.caseInsensitive)

var currentCatchall: Node?
var currentCatchall: (Node, [String])?

// traverse the string path supplied
search: for path in path {
search: for (index, slice) in path.enumerated() {
// store catchall in case search hits dead end
if let catchall = currentNode.catchall {
currentCatchall = catchall
currentCatchall = (catchall, [String](path.dropFirst(index)))
}

// check the constants first
if let constant = currentNode.constants[isCaseInsensitive ? path.lowercased() : path] {
if let constant = currentNode.constants[isCaseInsensitive ? slice.lowercased() : slice] {
currentNode = constant
continue search
}
Expand All @@ -95,7 +95,7 @@ public final class TrieRouter<Output>: Router, CustomStringConvertible {
if let (name, parameter) = currentNode.parameter {
// if no constant routes were found that match the path, but
// a dynamic parameter child was found, we can use it
parameters.set(name, to: path)
parameters.set(name, to: slice)
currentNode = parameter
continue search
}
Expand All @@ -107,8 +107,9 @@ public final class TrieRouter<Output>: Router, CustomStringConvertible {
}

// no matches, stop searching
if let catchall = currentCatchall {
if let (catchall, subpaths) = currentCatchall {
// fallback to catchall output if we have one
parameters.setCatchall(matched: subpaths)
return catchall.output
} else {
return nil
Expand All @@ -118,8 +119,9 @@ public final class TrieRouter<Output>: Router, CustomStringConvertible {
if let output = currentNode.output {
// return the currently resolved responder if there hasn't been an early exit.
return output
} else if let catchall = currentCatchall {
} else if let (catchall, subpaths) = currentCatchall {
// fallback to catchall output if we have one
parameters.setCatchall(matched: subpaths)
return catchall.output
} else {
// current node has no output and there was not catchall
Expand Down
18 changes: 16 additions & 2 deletions Tests/RoutingKitTests/RouterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ final class RouterTests: XCTestCase {
}

// https://github.com/vapor/routing-kit/issues/74
func testCatchAllNested() throws {
func testCatchallNested() throws {
let router = TrieRouter(String.self)
router.register("/**", at: [.catchall])
router.register("/a/**", at: ["a", .catchall])
Expand All @@ -117,7 +117,7 @@ final class RouterTests: XCTestCase {
XCTAssertEqual(router.route(path: ["b", "c", "d", "e"], parameters: &params), "/**")
}

func testCatchAllPrecedence() throws {
func testCatchallPrecedence() throws {
let router = TrieRouter(String.self)
router.register("a", at: ["v1", "test"])
router.register("b", at: ["v1", .catchall])
Expand All @@ -128,6 +128,20 @@ final class RouterTests: XCTestCase {
XCTAssertEqual(router.route(path: ["v1", "foo"], parameters: &params), "c")
}

func testCatchallValue() throws {
let router = TrieRouter<Int>()
router.register(42, at: ["users", ":user", "**"])
router.register(24, at: ["users", "**"])

var params = Parameters()
XCTAssertNil(router.route(path: ["users"], parameters: &params))
XCTAssertEqual(params.getCatchall().count, 0)
XCTAssertEqual(router.route(path: ["users", "stevapple"], parameters: &params), 24)
XCTAssertEqual(params.getCatchall(), ["stevapple"])
XCTAssertEqual(router.route(path: ["users", "stevapple", "posts", "2"], parameters: &params), 42)
XCTAssertEqual(params.getCatchall(), ["posts", "2"])
}

func testRouterDescription() throws {
// Use simple routing to eliminate the impact of registration order
let constA: PathComponent = "a"
Expand Down

0 comments on commit e7f2d5b

Please sign in to comment.