Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Router improvements #179

Merged
merged 7 commits into from
Mar 20, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Router improvements
Capture paths caught by recursive wildcard
Wildcards with both prefix and suffix
  • Loading branch information
adam-fowler committed Mar 20, 2023
commit 91067df82c786e26ece5b9745a7928b6f2c7a935
25 changes: 25 additions & 0 deletions Sources/Hummingbird/Router/Parameters.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,22 @@ public struct HBParameters {
public typealias Collection = FlatDictionary<Substring, Substring>
internal var parameters: Collection

static let recursiveCaptureKey: Substring = ":**:"

init() {
self.parameters = .init()
}

init(_ values: Collection) {
self.parameters = values
}

/// Return if parameter exists
/// - Parameter s: parameter id
public func has(_ s: Substring) -> Bool {
return self.parameters.has(s)
}

/// Return parameter with specified id
/// - Parameter s: parameter id
public func get(_ s: String) -> String? {
Expand All @@ -35,6 +47,11 @@ public struct HBParameters {
return self.parameters[s[...]].map { T(String($0)) } ?? nil
}

/// Return path elements caught by recursive capture
public func getRecursiveCapture() -> String? {
return self.parameters[Self.recursiveCaptureKey].map { String($0) }
}

/// Return parameter with specified id
/// - Parameter s: parameter id
public func require(_ s: String) throws -> String {
Expand Down Expand Up @@ -92,6 +109,14 @@ public struct HBParameters {
self.parameters[s] = value
}

/// Set path components caught by recursive capture
/// - Parameters:
/// - value: parameter value
mutating func setRecursiveCapture(_ value: Substring) {
guard !self.parameters.has(Self.recursiveCaptureKey) else { return }
self.parameters[Self.recursiveCaptureKey] = value
}

public subscript(_ s: String) -> String? {
return self.parameters[s[...]].map { String($0) }
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/Hummingbird/Router/Router.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ struct HBRouter: HBResponder {
return self.notFoundResponder.respond(to: request)
}
var request = request
if result.parameters.count > 0 {
request.parameters = result.parameters
if let parameters = result.parameters {
request.parameters = parameters
}
// store endpoint path in request (mainly for metrics)
request.endpointPath = result.value.path
Expand Down
10 changes: 10 additions & 0 deletions Sources/Hummingbird/Router/RouterPath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ struct RouterPath: ExpressibleByStringLiteral {
case path(Substring)
case parameter(Substring)
case wildcard
case prefixWildcard(Substring) // *.jpg
case suffixWildcard(Substring) // file.*
case recursiveWildcard
case null

Expand All @@ -29,6 +31,10 @@ struct RouterPath: ExpressibleByStringLiteral {
return true
case .wildcard:
return true
case .prefixWildcard(let suffix):
return rhs.hasSuffix(suffix)
case .suffixWildcard(let prefix):
return rhs.hasPrefix(prefix)
case .recursiveWildcard:
return true
case .null:
Expand Down Expand Up @@ -57,6 +63,10 @@ struct RouterPath: ExpressibleByStringLiteral {
return .wildcard
} else if component == "**" {
return .recursiveWildcard
} else if component.first == "*" {
return .prefixWildcard(component.dropFirst())
} else if component.last == "*" {
return .suffixWildcard(component.dropLast())
} else {
return .path(component)
}
Expand Down
28 changes: 26 additions & 2 deletions Sources/Hummingbird/Router/TrieRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,17 @@ struct RouterPathTrie<Value> {
}
}

func getValueAndParameters(_ path: String) -> (value: Value, parameters: HBParameters)? {
func getValueAndParameters(_ path: String) -> (value: Value, parameters: HBParameters?)? {
let pathComponents = path.split(separator: "/", omittingEmptySubsequences: true)
var parameters = HBParameters()
var parameters: HBParameters?
var node = self.root
for component in pathComponents {
if let childNode = node.getChild(component) {
node = childNode
if case .parameter(let key) = node.key {
parameters.set(key, value: component)
} else if case .recursiveWildcard = node.key {
parameters.setRecursiveCapture(path[component.startIndex..<path.endIndex])
}
} else if case .recursiveWildcard = node.key {
} else {
Expand Down Expand Up @@ -87,3 +89,25 @@ struct RouterPathTrie<Value> {
}
}
}

extension Optional where Wrapped == HBParameters {
mutating func set(_ s: Substring, value: Substring) {
switch self {
case .some(var parameters):
parameters.set(s, value: value)
self = .some(parameters)
case .none:
self = .some(.init(.init([(s, value)])))
}
}

mutating func setRecursiveCapture(_ value: Substring) {
switch self {
case .some(var parameters):
parameters.setRecursiveCapture(value)
self = .some(parameters)
case .none:
self = .some(.init(.init([(HBParameters.recursiveCaptureKey, value)])))
}
}
}
9 changes: 8 additions & 1 deletion Sources/Hummingbird/Utils/FlatDictionary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public struct FlatDictionary<Key: Hashable, Value>: Collection {
}

/// Create a new FlatDictionary from an array of key value pairs
public init(_ values: [(key: Key, value: Value)]) {
public init(_ values: [Element]) {
self.elements = values
self.hashKeys = values.map {
Self.hashKey($0.key)
Expand Down Expand Up @@ -88,6 +88,13 @@ public struct FlatDictionary<Key: Hashable, Value>: Collection {
}
}

/// Return if dictionary has this value
/// - Parameter key:
public func has(_ key: Key) -> Bool {
let hashKey = Self.hashKey(key)
return self.hashKeys.firstIndex(of: hashKey) != nil
}

/// Return all the values, associated with a given key
public func getAll(for key: Key) -> [Value] {
var values: [Value] = []
Expand Down
31 changes: 28 additions & 3 deletions Tests/HummingbirdTests/TrieRouterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
@testable import Hummingbird
import XCTest

class HummingbirdTrieRouterTests: XCTestCase {
class TrieRouterTests: XCTestCase {
func testPathComponentsTrie() {
let trie = RouterPathTrie<String>()
trie.addEntry("/usr/local/bin", value: "test1")
Expand Down Expand Up @@ -51,8 +51,8 @@ class HummingbirdTrieRouterTests: XCTestCase {
trie.addEntry("users/:user", value: "test1")
trie.addEntry("users/:user/name", value: "john smith")
XCTAssertNil(trie.getValueAndParameters("/user/"))
XCTAssertEqual(trie.getValueAndParameters("/users/1234")?.parameters.get("user"), "1234")
XCTAssertEqual(trie.getValueAndParameters("/users/1234/name")?.parameters.get("user"), "1234")
XCTAssertEqual(trie.getValueAndParameters("/users/1234")?.parameters?.get("user"), "1234")
XCTAssertEqual(trie.getValueAndParameters("/users/1234/name")?.parameters?.get("user"), "1234")
XCTAssertEqual(trie.getValueAndParameters("/users/1234/name")?.value, "john smith")
}

Expand All @@ -62,6 +62,7 @@ class HummingbirdTrieRouterTests: XCTestCase {
XCTAssertEqual(trie.getValueAndParameters("/one")?.value, "**")
XCTAssertEqual(trie.getValueAndParameters("/one/two")?.value, "**")
XCTAssertEqual(trie.getValueAndParameters("/one/two/three")?.value, "**")
XCTAssertEqual(trie.getValueAndParameters("/one/two/three")?.parameters?.getRecursiveCapture(), "one/two/three")
}

func testRecursiveWildcardWithPrefix() {
Expand All @@ -72,5 +73,29 @@ class HummingbirdTrieRouterTests: XCTestCase {
XCTAssertEqual(trie.getValueAndParameters("/Test/one")?.value, "true")
XCTAssertEqual(trie.getValueAndParameters("/Test/one/two")?.value, "true")
XCTAssertEqual(trie.getValueAndParameters("/Test/one/two/three")?.value, "true")
XCTAssertEqual(trie.getValueAndParameters("/Test/")?.parameters?.getRecursiveCapture(), nil)
XCTAssertEqual(trie.getValueAndParameters("/Test/one/two")?.parameters?.getRecursiveCapture(), "one/two")
}

func testPrefixWildcard() {
let trie = RouterPathTrie<String>()
trie.addEntry("*.jpg", value: "jpg")
trie.addEntry("test/*.jpg", value: "testjpg")
trie.addEntry("*.app/config.json", value: "app")
XCTAssertNil(trie.getValueAndParameters("/hello.png"))
XCTAssertEqual(trie.getValueAndParameters("/hello.jpg")?.value, "jpg")
XCTAssertEqual(trie.getValueAndParameters("/test/hello.jpg")?.value, "testjpg")
XCTAssertEqual(trie.getValueAndParameters("/hello.app/config.json")?.value, "app")
}

func testSuffixWildcard() {
let trie = RouterPathTrie<String>()
trie.addEntry("file.*", value: "file")
trie.addEntry("test/file.*", value: "testfile")
trie.addEntry("file.*/test", value: "filetest")
XCTAssertNil(trie.getValueAndParameters("/file2.png"))
XCTAssertEqual(trie.getValueAndParameters("/file.jpg")?.value, "file")
XCTAssertEqual(trie.getValueAndParameters("/test/file.jpg")?.value, "testfile")
XCTAssertEqual(trie.getValueAndParameters("/file.png/test")?.value, "filetest")
}
}