From 7a263ad74cf8de2e47164728e277fac45ba349ca Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Sat, 1 Jan 2022 17:45:58 -0500 Subject: [PATCH 1/6] Add async/await support --- Sources/Request/Request/Request.swift | 16 +++++++++ Tests/LinuxMain.swift | 8 +---- Tests/RequestTests/RequestTests.swift | 42 ++++++++++++------------ Tests/RequestTests/XCTestManifests.swift | 10 ------ 4 files changed, 38 insertions(+), 38 deletions(-) delete mode 100644 Tests/RequestTests/XCTestManifests.swift diff --git a/Sources/Request/Request/Request.swift b/Sources/Request/Request/Request.swift index 867b41a..a90692d 100644 --- a/Sources/Request/Request/Request.swift +++ b/Sources/Request/Request/Request.swift @@ -106,6 +106,22 @@ public struct AnyRequest where ResponseType: Decodable { .subscribe(UpdateSubscriber(request: self)) } } + + #if swift(>=5.5) + /// Performs the `Request`, then returns the `ResponseType` or throws. + public func call() async throws -> ResponseType { + return try await withCheckedThrowingContinuation { continuation in + self + .onObject(continuation.resume(returning:)) + .onError(continuation.resume(throwing:)) + .call() + } + } + + public func callAsFunction() async throws -> ResponseType { + return try await call() + } + #endif internal func buildSession() -> (configuration: URLSessionConfiguration, request: URLRequest) { var request = URLRequest(url: URL(string: "https://")!) diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 067fda8..1c96c53 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -1,7 +1 @@ -import XCTest - -import RequestTests - -var tests = [XCTestCaseEntry]() -tests += RequestTests.allTests() -XCTMain(tests) +fatalError("Test with the `--enable-test-discovery` flag.") diff --git a/Tests/RequestTests/RequestTests.swift b/Tests/RequestTests/RequestTests.swift index da51bc8..e9cd678 100644 --- a/Tests/RequestTests/RequestTests.swift +++ b/Tests/RequestTests/RequestTests.swift @@ -701,26 +701,26 @@ final class RequestTests: XCTestCase { waitForExpectations(timeout: 10000) } - - static var allTests = [ - ("simpleRequest", testSimpleRequest), - ("post", testPost), - ("query", testQuery), - ("complexRequest", testComplexRequest), - ("headers", testHeaders), - ("onObject", testObject), - ("onStatusCode", testStatusCode), - ("onString", testString), - ("onJson", testJson), - ("requestGroup", testRequestGroup), - ("requestChain", testRequestChain), - ("requestChainErrors", testRequestChainErrors), - ("anyRequest", testAnyRequest), - ("testError", testError), - ("testUpdate", testUpdate), + + #if swift(>=5.5) + func testAsync() async throws { + struct Todo: Decodable, Hashable { + let id: Int + let userId: Int + let title: String + let completed: Bool + } + + let getTodos = AnyRequest<[Todo]> { + Url("https://jsonplaceholder.typicode.com/todos") + } - ("testPublisher", testPublisher), - ("testPublisherDecode", testPublisherDecode), - ("testPublisherGroup", testPublisherGroup) - ] + // Test out `async let`, `try await`, and `callAsFunction`. + async let callTodos = getTodos.call() + async let callAsFunctionTodos = getTodos() + let calledTodos = try await callTodos + let calledAsFunctionTodos = try await callAsFunctionTodos + XCTAssertEqual(calledTodos, calledAsFunctionTodos) + } + #endif } diff --git a/Tests/RequestTests/XCTestManifests.swift b/Tests/RequestTests/XCTestManifests.swift deleted file mode 100644 index b4b4015..0000000 --- a/Tests/RequestTests/XCTestManifests.swift +++ /dev/null @@ -1,10 +0,0 @@ -import XCTest - -#if !canImport(ObjectiveC) -public func allTests() -> [XCTestCaseEntry] { - return [ - testCase(RequestTests.allTests), - testCase(JsonTests.allTests) - ] -} -#endif From d87dfd55f74083b842c43abf25bf093f92a72d03 Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Tue, 26 Jul 2022 12:50:39 -0400 Subject: [PATCH 2/6] Add buildArray for for loops inside Request --- Sources/Request/Request/RequestBuilder.swift | 4 ++++ Sources/Request/Request/RequestParams/Header.swift | 12 +++++++++++- Tests/RequestTests/RequestTests.swift | 10 ++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/Sources/Request/Request/RequestBuilder.swift b/Sources/Request/Request/RequestBuilder.swift index aa0036d..f14ceb3 100644 --- a/Sources/Request/Request/RequestBuilder.swift +++ b/Sources/Request/Request/RequestBuilder.swift @@ -32,4 +32,8 @@ public struct RequestBuilder { public static func buildEither(second: RequestParam) -> RequestParam { second } + + public static func buildArray(_ components: [RequestParam]) -> RequestParam { + CombinedParams(children: components) + } } diff --git a/Sources/Request/Request/RequestParams/Header.swift b/Sources/Request/Request/RequestParams/Header.swift index 5db81b9..da8a70c 100644 --- a/Sources/Request/Request/RequestParams/Header.swift +++ b/Sources/Request/Request/RequestParams/Header.swift @@ -8,7 +8,17 @@ import Foundation /// Creates a `HeaderParam` for any number of different headers -public struct Header { +public struct Header: RequestParam { + private let _backing: `Any` + + public init(key: String, value: String) { + self._backing = .init(key: key, value: value) + } + + public func buildParam(_ request: inout URLRequest) { + _backing.buildParam(&request) + } + /// Sets the value for any header public struct `Any`: RequestParam { private let key: String diff --git a/Tests/RequestTests/RequestTests.swift b/Tests/RequestTests/RequestTests.swift index e9cd678..d76e58c 100644 --- a/Tests/RequestTests/RequestTests.swift +++ b/Tests/RequestTests/RequestTests.swift @@ -120,6 +120,16 @@ final class RequestTests: XCTestCase { Query([QueryParam("key", value: "value"), QueryParam("key2", value: "value2")]) }) } + + func testBuildArray() { + let headers = ["Content-Type": "application/json", "Cache-Control": "no-cache"] + performRequest(Request { + Url("https://jsonplaceholder.typicode.com/todos") + for (key, value) in headers { + Header(key: key, value: value) + } + }) + } func testComplexRequest() { performRequest(Request { From 3329c4cdecdb29c8ea641dd5af3fa3fbe157bc82 Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Tue, 26 Jul 2022 12:52:08 -0400 Subject: [PATCH 3/6] Support Foundation.URL as a RequestParam --- .../Request/RequestParams/Url/URL+RequestParam.swift | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 Sources/Request/Request/RequestParams/Url/URL+RequestParam.swift diff --git a/Sources/Request/Request/RequestParams/Url/URL+RequestParam.swift b/Sources/Request/Request/RequestParams/Url/URL+RequestParam.swift new file mode 100644 index 0000000..c8ab442 --- /dev/null +++ b/Sources/Request/Request/RequestParams/Url/URL+RequestParam.swift @@ -0,0 +1,7 @@ +import Foundation + +extension Foundation.URL: RequestParam { + public func buildParam(_ request: inout URLRequest) { + request.url = self + } +} From 6668dd78c4734fb07bcd126454d007518357a011 Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Tue, 26 Jul 2022 14:10:58 -0400 Subject: [PATCH 4/6] Make EmptyParam.init public --- Sources/Request/Request/RequestParams/EmptyParam.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/Request/Request/RequestParams/EmptyParam.swift b/Sources/Request/Request/RequestParams/EmptyParam.swift index 83c6335..db533a2 100644 --- a/Sources/Request/Request/RequestParams/EmptyParam.swift +++ b/Sources/Request/Request/RequestParams/EmptyParam.swift @@ -8,6 +8,8 @@ import Foundation public struct EmptyParam: RequestParam { + public init() {} + public func buildParam(_ request: inout URLRequest) {} } From 47de957df470ddd289d522c127dd3faa3bfb44b5 Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Tue, 26 Jul 2022 14:39:22 -0400 Subject: [PATCH 5/6] Resolved continuation leaking error --- Sources/Request/Request/Request.swift | 22 ++++++++++++--- .../RequestParams/CombinedParams.swift | 2 +- Tests/RequestTests/RequestTests.swift | 27 ++++++++++++++----- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/Sources/Request/Request/Request.swift b/Sources/Request/Request/Request.swift index a90692d..8b013d7 100644 --- a/Sources/Request/Request/Request.swift +++ b/Sources/Request/Request/Request.swift @@ -110,11 +110,25 @@ public struct AnyRequest where ResponseType: Decodable { #if swift(>=5.5) /// Performs the `Request`, then returns the `ResponseType` or throws. public func call() async throws -> ResponseType { + let publisher = self.buildPublisher() + return try await withCheckedThrowingContinuation { continuation in - self - .onObject(continuation.resume(returning:)) - .onError(continuation.resume(throwing:)) - .call() + var cancellable: AnyCancellable? + cancellable = publisher.sink { completion in + switch completion { + case .finished: + break + case let .failure(error): + continuation.resume(throwing: error) + } + cancellable?.cancel() + } receiveValue: { value in + do { + continuation.resume(returning: try JSONDecoder().decode(ResponseType.self, from: value.data)) + } catch { + continuation.resume(throwing: error) + } + } } } diff --git a/Sources/Request/Request/RequestParams/CombinedParams.swift b/Sources/Request/Request/RequestParams/CombinedParams.swift index 0e0ad40..54097d7 100644 --- a/Sources/Request/Request/RequestParams/CombinedParams.swift +++ b/Sources/Request/Request/RequestParams/CombinedParams.swift @@ -16,7 +16,7 @@ internal struct CombinedParams: RequestParam, SessionParam { func buildParam(_ request: inout URLRequest) { children - .sorted { a, _ in (a is Url) } + .sorted { a, _ in (a is Url) || (a is Foundation.URL) } .filter { !($0 is SessionParam) || $0 is CombinedParams } .forEach { $0.buildParam(&request) diff --git a/Tests/RequestTests/RequestTests.swift b/Tests/RequestTests/RequestTests.swift index d76e58c..db33345 100644 --- a/Tests/RequestTests/RequestTests.swift +++ b/Tests/RequestTests/RequestTests.swift @@ -30,6 +30,12 @@ final class RequestTests: XCTestCase { Url("https://jsonplaceholder.typicode.com/todos") }) } + + func testFoundationURL() { + performRequest(Request { + URL(string: "https://jsonplaceholder.typicode.com/todos")! + }) + } func testRequestWithCondition() { let condition = true @@ -713,13 +719,14 @@ final class RequestTests: XCTestCase { } #if swift(>=5.5) + struct Todo: Decodable, Hashable { + let id: Int + let userId: Int + let title: String + let completed: Bool + } + func testAsync() async throws { - struct Todo: Decodable, Hashable { - let id: Int - let userId: Int - let title: String - let completed: Bool - } let getTodos = AnyRequest<[Todo]> { Url("https://jsonplaceholder.typicode.com/todos") @@ -732,5 +739,13 @@ final class RequestTests: XCTestCase { let calledAsFunctionTodos = try await callAsFunctionTodos XCTAssertEqual(calledTodos, calledAsFunctionTodos) } + + func testCallAsFunction() async throws { + let request = AnyRequest<[Todo]> { + Url("https://jsonplaceholder.typicode.com/todos") + } + + let _ = try await request() + } #endif } From 63a74545f19333a71f9d4943f1f8158e23d904f9 Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Tue, 26 Jul 2022 17:04:02 -0400 Subject: [PATCH 6/6] Add special case for raw-data decoding --- Sources/Request/Request/Request.swift | 6 +++++- Tests/RequestTests/RequestTests.swift | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/Request/Request/Request.swift b/Sources/Request/Request/Request.swift index 8b013d7..1f5ce85 100644 --- a/Sources/Request/Request/Request.swift +++ b/Sources/Request/Request/Request.swift @@ -124,7 +124,11 @@ public struct AnyRequest where ResponseType: Decodable { cancellable?.cancel() } receiveValue: { value in do { - continuation.resume(returning: try JSONDecoder().decode(ResponseType.self, from: value.data)) + if ResponseType.self == Data.self { + continuation.resume(returning: value.data as! ResponseType) + } else { + continuation.resume(returning: try JSONDecoder().decode(ResponseType.self, from: value.data)) + } } catch { continuation.resume(throwing: error) } diff --git a/Tests/RequestTests/RequestTests.swift b/Tests/RequestTests/RequestTests.swift index db33345..14d9ec1 100644 --- a/Tests/RequestTests/RequestTests.swift +++ b/Tests/RequestTests/RequestTests.swift @@ -741,7 +741,7 @@ final class RequestTests: XCTestCase { } func testCallAsFunction() async throws { - let request = AnyRequest<[Todo]> { + let request = Request { Url("https://jsonplaceholder.typicode.com/todos") }