diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..00afc5c Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index d92ddf2..14a569a 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ playground.xcworkspace # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. # Packages/ .build/ +Package.resolved # CocoaPods # diff --git a/Ambassador.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Ambassador.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Ambassador.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Ambassador/Responses/DelayResponse.swift b/Ambassador/Responses/DelayResponse.swift index 026b0bf..c50ecf5 100644 --- a/Ambassador/Responses/DelayResponse.swift +++ b/Ambassador/Responses/DelayResponse.swift @@ -42,8 +42,13 @@ public struct DelayResponse: WebApp { case .delay(let seconds): delayTime = seconds case .random(let min, let max): - let random = (Double(arc4random()) / 0x100000000) - delayTime = min + (max - min) * random + #if os(Linux) + srandom(UInt32(time(nil))) + let randomValue = (Double(random()) / 0x100000000) + #else + let randomValue = (Double(arc4random()) / 0x100000000) + #endif + delayTime = min + (max - min) * randomValue } let loop = environ["embassy.event_loop"] as! EventLoop diff --git a/Ambassador/Router.swift b/Ambassador/Router.swift index 6cdf73d..ca782de 100644 --- a/Ambassador/Router.swift +++ b/Ambassador/Router.swift @@ -57,6 +57,8 @@ open class Router: WebApp { } private func matchRoute(to searchPath: String) -> (WebApp, [String])? { + typealias ReturnValue = (WebApp, [String]) + var routeMatches: [(NSTextCheckingResult, ReturnValue)] = [] for (path, route) in routes { let regex = try! NSRegularExpression(pattern: path, options: []) let matches = regex.matches( @@ -65,15 +67,36 @@ open class Router: WebApp { range: NSRange(location: 0, length: searchPath.count) ) if !matches.isEmpty { - let searchPath = searchPath as NSString let match = matches[0] + guard match.range.length == searchPath.count else { continue } + let searchPath = NSString(string: searchPath) var captures = [String]() for rangeIdx in 1 ..< match.numberOfRanges { captures.append(searchPath.substring(with: match.range(at: rangeIdx))) } - return (route, captures) + let possibleReturnValue = (route, captures) + routeMatches.append((match, possibleReturnValue)) } } - return nil + + // sort the most specific route to top and return the result + return routeMatches.sorted(by: { (routeMatch1, routeMatch2) -> Bool in + guard let regex1 = routeMatch1.0.regularExpression, + let regex2 = routeMatch2.0.regularExpression + else { + return false + } + + // prefer regex without capture groups + // skip this decision when number of capture groups are equal + if regex1.numberOfCaptureGroups < regex2.numberOfCaptureGroups { + return true + } else if regex1.numberOfCaptureGroups > regex2.numberOfCaptureGroups { + return false + } + + // prefer the shorter regex pattern + return regex1.pattern.count < regex2.pattern.count + }).first?.1 } } diff --git a/AmbassadorTests/RouterTests.swift b/AmbassadorTests/RouterTests.swift index da0547c..f713ca6 100644 --- a/AmbassadorTests/RouterTests.swift +++ b/AmbassadorTests/RouterTests.swift @@ -112,4 +112,154 @@ class RouterTests: XCTestCase { XCTAssertEqual(receivedData.last?.count, 0) XCTAssertEqual(receivedCaptures ?? [], ["fang@envoy.com", "ABCD1234"]) } + + func testRoutingOnSimilarRoutes() { + let router = Router() + router["/resource"] = DataResponse() { environ -> Data in + return Data("index".utf8) + } + router["/resource/([0-9])"] = DataResponse() { environ -> Data in + return Data("show".utf8) + } + router["/resource/([a-zA-Z0-9-]+)"] = DataResponse() { environ -> Data in + return Data("show uuid".utf8) + } + router["/resource/([0-9])/action"] = DataResponse() { environ -> Data in + return Data("action on single resource".utf8) + } + router["/resource/([a-zA-Z0-9-]+)/action"] = DataResponse() { environ -> Data in + return Data("action on single resource with uuid".utf8) + } + router["/resource/types"] = DataResponse() { environ -> Data in + return Data("static".utf8) + } + router["/resource/verylongandexplicitstaticmethod"] = DataResponse() { environ -> Data in + return Data("verylongandexplicitstaticmethod".utf8) + } + + var receivedStatus: [String] = [] + let startResponse = { (status: String, headers: [(String, String)]) in + receivedStatus.append(status) + } + + var receivedData: [Data] = [] + let sendBody = { (data: Data) in + receivedData.append(data) + } + + // test /resource + var environ: [String: Any] = [ + "REQUEST_METHOD": "GET", + "SCRIPT_NAME": "", + "PATH_INFO": "/resource", + ] + router.app( + environ, + startResponse: startResponse, + sendBody: sendBody + ) + XCTAssertEqual(receivedStatus.count, 1) + XCTAssertEqual(receivedStatus.last, "200 OK") + XCTAssertEqual(receivedData.count, 2) + XCTAssertEqual(String(data: receivedData[0], encoding: .utf8), "index") + XCTAssertEqual(receivedData.last?.count, 0) + receivedStatus.removeAll() + receivedData.removeAll() + + + // test /resource/([0-9]) + environ = [ + "REQUEST_METHOD": "GET", + "SCRIPT_NAME": "", + "PATH_INFO": "/resource/1", + ] + router.app( + environ, + startResponse: startResponse, + sendBody: sendBody + ) + XCTAssertEqual(receivedStatus.count, 1) + XCTAssertEqual(receivedStatus.last, "200 OK") + XCTAssertEqual(receivedData.count, 2) + XCTAssertEqual(String(data: receivedData[0], encoding: .utf8), "show") + XCTAssertEqual(receivedData.last?.count, 0) + receivedStatus.removeAll() + receivedData.removeAll() + + // test /resource/([a-zA-Z0-9-]+) + environ = [ + "REQUEST_METHOD": "GET", + "SCRIPT_NAME": "", + "PATH_INFO": "/resource/\(UUID().uuidString)", + ] + router.app( + environ, + startResponse: startResponse, + sendBody: sendBody + ) + XCTAssertEqual(receivedStatus.count, 1) + XCTAssertEqual(receivedStatus.last, "200 OK") + XCTAssertEqual(receivedData.count, 2) + XCTAssertEqual(String(data: receivedData[0], encoding: .utf8), "show uuid") + XCTAssertEqual(receivedData.last?.count, 0) + receivedStatus.removeAll() + receivedData.removeAll() + + // test /resource/([0-9])/action + environ = [ + "REQUEST_METHOD": "GET", + "SCRIPT_NAME": "", + "PATH_INFO": "/resource/1/action", + ] + router.app( + environ, + startResponse: startResponse, + sendBody: sendBody + ) + XCTAssertEqual(receivedStatus.count, 1) + XCTAssertEqual(receivedStatus.last, "200 OK") + XCTAssertEqual(receivedData.count, 2) + XCTAssertEqual(String(data: receivedData[0], encoding: .utf8), "action on single resource") + XCTAssertEqual(receivedData.last?.count, 0) + receivedStatus.removeAll() + receivedData.removeAll() + + // test /resource/types + environ = [ + "REQUEST_METHOD": "GET", + "SCRIPT_NAME": "", + "PATH_INFO": "/resource/types", + ] + router.app( + environ, + startResponse: startResponse, + sendBody: sendBody + ) + XCTAssertEqual(receivedStatus.count, 1) + XCTAssertEqual(receivedStatus.last, "200 OK") + XCTAssertEqual(receivedData.count, 2) + XCTAssertEqual(String(data: receivedData[0], encoding: .utf8), "static") + XCTAssertEqual(receivedData.last?.count, 0) + receivedStatus.removeAll() + receivedData.removeAll() + + // test /resource/verylongandexplicitstaticmethod + environ = [ + "REQUEST_METHOD": "GET", + "SCRIPT_NAME": "", + "PATH_INFO": "/resource/verylongandexplicitstaticmethod", + ] + router.app( + environ, + startResponse: startResponse, + sendBody: sendBody + ) + XCTAssertEqual(receivedStatus.count, 1) + XCTAssertEqual(receivedStatus.last, "200 OK") + XCTAssertEqual(receivedData.count, 2) + XCTAssertEqual(String(data: receivedData[0], encoding: .utf8), "verylongandexplicitstaticmethod") + XCTAssertEqual(receivedData.last?.count, 0) + receivedStatus.removeAll() + receivedData.removeAll() + } } diff --git a/Cartfile.resolved b/Cartfile.resolved index 58c3443..83ccf20 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1 +1 @@ -github "envoy/Embassy" "v4.0.0" +github "envoy/Embassy" "v4.0.5" diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..eef603f --- /dev/null +++ b/Package.swift @@ -0,0 +1,17 @@ +// swift-tools-version:4.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "Ambassador", + products: [ + .library(name: "Ambassador", targets: ["Ambassador"]), + ], + dependencies: [ + .package(url: "https://github.com/envoy/Embassy.git", from: "4.0.5") + ], + targets: [ + .target(name: "Ambassador", dependencies: ["Embassy"], path: "Ambassador"), + ] +)