diff --git a/README.md b/README.md index 804434ba..4ac642bd 100644 --- a/README.md +++ b/README.md @@ -427,6 +427,14 @@ The response handlers for each request type are similar to one another. The argu Note: Response handlers are required for all endpoints. Progress handlers, on the other hand, are optional for all endpoints. +#### Swift Concurrency + +As of the 10.0.0 release, all of the request types also support Swift Concurrency (`async`/`await`) via the async `response()` function. + +```swift +let response = try await client.files.createFolder(path: "/test/path/in/Dropbox/account").response() +``` + --- ### Request types diff --git a/Source/SwiftyDropbox/Shared/Handwritten/Request+Async.swift b/Source/SwiftyDropbox/Shared/Handwritten/Request+Async.swift new file mode 100644 index 00000000..18e12892 --- /dev/null +++ b/Source/SwiftyDropbox/Shared/Handwritten/Request+Async.swift @@ -0,0 +1,51 @@ +// +// Copyright (c) 2023 Dropbox Inc. All rights reserved. +// + +import Foundation + +public protocol HasRequestResponse { + associatedtype ValueType + associatedtype ESerial: JSONSerializer + + @discardableResult + func response( + queue: DispatchQueue?, + completionHandler: @escaping (ValueType?, CallError?) -> Void + ) -> Self +} + +@available(iOS 13.0, macOS 10.15, *) +extension HasRequestResponse { + /// Async wrapper for retrieving a request's response + /// + /// This could have a better name, but this avoids a collision with the other `response` methods + public func responseResult( + ) async -> Result> { + await withCheckedContinuation { continuation in + self.response(queue: nil) { result, error in + if let result { + continuation.resume(returning: .success(result)) + } else if let error { + continuation.resume(returning: .failure(error)) + } else { + // this should never happen + continuation.resume(returning: .failure(.clientError(.unexpectedState))) + } + } + } + } + + /// Async wrapper for retrieving a request's response + /// + /// Same thing as `responseResult` but using async throws instead of returing a Result + public func response( + ) async throws -> ValueType { + try await responseResult().get() + } +} + +extension RpcRequest: HasRequestResponse {} +extension UploadRequest: HasRequestResponse {} +extension DownloadRequestFile: HasRequestResponse {} +extension DownloadRequestMemory: HasRequestResponse {} diff --git a/Source/SwiftyDropboxUnitTests/Request+Async.test.swift b/Source/SwiftyDropboxUnitTests/Request+Async.test.swift new file mode 100644 index 00000000..523a8460 --- /dev/null +++ b/Source/SwiftyDropboxUnitTests/Request+Async.test.swift @@ -0,0 +1,35 @@ +/// +/// Copyright (c) 2023 Dropbox, Inc. All rights reserved. +/// + +@testable import SwiftyDropbox +import XCTest + +@available(iOS 13.0, macOS 10.15, *) +final class RequestAsyncTests: XCTestCase { + func testRpcResponseFails() async throws { + let mockTransferClient = MockDropboxTransportClient() + mockTransferClient.mockRequestHandler = MockDropboxTransportClient.alwaysFailMockRequestHandler + let apiClient = DropboxClient(transportClient: mockTransferClient) + + let exp = expectation(description: "should fail") + do { + _ = try await apiClient.check.user().response() + XCTFail("This should fail") + } catch { + exp.fulfill() + } + await fulfillment(of: [exp]) + } + + func testRpcResponseSucceeds() async throws { + let mockTransferClient = MockDropboxTransportClient() + mockTransferClient.mockRequestHandler = { request in + try? request.handleMockInput(.success(json: [:])) + } + let apiClient = DropboxClient(transportClient: mockTransferClient) + + let userCheck = try await apiClient.check.user().response() + XCTAssertNotNil(userCheck) + } +}