Skip to content

Commit 67215d5

Browse files
authored
Add async/await shim (#8)
* 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
1 parent dadbdeb commit 67215d5

File tree

5 files changed

+200
-22
lines changed

5 files changed

+200
-22
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the StructuredAPIClient open source project
4+
//
5+
// Copyright (c) Stairtree GmbH
6+
// Licensed under the MIT license
7+
//
8+
// See LICENSE.txt for license information
9+
//
10+
// SPDX-License-Identifier: MIT
11+
//
12+
//===----------------------------------------------------------------------===//
13+
14+
import Foundation
15+
#if canImport(FoundationNetworking)
16+
import FoundationNetworking
17+
#endif
18+
19+
#if compiler(>=5.5) && canImport(_Concurrency) && canImport(Darwin)
20+
21+
@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
22+
extension NetworkClient {
23+
public func load<Request: NetworkRequest>(_ req: Request) async throws -> Request.ResponseDataType {
24+
try await withCheckedThrowingContinuation { continuation in
25+
self.load(req) { switch $0 {
26+
case .success(let value): continuation.resume(returning: value)
27+
case .failure(let error): continuation.resume(throwing: error)
28+
} }
29+
}
30+
}
31+
}
32+
33+
#endif
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the StructuredAPIClient open source project
4+
//
5+
// Copyright (c) Stairtree GmbH
6+
// Licensed under the MIT license
7+
//
8+
// See LICENSE.txt for license information
9+
//
10+
// SPDX-License-Identifier: MIT
11+
//
12+
//===----------------------------------------------------------------------===//
13+
14+
import Foundation
15+
#if canImport(FoundationNetworking)
16+
import FoundationNetworking
17+
#endif
18+
19+
#if compiler(>=5.5) && canImport(_Concurrency) && canImport(Darwin)
20+
21+
@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
22+
extension URLSessionTransport {
23+
24+
/// Sends the request using a `URLSessionDataTask`
25+
/// - Parameter request: The configured request to send.
26+
/// - Returns: The received response from the server.
27+
public func send(request: URLRequest) async throws -> TransportResponse {
28+
do {
29+
let (data, response) = try await session.data(for: request)
30+
guard let httpResponse = response as? HTTPURLResponse else {
31+
throw TransportFailure.network(URLError(.unsupportedURL))
32+
}
33+
return httpResponse.asTransportResponse(withData: data)
34+
35+
} catch let netError as URLError {
36+
if netError.code == .cancelled { throw TransportFailure.cancelled }
37+
throw TransportFailure.network(netError)
38+
39+
} catch let error as TransportFailure {
40+
throw error
41+
42+
} catch {
43+
throw TransportFailure.unknown(error)
44+
}
45+
}
46+
}
47+
48+
#endif
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the StructuredAPIClient open source project
4+
//
5+
// Copyright (c) Stairtree GmbH
6+
// Licensed under the MIT license
7+
//
8+
// See LICENSE.txt for license information
9+
//
10+
// SPDX-License-Identifier: MIT
11+
//
12+
//===----------------------------------------------------------------------===//
13+
14+
import Foundation
15+
#if canImport(FoundationNetworking)
16+
import FoundationNetworking
17+
#endif
18+
19+
#if canImport(Combine)
20+
import Combine
21+
22+
@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *)
23+
extension URLSessionTransport {
24+
public func publisher(forRequest request: URLRequest) -> AnyPublisher<TransportResponse, Error> {
25+
return self.session.dataTaskPublisher(for: request)
26+
.mapError { netError -> Error in
27+
if netError.code == .cancelled { return TransportFailure.cancelled }
28+
else { return TransportFailure.network(netError) }
29+
}
30+
.tryMap { output in
31+
guard let response = output.response as? HTTPURLResponse else {
32+
throw TransportFailure.network(URLError(.unsupportedURL))
33+
}
34+
return response.asTransportResponse(withData: output.data)
35+
}
36+
.eraseToAnyPublisher()
37+
}
38+
}
39+
#endif
40+

Sources/StructuredAPIClient/Transport/URLSessionTransport.swift

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -61,28 +61,6 @@ public final class URLSessionTransport: Transport {
6161
}
6262
}
6363

