Skip to content

Commit

Permalink
Add async/await shim (#8)
Browse files Browse the repository at this point in the history
* Add async/await shim

* Add async/await tests

* Add an async/await shim for URLSessionTransport

* Guard Async/Await with compiler version check

Also, rearrange some code into new files.

* Restrict Async/Await to Darwin

… until URLSession APIs are implemented in swift-corelibs-foundation. See https://forums.swift.org/t/how-to-use-async-await-w-docker/49591/7
  • Loading branch information
tkrajacic authored Oct 31, 2021
1 parent dadbdeb commit 67215d5
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 22 deletions.
33 changes: 33 additions & 0 deletions Sources/StructuredAPIClient/NetworkClient+AsyncAwait.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the StructuredAPIClient open source project
//
// Copyright (c) Stairtree GmbH
// Licensed under the MIT license
//
// See LICENSE.txt for license information
//
// SPDX-License-Identifier: MIT
//
//===----------------------------------------------------------------------===//

import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

#if compiler(>=5.5) && canImport(_Concurrency) && canImport(Darwin)

@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
extension NetworkClient {
public func load<Request: NetworkRequest>(_ req: Request) async throws -> Request.ResponseDataType {
try await withCheckedThrowingContinuation { continuation in
self.load(req) { switch $0 {
case .success(let value): continuation.resume(returning: value)
case .failure(let error): continuation.resume(throwing: error)
} }
}
}
}

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the StructuredAPIClient open source project
//
// Copyright (c) Stairtree GmbH
// Licensed under the MIT license
//
// See LICENSE.txt for license information
//
// SPDX-License-Identifier: MIT
//
//===----------------------------------------------------------------------===//

import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

#if compiler(>=5.5) && canImport(_Concurrency) && canImport(Darwin)

@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
extension URLSessionTransport {

/// Sends the request using a `URLSessionDataTask`
/// - Parameter request: The configured request to send.
/// - Returns: The received response from the server.
public func send(request: URLRequest) async throws -> TransportResponse {
do {
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw TransportFailure.network(URLError(.unsupportedURL))
}
return httpResponse.asTransportResponse(withData: data)

} catch let netError as URLError {
if netError.code == .cancelled { throw TransportFailure.cancelled }
throw TransportFailure.network(netError)

} catch let error as TransportFailure {
throw error

} catch {
throw TransportFailure.unknown(error)
}
}
}

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the StructuredAPIClient open source project
//
// Copyright (c) Stairtree GmbH
// Licensed under the MIT license
//
// See LICENSE.txt for license information
//
// SPDX-License-Identifier: MIT
//
//===----------------------------------------------------------------------===//

import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

#if canImport(Combine)
import Combine

@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *)
extension URLSessionTransport {
public func publisher(forRequest request: URLRequest) -> AnyPublisher<TransportResponse, Error> {
return self.session.dataTaskPublisher(for: request)
.mapError { netError -> Error in
if netError.code == .cancelled { return TransportFailure.cancelled }
else { return TransportFailure.network(netError) }
}
.tryMap { output in
guard let response = output.response as? HTTPURLResponse else {
throw TransportFailure.network(URLError(.unsupportedURL))
}
return response.asTransportResponse(withData: output.data)
}
.eraseToAnyPublisher()
}
}
#endif

22 changes: 0 additions & 22 deletions Sources/StructuredAPIClient/Transport/URLSessionTransport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,28 +61,6 @@ public final class URLSessionTransport: Transport {
}
}

#if canImport(Combine)
import Combine

@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *)
extension URLSessionTransport {
public func publisher(forRequest request: URLRequest) -> AnyPublisher<TransportResponse, Error> {
return self.session.dataTaskPublisher(for: request)
.mapError { netError -> Error in
if netError.code == .cancelled { return TransportFailure.cancelled }
else { return TransportFailure.network(netError) }
}
.tryMap { output in
guard let response = output.response as? HTTPURLResponse else {
throw TransportFailure.network(URLError(.unsupportedURL))
}
return response.asTransportResponse(withData: output.data)
}
.eraseToAnyPublisher()
}
}
#endif

extension URLRequest {
var debugString: String {
"\(httpMethod.map { "[\($0)] " } ?? "")\(url.map { "\($0) " } ?? "")"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the StructuredAPIClient open source project
//
// Copyright (c) Stairtree GmbH
// Licensed under the MIT license
//
// See LICENSE.txt for license information
//
// SPDX-License-Identifier: MIT
//
//===----------------------------------------------------------------------===//

import XCTest
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
@testable import StructuredAPIClient
import StructuredAPIClientTestSupport

#if compiler(>=5.5) && canImport(_Concurrency) && canImport(Darwin)

@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
final class NetworkClientWithAsyncAwaitTests: XCTestCase {

func testNetworkClientWithAsyncAwait() async throws {
struct TestRequest: NetworkRequest {
func makeRequest(baseURL: URL) throws -> URLRequest { URLRequest(url: baseURL) }
func parseResponse(_ response: TransportResponse) throws -> String { .init(decoding: response.body, as: UTF8.self) }
}

let response: Result<TransportResponse, Error> = .success(.init(status: .ok, headers: [:], body: Data("Test".utf8)))

let requestAssertions: (URLRequest) -> Void = {
XCTAssertEqual($0.url, URL(string: "https://test.somewhere.com")!)
}

let client = NetworkClient(baseURL: URL(string: "https://test.somewhere.com")!, transport: TestTransport(responses: [response], assertRequest: requestAssertions))

let value = try await client.load(TestRequest())

XCTAssertEqual(value, "Test")
XCTAssertEqual(client.baseURL.absoluteString, "https://test.somewhere.com")
}

func testTokenAuthWithAsyncAwait() async throws {
struct TestRequest: NetworkRequest {
func makeRequest(baseURL: URL) throws -> URLRequest { URLRequest(url: baseURL) }
func parseResponse(_ response: TransportResponse) throws -> String { .init(decoding: response.body, as: UTF8.self) }
}

let accessToken = TestToken(raw: "abc", expiresAt: Date())
let refreshToken = TestToken(raw: "def", expiresAt: Date())

let tokenProvider = TestTokenProvider(accessToken: accessToken, refreshToken: refreshToken)

let response: Result<TransportResponse, Error> = .success(.init(status: .ok, headers: [:], body: Data("Test".utf8)))

let requestAssertions: (URLRequest) -> Void = {
XCTAssertEqual($0.url, URL(string: "https://test.somewhere.com")!)
XCTAssertEqual($0.allHTTPHeaderFields?["Authorization"], "Bearer abc")
}

let client = NetworkClient(
baseURL: URL(string: "https://test.somewhere.com")!,
transport: TokenAuthenticationHandler(
base: TestTransport(responses: [response], assertRequest: requestAssertions),
tokenProvider: tokenProvider
)
)

let value = try await client.load(TestRequest())

XCTAssertEqual(value, "Test")
XCTAssertEqual(client.baseURL.absoluteString, "https://test.somewhere.com")
}
}

#endif

0 comments on commit 67215d5

Please sign in to comment.