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

Async handling; Controller protocol; Path/handler conveniences #526

Open
wants to merge 9 commits into
base: stable
Choose a base branch
from
4 changes: 2 additions & 2 deletions .github/workflows/linux-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ jobs:
Test-Linux-Platform:
runs-on: ubuntu-latest
container:
image: swift:5.2
image: swift:5.6
options: --cap-add=SYS_PTRACE --security-opt seccomp=unconfined --security-opt apparmor=unconfined
steps:
- uses: actions/checkout@v2
- name: Run Unit Tests
run: swift test
run: swift test
4 changes: 2 additions & 2 deletions .github/workflows/macos-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
steps:
- uses: maxim-lobanov/[email protected]
with:
xcode-version: 12.0
xcode-version: 13.2
- uses: actions/checkout@v2
- name: Create Test Result Directory
run: |
Expand All @@ -39,4 +39,4 @@ jobs:
uses: actions/upload-artifact@v1
with:
name: test-results
path: tmp/test-results
path: tmp/test-results
11 changes: 7 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// swift-tools-version:5.0
// swift-tools-version:5.6

import PackageDescription

let package = Package(
name: "Swifter",

platforms: [.macOS(.v10_15)],

products: [
.library(name: "Swifter", targets: ["Swifter"]),
.executable(name: "SwifterExample", targets: ["SwifterExample"])
Expand All @@ -16,10 +18,11 @@ let package = Package(
.target(
name: "Swifter",
dependencies: [],
path: "Xcode/Sources"
),
path: "Xcode/Sources",
exclude: ["Xcode/Sources/DemoServer.swift"]
),

.target(
.executableTarget(
name: "SwifterExample",
dependencies: [
"Swifter"
Expand Down
3 changes: 3 additions & 0 deletions Xcode/Sources/Controller.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
public protocol Controller {
init(request: HttpRequest)
}
2 changes: 1 addition & 1 deletion Xcode/Sources/Files.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public func directoryBrowser(_ dir: String) -> ((HttpRequest) -> HttpResponse) {
tr {
td {
a {
href = request.path + "/" + file
href = request.path + "%2F" + file
inner = file
}
}
Expand Down
15 changes: 8 additions & 7 deletions Xcode/Sources/HttpRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ open class HttpRouter {
var isEndOfRoute: Bool = false

/// The closure to handle the route
var handler: ((HttpRequest) -> HttpResponse)?
var handler: ((HttpRequest) async -> HttpResponse)?
}

private var rootNode = Node()
Expand All @@ -47,7 +47,7 @@ open class HttpRouter {
return result
}

public func register(_ method: String?, path: String, handler: ((HttpRequest) -> HttpResponse)?) {
public func register(_ method: String?, path: String, handler: ((HttpRequest) async -> HttpResponse)?) {
var pathSegments = stripQuery(path).split("/")
if let method = method {
pathSegments.insert(method, at: 0)
Expand All @@ -58,7 +58,7 @@ open class HttpRouter {
inflate(&rootNode, generator: &pathSegmentsGenerator).handler = handler
}

public func route(_ method: String?, path: String) -> ([String: String], (HttpRequest) -> HttpResponse)? {
public func route(_ method: String?, path: String) -> ([String: String], (HttpRequest) async -> HttpResponse)? {

return queue.sync {
if let method = method {
Expand Down Expand Up @@ -98,7 +98,7 @@ open class HttpRouter {
return currentNode
}

private func findHandler(_ node: inout Node, params: inout [String: String], generator: inout IndexingIterator<[String]>) -> ((HttpRequest) -> HttpResponse)? {
private func findHandler(_ node: inout Node, params: inout [String: String], generator: inout IndexingIterator<[String]>) -> ((HttpRequest) async -> HttpResponse)? {

var matchedRoutes = [Node]()
let pattern = generator.map { $0 }
Expand All @@ -125,20 +125,21 @@ open class HttpRouter {
var currentIndex = index + 1
let variableNodes = node.nodes.filter { $0.0.first == ":" }
if let variableNode = variableNodes.first {
let paramName = String(variableNode.0.dropFirst())
if currentIndex == count && variableNode.1.isEndOfRoute {
// if it's the last element of the pattern and it's a variable, stop the search and
// append a tail as a value for the variable.
let tail = pattern[currentIndex..<count].joined(separator: "/")
if tail.count > 0 {
params[variableNode.0] = pathToken + "/" + tail
params[paramName] = pathToken + "/" + tail
} else {
params[variableNode.0] = pathToken
params[paramName] = pathToken
}

matchedNodes.append(variableNode.value)
return
}
params[variableNode.0] = pathToken
params[paramName] = pathToken
findHandler(&node.nodes[variableNode.0]!, params: &params, pattern: pattern, matchedNodes: &matchedNodes, index: currentIndex, count: count)
}

Expand Down
19 changes: 15 additions & 4 deletions Xcode/Sources/HttpServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ open class HttpServer: HttpServerIO {
public var DELETE, PATCH, HEAD, POST, GET, PUT: MethodRoute
public var delete, patch, head, post, get, put: MethodRoute

public subscript(path: String) -> ((HttpRequest) -> HttpResponse)? {
public subscript(path: String) -> ((HttpRequest) async -> HttpResponse)? {
get { return nil }
set {
router.register(nil, path: path, handler: newValue)
Expand All @@ -56,7 +56,7 @@ open class HttpServer: HttpServerIO {

public var middleware = [(HttpRequest) -> HttpResponse?]()

override open func dispatch(_ request: HttpRequest) -> ([String: String], (HttpRequest) -> HttpResponse) {
override open func dispatch(_ request: HttpRequest) -> ([String: String], (HttpRequest) async -> HttpResponse) {
for layer in middleware {
if let response = layer(request) {
return ([:], { _ in response })
Expand All @@ -74,10 +74,21 @@ open class HttpServer: HttpServerIO {
public struct MethodRoute {
public let method: String
public let router: HttpRouter
public subscript(path: String) -> ((HttpRequest) -> HttpResponse)? {
public subscript(paths: String...) -> ((HttpRequest) async -> HttpResponse)? {
get { return nil }
set {
router.register(method, path: path, handler: newValue)
paths.forEach { router.register(method, path: $0, handler: newValue) }
}
}
public subscript<T: Controller>(paths: String...) -> ((T) -> () async -> HttpResponse)? {
get { nil }
set {
guard let newValue = newValue else { return }
paths.forEach { path in
router.register(method, path: path) { request in
await newValue(T(request: request))()
}
}
}
}
}
Expand Down
45 changes: 23 additions & 22 deletions Xcode/Sources/HttpServerIO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ open class HttpServerIO {
stop()
}

@available(macOS 10.10, *)
public func start(_ port: in_port_t = 8080, forceIPv4: Bool = false, priority: DispatchQoS.QoSClass = DispatchQoS.QoSClass.background) throws {
guard !self.operating else { return }
stop()
Expand Down Expand Up @@ -111,34 +110,36 @@ open class HttpServerIO {
self.state = .stopped
}

open func dispatch(_ request: HttpRequest) -> ([String: String], (HttpRequest) -> HttpResponse) {
open func dispatch(_ request: HttpRequest) -> ([String: String], (HttpRequest) async -> HttpResponse) {
return ([:], { _ in HttpResponse.notFound(nil) })
}

private func handleConnection(_ socket: Socket) {
let parser = HttpParser()
while self.operating, let request = try? parser.readHttpRequest(socket) {
let request = request
request.address = try? socket.peername()
let (params, handler) = self.dispatch(request)
request.params = params
let response = handler(request)
var keepConnection = parser.supportsKeepAlive(request.headers)
do {
if self.operating {
keepConnection = try self.respond(socket, response: response, keepAlive: keepConnection)
Task {
let parser = HttpParser()
while self.operating, let request = try? parser.readHttpRequest(socket) {
let request = request
request.address = try? socket.peername()
let (params, handler) = self.dispatch(request)
request.params = params
let response = await handler(request)
var keepConnection = parser.supportsKeepAlive(request.headers)
do {
if self.operating {
keepConnection = try self.respond(socket, response: response, keepAlive: keepConnection)
}
} catch {
print("Failed to send response: \(error)")
}
} catch {
print("Failed to send response: \(error)")
}
if let session = response.socketSession() {
delegate?.socketConnectionReceived(socket)
session(socket)
break
if let session = response.socketSession() {
delegate?.socketConnectionReceived(socket)
session(socket)
break
}
if !keepConnection { break }
}
if !keepConnection { break }
socket.close()
}
socket.close()
}

private struct InnerWriteContext: HttpResponseBodyWriter {
Expand Down
6 changes: 3 additions & 3 deletions Xcode/Tests/FilesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class FilesTests: XCTestCase {
let closure = shareFile(temporaryDirectoryURL.appendingPathComponent("does_not_exist").path)
let result = closure(request)

XCTAssert(result == .notFound)
XCTAssert(result == .notFound())
}

func testShareFilesFromDirectory() {
Expand All @@ -77,7 +77,7 @@ class FilesTests: XCTestCase {
let closure = shareFilesFromDirectory(temporaryDirectoryURL.path)
let result = closure(request)

XCTAssert(result == .notFound)
XCTAssert(result == .notFound())
}

func testDirectoryBrowser() {
Expand All @@ -95,6 +95,6 @@ class FilesTests: XCTestCase {
let closure = directoryBrowser(temporaryDirectoryURL.path)
let result = closure(request)

XCTAssert(result == .notFound)
XCTAssert(result == .notFound())
}
}
34 changes: 17 additions & 17 deletions Xcode/Tests/SwifterTestsHttpRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ class SwifterTestsHttpRouter: XCTestCase {
XCTAssertNotNil(router.route(nil, path: "/a/%3C%3E/%5E"))
}

func testHttpRouterHandlesOverlappingPaths() {
func testHttpRouterHandlesOverlappingPaths() async {

let request = HttpRequest()

Expand All @@ -151,19 +151,19 @@ class SwifterTestsHttpRouter: XCTestCase {
let staticRouteResult = router.route("GET", path: "a/b")
let staticRouterHandler = staticRouteResult?.1
XCTAssertNotNil(staticRouteResult)
_ = staticRouterHandler?(request)
_ = await staticRouterHandler?(request)

let variableRouteResult = router.route("GET", path: "a/b/c")
let variableRouterHandler = variableRouteResult?.1
XCTAssertNotNil(variableRouteResult)
_ = variableRouterHandler?(request)
_ = await variableRouterHandler?(request)

waitForExpectations(timeout: 10, handler: nil)
await waitForExpectations(timeout: 10, handler: nil)
XCTAssertTrue(foundStaticRoute)
XCTAssertTrue(foundVariableRoute)
}

func testHttpRouterHandlesOverlappingPathsInDynamicRoutes() {
func testHttpRouterHandlesOverlappingPathsInDynamicRoutes() async {

let request = HttpRequest()

Expand All @@ -186,19 +186,19 @@ class SwifterTestsHttpRouter: XCTestCase {
let firstRouteResult = router.route("GET", path: "a/b")
let firstRouterHandler = firstRouteResult?.1
XCTAssertNotNil(firstRouteResult)
_ = firstRouterHandler?(request)
_ = await firstRouterHandler?(request)

let secondRouteResult = router.route("GET", path: "a/b/c")
let secondRouterHandler = secondRouteResult?.1
XCTAssertNotNil(secondRouteResult)
_ = secondRouterHandler?(request)
_ = await secondRouterHandler?(request)

waitForExpectations(timeout: 10, handler: nil)
await waitForExpectations(timeout: 10, handler: nil)
XCTAssertTrue(foundFirstVariableRoute)
XCTAssertTrue(foundSecondVariableRoute)
}

func testHttpRouterShouldHandleOverlappingRoutesInTrail() {
func testHttpRouterShouldHandleOverlappingRoutesInTrail() async {

let request = HttpRequest()

Expand Down Expand Up @@ -229,25 +229,25 @@ class SwifterTestsHttpRouter: XCTestCase {
let firstRouteResult = router.route("GET", path: "/a")
let firstRouterHandler = firstRouteResult?.1
XCTAssertNotNil(firstRouteResult)
_ = firstRouterHandler?(request)
_ = await firstRouterHandler?(request)

let secondRouteResult = router.route("GET", path: "/a/b")
let secondRouterHandler = secondRouteResult?.1
XCTAssertNotNil(secondRouteResult)
_ = secondRouterHandler?(request)
_ = await secondRouterHandler?(request)

let thirdRouteResult = router.route("GET", path: "/a/b/b")
let thirdRouterHandler = thirdRouteResult?.1
XCTAssertNotNil(thirdRouteResult)
_ = thirdRouterHandler?(request)
_ = await thirdRouterHandler?(request)

waitForExpectations(timeout: 10, handler: nil)
await waitForExpectations(timeout: 10, handler: nil)
XCTAssertTrue(foundFirstVariableRoute)
XCTAssertTrue(foundSecondVariableRoute)
XCTAssertTrue(foundThirdVariableRoute)
}

func testHttpRouterHandlesOverlappingPathsInDynamicRoutesInTheMiddle() {
func testHttpRouterHandlesOverlappingPathsInDynamicRoutesInTheMiddle() async {

let request = HttpRequest()

Expand All @@ -270,14 +270,14 @@ class SwifterTestsHttpRouter: XCTestCase {
let firstRouteResult = router.route("GET", path: "/a/b/c/d/e")
let firstRouterHandler = firstRouteResult?.1
XCTAssertNotNil(firstRouteResult)
_ = firstRouterHandler?(request)
_ = await firstRouterHandler?(request)

let secondRouteResult = router.route("GET", path: "/a/b/f/g")
let secondRouterHandler = secondRouteResult?.1
XCTAssertNotNil(secondRouteResult)
_ = secondRouterHandler?(request)
_ = await secondRouterHandler?(request)

waitForExpectations(timeout: 10, handler: nil)
await waitForExpectations(timeout: 10, handler: nil)
XCTAssertTrue(foundFirstVariableRoute)
XCTAssertTrue(foundSecondVariableRoute)
}
Expand Down