64-
#if canImport(Combine)
65-
import Combine
66-
67-
@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *)
68-
extension URLSessionTransport {
69-
public func publisher(forRequest request: URLRequest) -> AnyPublisher<TransportResponse, Error> {
70-
return self.session.dataTaskPublisher(for: request)
71-
.mapError { netError -> Error in
72-
if netError.code == .cancelled { return TransportFailure.cancelled }
73-
else { return TransportFailure.network(netError) }
74-
}
75-
.tryMap { output in
76-
guard let response = output.response as? HTTPURLResponse else {
77-
throw TransportFailure.network(URLError(.unsupportedURL))
78-
}
79-
return response.asTransportResponse(withData: output.data)
80-
}
81-
.eraseToAnyPublisher()
82-
}
83-
}
84-
#endif
85-
8664
extension URLRequest {
8765
var debugString: String {
8866
"\(httpMethod.map { "[\($0)] " } ?? "")\(url.map { "\($0) " } ?? "")"
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the StructuredAPIClient open source project
4+
//
5+
// Copyright (c) Stairtree GmbH
6+
// Licensed under the MIT license
7+
//
8+
// See LICENSE.txt for license information
9+
//
10+
// SPDX-License-Identifier: MIT
11+
//
12+
//===----------------------------------------------------------------------===//
13+
14+
import XCTest
15+
#if canImport(FoundationNetworking)
16+
import FoundationNetworking
17+
#endif
18+
@testable import StructuredAPIClient
19+
import StructuredAPIClientTestSupport
20+
21+
#if compiler(>=5.5) && canImport(_Concurrency) && canImport(Darwin)
22+
23+
@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
24+
final class NetworkClientWithAsyncAwaitTests: XCTestCase {
25+
26+
func testNetworkClientWithAsyncAwait() async throws {
27+
struct TestRequest: NetworkRequest {
28+
func makeRequest(baseURL: URL) throws -> URLRequest { URLRequest(url: baseURL) }
29+
func parseResponse(_ response: TransportResponse) throws -> String { .init(decoding: response.body, as: UTF8.self) }
30+
}
31+
32+
let response: Result<TransportResponse, Error> = .success(.init(status: .ok, headers: [:], body: Data("Test".utf8)))
33+
34+
let requestAssertions: (URLRequest) -> Void = {
35+
XCTAssertEqual($0.url, URL(string: "https://test.somewhere.com")!)
36+
}
37+
38+
let client = NetworkClient(baseURL: URL(string: "https://test.somewhere.com")!, transport: TestTransport(responses: [response], assertRequest: requestAssertions))
39+
40+
let value = try await client.load(TestRequest())
41+
42+
XCTAssertEqual(value, "Test")
43+
XCTAssertEqual(client.baseURL.absoluteString, "https://test.somewhere.com")
44+
}
45+
46+
func testTokenAuthWithAsyncAwait() async throws {
47+
struct TestRequest: NetworkRequest {
48+
func makeRequest(baseURL: URL) throws -> URLRequest { URLRequest(url: baseURL) }
49+
func parseResponse(_ response: TransportResponse) throws -> String { .init(decoding: response.body, as: UTF8.self) }
50+
}
51+
52+
let accessToken = TestToken(raw: "abc", expiresAt: Date())
53+
let refreshToken = TestToken(raw: "def", expiresAt: Date())
54+
55+
let tokenProvider = TestTokenProvider(accessToken: accessToken, refreshToken: refreshToken)
56+
57+
let response: Result<TransportResponse, Error> = .success(.init(status: .ok, headers: [:], body: Data("Test".utf8)))
58+
59+
let requestAssertions: (URLRequest) -> Void = {
60+
XCTAssertEqual($0.url, URL(string: "https://test.somewhere.com")!)
61+
XCTAssertEqual($0.allHTTPHeaderFields?["Authorization"], "Bearer abc")
62+
}
63+
64+
let client = NetworkClient(
65+
baseURL: URL(string: "https://test.somewhere.com")!,
66+
transport: TokenAuthenticationHandler(
67+
base: TestTransport(responses: [response], assertRequest: requestAssertions),
68+
tokenProvider: tokenProvider
69+
)
70+
)
71+
72+
let value = try await client.load(TestRequest())
73+
74+
XCTAssertEqual(value, "Test")
75+
XCTAssertEqual(client.baseURL.absoluteString, "https://test.somewhere.com")
76+
}
77+
}
78+
79+
#endif

0 commit comments

Comments
 (0)