From 7e51331a6ad9b63fd3b822f8ffdbd37a414c4d18 Mon Sep 17 00:00:00 2001 From: Simon Blum Date: Fri, 7 Jan 2022 15:45:36 +0100 Subject: [PATCH 01/10] Add GET functionality with async await --- Package.swift | 2 +- Sources/Jetworking/Client/Client.swift | 24 ++++++++ .../AsyncRequestExecuter.swift | 5 ++ .../RequestExecuter/RequestExecuter.swift | 3 + .../SyncRequestExecuter.swift | 5 ++ .../Jetworking/Client/ResponseHandler.swift | 57 +++++++++++++++++++ .../JetworkingTests/Mocks/MockExecuter.swift | 5 ++ 7 files changed, 100 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index b5463bd..ecb7a74 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.5 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription diff --git a/Sources/Jetworking/Client/Client.swift b/Sources/Jetworking/Client/Client.swift index fc96a99..76bdb7e 100644 --- a/Sources/Jetworking/Client/Client.swift +++ b/Sources/Jetworking/Client/Client.swift @@ -12,6 +12,7 @@ public enum APIError: Error { } public final class Client { + public typealias RequestResult = (HTTPURLResponse?, Result) public typealias RequestCompletion = (HTTPURLResponse?, Result) -> Void // MARK: - Properties @@ -127,6 +128,29 @@ public final class Client { return nil } + @available(iOS 15.0, macOS 12.0, *) + public func get( + endpoint: Endpoint, + andAdditionalHeaderFields additionalHeaderFields: [String: String] = [:] + ) async -> RequestResult { + do { + let request: URLRequest = try createRequest( + forHttpMethod: .GET, + and: endpoint, + andAdditionalHeaderFields: additionalHeaderFields + ) + + let (data, urlResponse) = try await requestExecuter.send(request: request, delegate: nil) + return await responseHandler.handleDecodableResponse( + data: data, + urlResponse: urlResponse, + endpoint: endpoint + ) + } catch { + return (nil, .failure(error)) + } + } + @discardableResult public func post( endpoint: Endpoint, diff --git a/Sources/Jetworking/Client/Executor/RequestExecuter/AsyncRequestExecuter/AsyncRequestExecuter.swift b/Sources/Jetworking/Client/Executor/RequestExecuter/AsyncRequestExecuter/AsyncRequestExecuter.swift index 0ae2383..2708e7e 100644 --- a/Sources/Jetworking/Client/Executor/RequestExecuter/AsyncRequestExecuter/AsyncRequestExecuter.swift +++ b/Sources/Jetworking/Client/Executor/RequestExecuter/AsyncRequestExecuter/AsyncRequestExecuter.swift @@ -13,4 +13,9 @@ final class AsyncRequestExecuter: RequestExecuter { return dataTask } + + @available(iOS 15.0, macOS 12.0, *) + func send(request: URLRequest, delegate: URLSessionTaskDelegate? = nil) async throws -> (Data?, URLResponse?) { + return try await session.data(for: request, delegate: delegate) + } } diff --git a/Sources/Jetworking/Client/Executor/RequestExecuter/RequestExecuter.swift b/Sources/Jetworking/Client/Executor/RequestExecuter/RequestExecuter.swift index 830aba1..28fe755 100644 --- a/Sources/Jetworking/Client/Executor/RequestExecuter/RequestExecuter.swift +++ b/Sources/Jetworking/Client/Executor/RequestExecuter/RequestExecuter.swift @@ -28,4 +28,7 @@ public protocol RequestExecuter { * The request to be able to cancel it if necessary. */ func send(request: URLRequest, _ completion: @escaping ((Data?, URLResponse?, Error?) -> Void)) -> CancellableRequest? + + @available(iOS 15.0, macOS 12.0, *) + func send(request: URLRequest, delegate: URLSessionTaskDelegate?) async throws -> (Data?, URLResponse?) } diff --git a/Sources/Jetworking/Client/Executor/RequestExecuter/SyncRequestExecuter/SyncRequestExecuter.swift b/Sources/Jetworking/Client/Executor/RequestExecuter/SyncRequestExecuter/SyncRequestExecuter.swift index b2c6362..3291b59 100644 --- a/Sources/Jetworking/Client/Executor/RequestExecuter/SyncRequestExecuter/SyncRequestExecuter.swift +++ b/Sources/Jetworking/Client/Executor/RequestExecuter/SyncRequestExecuter/SyncRequestExecuter.swift @@ -19,4 +19,9 @@ final class SyncRequestExecuter: RequestExecuter { return operation } + + @available(iOS 15.0, macOS 12.0, *) + func send(request: URLRequest, delegate: URLSessionTaskDelegate?) async throws -> (Data?, URLResponse?) { + return (nil, nil) + } } diff --git a/Sources/Jetworking/Client/ResponseHandler.swift b/Sources/Jetworking/Client/ResponseHandler.swift index 0cd5aa1..1c183f2 100644 --- a/Sources/Jetworking/Client/ResponseHandler.swift +++ b/Sources/Jetworking/Client/ResponseHandler.swift @@ -29,6 +29,15 @@ final class ResponseHandler { evaluate(data: data, urlResponse: urlResponse, error: error, endpoint: endpoint, completionWrapper: makeDecodableCompletionWrapper, completion: completion) } + @available(iOS 15.0, macOS 12.0, *) + func handleDecodableResponse( + data: Data?, + urlResponse: URLResponse?, + endpoint: Endpoint? = nil + ) async -> (HTTPURLResponse?, Result) { + await evaluate(data: data, urlResponse: urlResponse, endpoint: endpoint) + } + private func makeVoidCompletionWrapper( currentURLResponse: HTTPURLResponse?, data: Data?, @@ -121,6 +130,54 @@ final class ResponseHandler { } } + @available(iOS 13.0.0, macOS 12.0, *) + private func evaluate( + data: Data?, + urlResponse: URLResponse?, + endpoint: Endpoint? = nil + ) async -> (HTTPURLResponse?, Result) { + let interceptedResponse = configuration.interceptors.reduce(urlResponse) { response, component in + return component.intercept(response: response, data: data, error: nil) + } + + guard let currentURLResponse = interceptedResponse as? HTTPURLResponse else { + return (nil, .failure(APIError.responseMissing)) + } + + switch HTTPStatusCodeType(statusCode: currentURLResponse.statusCode) { + case .successful: + guard let data = data else { return (nil, .failure(APIError.missingResponseBody)) } + let decoder = endpoint?.decoder ?? configuration.decoder + do { + let responseType = try decoder.decode(ResponseType.self, from: data) + return (currentURLResponse, .success(responseType)) + } catch { + return (nil, .failure(APIError.decodingError(error))) + } + + case .serverError: + let apiError: APIError = APIError.serverError( + statusCode: currentURLResponse.statusCode, + error: nil, + body: data + ) + + return (currentURLResponse, .failure(apiError)) + + case .clientError: + let apiError: APIError = APIError.clientError( + statusCode: currentURLResponse.statusCode, + error: nil, + body: data + ) + + return (currentURLResponse, .failure(apiError)) + + default: + return (nil, .failure(APIError.unexpectedError)) + } + } + private func enqueue(_ completion: @escaping @autoclosure () -> Void, inDispatchQueue queue: DispatchQueue) { queue.async { completion() diff --git a/Tests/JetworkingTests/Mocks/MockExecuter.swift b/Tests/JetworkingTests/Mocks/MockExecuter.swift index 63d0836..204ca5c 100644 --- a/Tests/JetworkingTests/Mocks/MockExecuter.swift +++ b/Tests/JetworkingTests/Mocks/MockExecuter.swift @@ -54,6 +54,11 @@ final class MockExecuter: RequestExecuter { } } + @available(iOS 15.0, macOS 12.0, *) + func send(request: URLRequest, delegate: URLSessionTaskDelegate?) async throws -> (Data?, URLResponse?) { + return (nil, nil) + } + private func execute( request: URLRequest, completion: @escaping ((Data?, URLResponse?, Error?) -> Void) From b42cba0bac24fd741aa3f2bed94ddad1d165aba7 Mon Sep 17 00:00:00 2001 From: Simon Blum Date: Fri, 14 Jan 2022 15:51:03 +0100 Subject: [PATCH 02/10] Add POST functionality with async await --- Sources/Jetworking/Client/Client.swift | 53 ++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/Sources/Jetworking/Client/Client.swift b/Sources/Jetworking/Client/Client.swift index 76bdb7e..532d048 100644 --- a/Sources/Jetworking/Client/Client.swift +++ b/Sources/Jetworking/Client/Client.swift @@ -184,6 +184,34 @@ public final class Client { return nil } + + @available(iOS 15.0, macOS 12.0, *) + @discardableResult + public func post( + endpoint: Endpoint, + body: BodyType, + andAdditionalHeaderFields additionalHeaderFields: [String: String] = [:] + ) async -> RequestResult { + do { + let encoder: Encoder = endpoint.encoder ?? configuration.encoder + let bodyData: Data = try encoder.encode(body) + let request: URLRequest = try createRequest( + forHttpMethod: .POST, + and: endpoint, + and: bodyData, + andAdditionalHeaderFields: additionalHeaderFields + ) + + let (data, urlResponse) = try await requestExecuter.send(request: request, delegate: nil) + return await responseHandler.handleDecodableResponse( + data: data, + urlResponse: urlResponse, + endpoint: endpoint + ) + } catch { + return (nil, .failure(error)) + } + } @discardableResult public func post( @@ -216,6 +244,31 @@ public final class Client { return nil } + @available(iOS 15.0, macOS 12.0, *) + @discardableResult + public func post( + endpoint: Endpoint, + body: ExpressibleByNilLiteral? = nil, + andAdditionalHeaderFields additionalHeaderFields: [String: String] = [:] + ) async -> RequestResult { + do { + let request: URLRequest = try createRequest( + forHttpMethod: .POST, + and: endpoint, + andAdditionalHeaderFields: additionalHeaderFields + ) + + let (data, urlResponse) = try await requestExecuter.send(request: request, delegate: nil) + return await responseHandler.handleDecodableResponse( + data: data, + urlResponse: urlResponse, + endpoint: endpoint + ) + } catch { + return (nil, .failure(error)) + } + } + @discardableResult public func put( endpoint: Endpoint, From 8d545080149e256acd2182205abcdb7eda9e4aea Mon Sep 17 00:00:00 2001 From: Simon Blum Date: Fri, 14 Jan 2022 15:58:07 +0100 Subject: [PATCH 03/10] Add PUT functionality with async await --- Sources/Jetworking/Client/Client.swift | 38 ++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/Sources/Jetworking/Client/Client.swift b/Sources/Jetworking/Client/Client.swift index 532d048..3e9339d 100644 --- a/Sources/Jetworking/Client/Client.swift +++ b/Sources/Jetworking/Client/Client.swift @@ -14,7 +14,7 @@ public enum APIError: Error { public final class Client { public typealias RequestResult = (HTTPURLResponse?, Result) public typealias RequestCompletion = (HTTPURLResponse?, Result) -> Void - + // MARK: - Properties private lazy var sessionCache: SessionCache = .init(configuration: configuration) @@ -184,7 +184,7 @@ public final class Client { return nil } - + @available(iOS 15.0, macOS 12.0, *) @discardableResult public func post( @@ -303,6 +303,34 @@ public final class Client { return nil } + @available(iOS 15.0, macOS 12.0, *) + @discardableResult + public func put( + endpoint: Endpoint, + body: BodyType, + andAdditionalHeaderFields additionalHeaderFields: [String: String] = [:] + ) async -> RequestResult { + do { + let encoder: Encoder = endpoint.encoder ?? configuration.encoder + let bodyData: Data = try encoder.encode(body) + let request: URLRequest = try createRequest( + forHttpMethod: .PUT, + and: endpoint, + and: bodyData, + andAdditionalHeaderFields: additionalHeaderFields + ) + + let (data, urlResponse) = try await requestExecuter.send(request: request, delegate: nil) + return await responseHandler.handleDecodableResponse( + data: data, + urlResponse: urlResponse, + endpoint: endpoint + ) + } catch { + return (nil, .failure(error)) + } + } + @discardableResult public func patch( endpoint: Endpoint, @@ -457,7 +485,7 @@ public final class Client { } return task } - + private func checkForValidDownloadURL(_ url: URL) -> Bool { guard let scheme = URLComponents(string: url.absoluteString)?.scheme else { return false } @@ -578,13 +606,13 @@ extension Client: UploadExecuterDelegate { guard let progressHandler = executingUploads[uploadTask.identifier]?.progressHandler else { return } enqueue(progressHandler(totalBytesSent, totalBytesExpectedToSend)) } - + public func uploadExecuter(didFinishWith uploadTask: URLSessionUploadTask) { // TODO handle response before calling the completion guard let completionHandler = executingUploads[uploadTask.identifier]?.completionHandler else { return } enqueue(completionHandler(uploadTask.response, uploadTask.error)) } - + public func uploadExecuter(_ uploadTask: URLSessionUploadTask, didCompleteWithError error: Error?) { // TODO handle response before calling the completion guard let completionHandler = executingUploads[uploadTask.identifier]?.completionHandler else { return } From d983d9f8a0484bc1f0567eb491d56a552919c642 Mon Sep 17 00:00:00 2001 From: Simon Blum Date: Fri, 14 Jan 2022 15:59:27 +0100 Subject: [PATCH 04/10] Add PATCH functionality with async await --- Sources/Jetworking/Client/Client.swift | 28 ++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Sources/Jetworking/Client/Client.swift b/Sources/Jetworking/Client/Client.swift index 3e9339d..cb37e02 100644 --- a/Sources/Jetworking/Client/Client.swift +++ b/Sources/Jetworking/Client/Client.swift @@ -365,6 +365,34 @@ public final class Client { return nil } + @available(iOS 15.0, macOS 12.0, *) + @discardableResult + public func patch( + endpoint: Endpoint, + body: BodyType, + andAdditionalHeaderFields additionalHeaderFields: [String: String] = [:] + ) async -> RequestResult { + do { + let encoder: Encoder = endpoint.encoder ?? configuration.encoder + let bodyData: Data = try encoder.encode(body) + let request: URLRequest = try createRequest( + forHttpMethod: .PATCH, + and: endpoint, + and: bodyData, + andAdditionalHeaderFields: additionalHeaderFields + ) + + let (data, urlResponse) = try await requestExecuter.send(request: request, delegate: nil) + return await responseHandler.handleDecodableResponse( + data: data, + urlResponse: urlResponse, + endpoint: endpoint + ) + } catch { + return (nil, .failure(error)) + } + } + @discardableResult public func delete( endpoint: Endpoint, From 20829558d72bc648fa0c489c24f58a47d6bb2620 Mon Sep 17 00:00:00 2001 From: Simon Blum Date: Fri, 14 Jan 2022 16:00:37 +0100 Subject: [PATCH 05/10] Add DELETE functionality with async await --- Sources/Jetworking/Client/Client.swift | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Sources/Jetworking/Client/Client.swift b/Sources/Jetworking/Client/Client.swift index cb37e02..2e4a53a 100644 --- a/Sources/Jetworking/Client/Client.swift +++ b/Sources/Jetworking/Client/Client.swift @@ -424,6 +424,31 @@ public final class Client { return nil } + @available(iOS 15.0, macOS 12.0, *) + @discardableResult + public func delete( + endpoint: Endpoint, + parameter: [String: Any] = [:], + andAdditionalHeaderFields additionalHeaderFields: [String: String] = [:] + ) async -> RequestResult { + do { + let request: URLRequest = try createRequest( + forHttpMethod: .DELETE, + and: endpoint, + andAdditionalHeaderFields: additionalHeaderFields + ) + + let (data, urlResponse) = try await requestExecuter.send(request: request, delegate: nil) + return await responseHandler.handleDecodableResponse( + data: data, + urlResponse: urlResponse, + endpoint: endpoint + ) + } catch { + return (nil, .failure(error)) + } + } + @discardableResult public func send(request: URLRequest, _ completion: @escaping (Data?, URLResponse?, Error?) -> Void) -> CancellableRequest? { return requestExecuter.send(request: request, completion) From a6a51ef5d8cdef2df8e1000d460c2630449a601a Mon Sep 17 00:00:00 2001 From: Simon Blum Date: Fri, 14 Jan 2022 16:03:53 +0100 Subject: [PATCH 06/10] Add simple send functionality with async await --- Sources/Jetworking/Client/Client.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Sources/Jetworking/Client/Client.swift b/Sources/Jetworking/Client/Client.swift index 2e4a53a..a972045 100644 --- a/Sources/Jetworking/Client/Client.swift +++ b/Sources/Jetworking/Client/Client.swift @@ -454,6 +454,17 @@ public final class Client { return requestExecuter.send(request: request, completion) } + @available(iOS 15.0, macOS 12.0, *) + @discardableResult + public func send(request: URLRequest) async -> (Data?, URLResponse?, Error?) { + do { + let (data, urlResponse) = try await requestExecuter.send(request: request, delegate: nil) + return (data, urlResponse, nil) + } catch { + return (nil, nil, error) + } + } + @discardableResult public func download( url: URL, From dac8e0a3513d1a30416e1081ed41db2802da0d7d Mon Sep 17 00:00:00 2001 From: Simon Blum Date: Fri, 28 Jan 2022 13:39:33 +0100 Subject: [PATCH 07/10] Restructure Client to separate completion from async await functionality --- Sources/Jetworking/Client/Client.swift | 422 +++++++++++++------------ 1 file changed, 217 insertions(+), 205 deletions(-) diff --git a/Sources/Jetworking/Client/Client.swift b/Sources/Jetworking/Client/Client.swift index a972045..2ca3368 100644 --- a/Sources/Jetworking/Client/Client.swift +++ b/Sources/Jetworking/Client/Client.swift @@ -12,9 +12,6 @@ public enum APIError: Error { } public final class Client { - public typealias RequestResult = (HTTPURLResponse?, Result) - public typealias RequestCompletion = (HTTPURLResponse?, Result) -> Void - // MARK: - Properties private lazy var sessionCache: SessionCache = .init(configuration: configuration) @@ -97,7 +94,61 @@ public final class Client { self.session = session } - // MARK: - Methods + private func checkForValidDownloadURL(_ url: URL) -> Bool { + guard let scheme = URLComponents(string: url.absoluteString)?.scheme else { return false } + + return scheme == "http" || scheme == "https" + } + + private func createRequest( + forHttpMethod httpMethod: HTTPMethod, + and endpoint: Endpoint, + and body: Data? = nil, + andAdditionalHeaderFields additionalHeaderFields: [String: String] + ) throws -> URLRequest { + var request = URLRequest( + url: try URLFactory.makeURL(from: endpoint, withBaseURL: configuration.baseURLProvider.baseURL), + httpMethod: httpMethod, + httpBody: body + ) + + var requestInterceptors: [Interceptor] = configuration.interceptors + + // Extra case: POST-request with empty content + // + // Adds custom interceptor after last interceptor for header fields + // to avoid conflict with other custom interceptor if any. + if body == nil && httpMethod == .POST { + let targetIndex = requestInterceptors.lastIndex { $0 is HeaderFieldsInterceptor } + let indexToInsert = targetIndex.flatMap { requestInterceptors.index(after: $0) } + requestInterceptors.insert( + EmptyContentHeaderFieldsInterceptor(), + at: indexToInsert ?? requestInterceptors.endIndex + ) + } + + // Append additional header fields. + additionalHeaderFields.forEach { key, value in + request.addValue(value, forHTTPHeaderField: key) + } + + return requestInterceptors.reduce(request) { request, interceptor in + return interceptor.intercept(request) + } + } + + private func enqueue(_ completion: @escaping @autoclosure () -> Void) { + configuration.responseQueue.async { + completion() + } + } +} + +// MARK: - completion API + +extension Client { + public typealias RequestCompletion = (HTTPURLResponse?, Result) -> Void + @discardableResult public func get( endpoint: Endpoint, @@ -128,29 +179,6 @@ public final class Client { return nil } - @available(iOS 15.0, macOS 12.0, *) - public func get( - endpoint: Endpoint, - andAdditionalHeaderFields additionalHeaderFields: [String: String] = [:] - ) async -> RequestResult { - do { - let request: URLRequest = try createRequest( - forHttpMethod: .GET, - and: endpoint, - andAdditionalHeaderFields: additionalHeaderFields - ) - - let (data, urlResponse) = try await requestExecuter.send(request: request, delegate: nil) - return await responseHandler.handleDecodableResponse( - data: data, - urlResponse: urlResponse, - endpoint: endpoint - ) - } catch { - return (nil, .failure(error)) - } - } - @discardableResult public func post( endpoint: Endpoint, @@ -185,34 +213,6 @@ public final class Client { return nil } - @available(iOS 15.0, macOS 12.0, *) - @discardableResult - public func post( - endpoint: Endpoint, - body: BodyType, - andAdditionalHeaderFields additionalHeaderFields: [String: String] = [:] - ) async -> RequestResult { - do { - let encoder: Encoder = endpoint.encoder ?? configuration.encoder - let bodyData: Data = try encoder.encode(body) - let request: URLRequest = try createRequest( - forHttpMethod: .POST, - and: endpoint, - and: bodyData, - andAdditionalHeaderFields: additionalHeaderFields - ) - - let (data, urlResponse) = try await requestExecuter.send(request: request, delegate: nil) - return await responseHandler.handleDecodableResponse( - data: data, - urlResponse: urlResponse, - endpoint: endpoint - ) - } catch { - return (nil, .failure(error)) - } - } - @discardableResult public func post( endpoint: Endpoint, @@ -244,31 +244,6 @@ public final class Client { return nil } - @available(iOS 15.0, macOS 12.0, *) - @discardableResult - public func post( - endpoint: Endpoint, - body: ExpressibleByNilLiteral? = nil, - andAdditionalHeaderFields additionalHeaderFields: [String: String] = [:] - ) async -> RequestResult { - do { - let request: URLRequest = try createRequest( - forHttpMethod: .POST, - and: endpoint, - andAdditionalHeaderFields: additionalHeaderFields - ) - - let (data, urlResponse) = try await requestExecuter.send(request: request, delegate: nil) - return await responseHandler.handleDecodableResponse( - data: data, - urlResponse: urlResponse, - endpoint: endpoint - ) - } catch { - return (nil, .failure(error)) - } - } - @discardableResult public func put( endpoint: Endpoint, @@ -303,34 +278,6 @@ public final class Client { return nil } - @available(iOS 15.0, macOS 12.0, *) - @discardableResult - public func put( - endpoint: Endpoint, - body: BodyType, - andAdditionalHeaderFields additionalHeaderFields: [String: String] = [:] - ) async -> RequestResult { - do { - let encoder: Encoder = endpoint.encoder ?? configuration.encoder - let bodyData: Data = try encoder.encode(body) - let request: URLRequest = try createRequest( - forHttpMethod: .PUT, - and: endpoint, - and: bodyData, - andAdditionalHeaderFields: additionalHeaderFields - ) - - let (data, urlResponse) = try await requestExecuter.send(request: request, delegate: nil) - return await responseHandler.handleDecodableResponse( - data: data, - urlResponse: urlResponse, - endpoint: endpoint - ) - } catch { - return (nil, .failure(error)) - } - } - @discardableResult public func patch( endpoint: Endpoint, @@ -365,34 +312,6 @@ public final class Client { return nil } - @available(iOS 15.0, macOS 12.0, *) - @discardableResult - public func patch( - endpoint: Endpoint, - body: BodyType, - andAdditionalHeaderFields additionalHeaderFields: [String: String] = [:] - ) async -> RequestResult { - do { - let encoder: Encoder = endpoint.encoder ?? configuration.encoder - let bodyData: Data = try encoder.encode(body) - let request: URLRequest = try createRequest( - forHttpMethod: .PATCH, - and: endpoint, - and: bodyData, - andAdditionalHeaderFields: additionalHeaderFields - ) - - let (data, urlResponse) = try await requestExecuter.send(request: request, delegate: nil) - return await responseHandler.handleDecodableResponse( - data: data, - urlResponse: urlResponse, - endpoint: endpoint - ) - } catch { - return (nil, .failure(error)) - } - } - @discardableResult public func delete( endpoint: Endpoint, @@ -424,47 +343,11 @@ public final class Client { return nil } - @available(iOS 15.0, macOS 12.0, *) - @discardableResult - public func delete( - endpoint: Endpoint, - parameter: [String: Any] = [:], - andAdditionalHeaderFields additionalHeaderFields: [String: String] = [:] - ) async -> RequestResult { - do { - let request: URLRequest = try createRequest( - forHttpMethod: .DELETE, - and: endpoint, - andAdditionalHeaderFields: additionalHeaderFields - ) - - let (data, urlResponse) = try await requestExecuter.send(request: request, delegate: nil) - return await responseHandler.handleDecodableResponse( - data: data, - urlResponse: urlResponse, - endpoint: endpoint - ) - } catch { - return (nil, .failure(error)) - } - } - @discardableResult public func send(request: URLRequest, _ completion: @escaping (Data?, URLResponse?, Error?) -> Void) -> CancellableRequest? { return requestExecuter.send(request: request, completion) } - @available(iOS 15.0, macOS 12.0, *) - @discardableResult - public func send(request: URLRequest) async -> (Data?, URLResponse?, Error?) { - do { - let (data, urlResponse) = try await requestExecuter.send(request: request, delegate: nil) - return (data, urlResponse, nil) - } catch { - return (nil, nil, error) - } - } - @discardableResult public func download( url: URL, @@ -549,57 +432,184 @@ public final class Client { } return task } +} - private func checkForValidDownloadURL(_ url: URL) -> Bool { - guard let scheme = URLComponents(string: url.absoluteString)?.scheme else { return false } +// MARK: - async / await API - return scheme == "http" || scheme == "https" +extension Client { + public typealias RequestResult = (HTTPURLResponse?, Result) + + @available(iOS 15.0, macOS 12.0, *) + public func get( + endpoint: Endpoint, + andAdditionalHeaderFields additionalHeaderFields: [String: String] = [:] + ) async -> RequestResult { + do { + let request: URLRequest = try createRequest( + forHttpMethod: .GET, + and: endpoint, + andAdditionalHeaderFields: additionalHeaderFields + ) + + let (data, urlResponse) = try await requestExecuter.send(request: request, delegate: nil) + return await responseHandler.handleDecodableResponse( + data: data, + urlResponse: urlResponse, + endpoint: endpoint + ) + } catch { + return (nil, .failure(error)) + } } - private func createRequest( - forHttpMethod httpMethod: HTTPMethod, - and endpoint: Endpoint, - and body: Data? = nil, - andAdditionalHeaderFields additionalHeaderFields: [String: String] - ) throws -> URLRequest { - var request = URLRequest( - url: try URLFactory.makeURL(from: endpoint, withBaseURL: configuration.baseURLProvider.baseURL), - httpMethod: httpMethod, - httpBody: body - ) + @available(iOS 15.0, macOS 12.0, *) + @discardableResult + public func post( + endpoint: Endpoint, + body: BodyType, + andAdditionalHeaderFields additionalHeaderFields: [String: String] = [:] + ) async -> RequestResult { + do { + let encoder: Encoder = endpoint.encoder ?? configuration.encoder + let bodyData: Data = try encoder.encode(body) + let request: URLRequest = try createRequest( + forHttpMethod: .POST, + and: endpoint, + and: bodyData, + andAdditionalHeaderFields: additionalHeaderFields + ) - var requestInterceptors: [Interceptor] = configuration.interceptors + let (data, urlResponse) = try await requestExecuter.send(request: request, delegate: nil) + return await responseHandler.handleDecodableResponse( + data: data, + urlResponse: urlResponse, + endpoint: endpoint + ) + } catch { + return (nil, .failure(error)) + } + } - // Extra case: POST-request with empty content - // - // Adds custom interceptor after last interceptor for header fields - // to avoid conflict with other custom interceptor if any. - if body == nil && httpMethod == .POST { - let targetIndex = requestInterceptors.lastIndex { $0 is HeaderFieldsInterceptor } - let indexToInsert = targetIndex.flatMap { requestInterceptors.index(after: $0) } - requestInterceptors.insert( - EmptyContentHeaderFieldsInterceptor(), - at: indexToInsert ?? requestInterceptors.endIndex + @available(iOS 15.0, macOS 12.0, *) + @discardableResult + public func post( + endpoint: Endpoint, + body: ExpressibleByNilLiteral? = nil, + andAdditionalHeaderFields additionalHeaderFields: [String: String] = [:] + ) async -> RequestResult { + do { + let request: URLRequest = try createRequest( + forHttpMethod: .POST, + and: endpoint, + andAdditionalHeaderFields: additionalHeaderFields ) + + let (data, urlResponse) = try await requestExecuter.send(request: request, delegate: nil) + return await responseHandler.handleDecodableResponse( + data: data, + urlResponse: urlResponse, + endpoint: endpoint + ) + } catch { + return (nil, .failure(error)) } + } - // Append additional header fields. - additionalHeaderFields.forEach { key, value in - request.addValue(value, forHTTPHeaderField: key) + @available(iOS 15.0, macOS 12.0, *) + @discardableResult + public func put( + endpoint: Endpoint, + body: BodyType, + andAdditionalHeaderFields additionalHeaderFields: [String: String] = [:] + ) async -> RequestResult { + do { + let encoder: Encoder = endpoint.encoder ?? configuration.encoder + let bodyData: Data = try encoder.encode(body) + let request: URLRequest = try createRequest( + forHttpMethod: .PUT, + and: endpoint, + and: bodyData, + andAdditionalHeaderFields: additionalHeaderFields + ) + + let (data, urlResponse) = try await requestExecuter.send(request: request, delegate: nil) + return await responseHandler.handleDecodableResponse( + data: data, + urlResponse: urlResponse, + endpoint: endpoint + ) + } catch { + return (nil, .failure(error)) } + } - return requestInterceptors.reduce(request) { request, interceptor in - return interceptor.intercept(request) + @available(iOS 15.0, macOS 12.0, *) + @discardableResult + public func patch( + endpoint: Endpoint, + body: BodyType, + andAdditionalHeaderFields additionalHeaderFields: [String: String] = [:] + ) async -> RequestResult { + do { + let encoder: Encoder = endpoint.encoder ?? configuration.encoder + let bodyData: Data = try encoder.encode(body) + let request: URLRequest = try createRequest( + forHttpMethod: .PATCH, + and: endpoint, + and: bodyData, + andAdditionalHeaderFields: additionalHeaderFields + ) + + let (data, urlResponse) = try await requestExecuter.send(request: request, delegate: nil) + return await responseHandler.handleDecodableResponse( + data: data, + urlResponse: urlResponse, + endpoint: endpoint + ) + } catch { + return (nil, .failure(error)) } } - private func enqueue(_ completion: @escaping @autoclosure () -> Void) { - configuration.responseQueue.async { - completion() + @available(iOS 15.0, macOS 12.0, *) + @discardableResult + public func delete( + endpoint: Endpoint, + parameter: [String: Any] = [:], + andAdditionalHeaderFields additionalHeaderFields: [String: String] = [:] + ) async -> RequestResult { + do { + let request: URLRequest = try createRequest( + forHttpMethod: .DELETE, + and: endpoint, + andAdditionalHeaderFields: additionalHeaderFields + ) + + let (data, urlResponse) = try await requestExecuter.send(request: request, delegate: nil) + return await responseHandler.handleDecodableResponse( + data: data, + urlResponse: urlResponse, + endpoint: endpoint + ) + } catch { + return (nil, .failure(error)) + } + } + + @available(iOS 15.0, macOS 12.0, *) + @discardableResult + public func send(request: URLRequest) async -> (Data?, URLResponse?, Error?) { + do { + let (data, urlResponse) = try await requestExecuter.send(request: request, delegate: nil) + return (data, urlResponse, nil) + } catch { + return (nil, nil, error) } } } +// MARK: - DownloadExecuterDelegate + extension Client: DownloadExecuterDelegate { public func downloadExecuter( _ downloadTask: URLSessionDownloadTask, @@ -660,6 +670,8 @@ extension Client: DownloadExecuterDelegate { } } +// MARK: - UploadExecuterDelegate + extension Client: UploadExecuterDelegate { public func uploadExecuter( _ uploadTask: URLSessionUploadTask, From 51771bf46451c9e52fac2f8ac35b7c22309ea0f7 Mon Sep 17 00:00:00 2001 From: Simon Blum Date: Fri, 28 Jan 2022 14:16:13 +0100 Subject: [PATCH 08/10] Add Combine functionality using a Future extension --- .../Extensions/Future+Extension.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 Sources/Jetworking/Extensions/Future+Extension.swift diff --git a/Sources/Jetworking/Extensions/Future+Extension.swift b/Sources/Jetworking/Extensions/Future+Extension.swift new file mode 100644 index 0000000..8f1d0e0 --- /dev/null +++ b/Sources/Jetworking/Extensions/Future+Extension.swift @@ -0,0 +1,18 @@ +import Combine +import Foundation + +@available(iOS 15.0, macOS 12.0, *) +extension Future where Failure == Error { + convenience init(operation: @escaping () async throws -> Output) { + self.init { promise in + Task { + do { + let output = try await operation() + promise(.success(output)) + } catch { + promise(.failure(error)) + } + } + } + } +} From 1447c8b6e387964955fdd9f8e077e410d8df2578 Mon Sep 17 00:00:00 2001 From: Simon Blum Date: Fri, 28 Jan 2022 14:17:14 +0100 Subject: [PATCH 09/10] Add retrying possibility to concurrency Task --- .../Extensions/Task+Extension.swift | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 Sources/Jetworking/Extensions/Task+Extension.swift diff --git a/Sources/Jetworking/Extensions/Task+Extension.swift b/Sources/Jetworking/Extensions/Task+Extension.swift new file mode 100644 index 0000000..2549196 --- /dev/null +++ b/Sources/Jetworking/Extensions/Task+Extension.swift @@ -0,0 +1,29 @@ +import Foundation + +@available(iOS 15.0, macOS 12.0, *) +extension Task where Failure == Error { + @discardableResult + static func retrying( + priority: TaskPriority? = nil, + maxRetryCount: Int = 3, + retryDelay: TimeInterval = 1, + operation: @Sendable @escaping () async throws -> Success + ) -> Task { + Task(priority: priority) { + for _ in 0...sleep(nanoseconds: delay) + + continue + } + } + + try Task.checkCancellation() + return try await operation() + } + } +} From edc00b295c635b6f86ad158dab8065c95e86ced4 Mon Sep 17 00:00:00 2001 From: Simon Blum Date: Fri, 28 Jan 2022 14:39:06 +0100 Subject: [PATCH 10/10] Add backward compatibility for async await to iOS 13 and macOS 10.15 --- Sources/Jetworking/Client/Client.swift | 14 ++++---- .../AsyncRequestExecuter.swift | 8 +++-- .../RequestExecuter/RequestExecuter.swift | 2 +- .../SyncRequestExecuter.swift | 2 +- .../Jetworking/Client/ResponseHandler.swift | 4 +-- .../Extensions/URLSession+Extension.swift | 35 +++++++++++++++++++ .../JetworkingTests/Mocks/MockExecuter.swift | 2 +- 7 files changed, 53 insertions(+), 14 deletions(-) create mode 100644 Sources/Jetworking/Extensions/URLSession+Extension.swift diff --git a/Sources/Jetworking/Client/Client.swift b/Sources/Jetworking/Client/Client.swift index 2ca3368..6d8b354 100644 --- a/Sources/Jetworking/Client/Client.swift +++ b/Sources/Jetworking/Client/Client.swift @@ -439,7 +439,7 @@ extension Client { extension Client { public typealias RequestResult = (HTTPURLResponse?, Result) - @available(iOS 15.0, macOS 12.0, *) + @available(iOS 13.0, macOS 10.15.0, *) public func get( endpoint: Endpoint, andAdditionalHeaderFields additionalHeaderFields: [String: String] = [:] @@ -462,7 +462,7 @@ extension Client { } } - @available(iOS 15.0, macOS 12.0, *) + @available(iOS 13.0, macOS 10.15.0, *) @discardableResult public func post( endpoint: Endpoint, @@ -490,7 +490,7 @@ extension Client { } } - @available(iOS 15.0, macOS 12.0, *) + @available(iOS 13.0, macOS 10.15.0, *) @discardableResult public func post( endpoint: Endpoint, @@ -515,7 +515,7 @@ extension Client { } } - @available(iOS 15.0, macOS 12.0, *) + @available(iOS 13.0, macOS 10.15.0, *) @discardableResult public func put( endpoint: Endpoint, @@ -543,7 +543,7 @@ extension Client { } } - @available(iOS 15.0, macOS 12.0, *) + @available(iOS 13.0, macOS 10.15.0, *) @discardableResult public func patch( endpoint: Endpoint, @@ -571,7 +571,7 @@ extension Client { } } - @available(iOS 15.0, macOS 12.0, *) + @available(iOS 13.0, macOS 10.15.0, *) @discardableResult public func delete( endpoint: Endpoint, @@ -596,7 +596,7 @@ extension Client { } } - @available(iOS 15.0, macOS 12.0, *) + @available(iOS 13.0, macOS 10.15.0, *) @discardableResult public func send(request: URLRequest) async -> (Data?, URLResponse?, Error?) { do { diff --git a/Sources/Jetworking/Client/Executor/RequestExecuter/AsyncRequestExecuter/AsyncRequestExecuter.swift b/Sources/Jetworking/Client/Executor/RequestExecuter/AsyncRequestExecuter/AsyncRequestExecuter.swift index 2708e7e..84ed1d7 100644 --- a/Sources/Jetworking/Client/Executor/RequestExecuter/AsyncRequestExecuter/AsyncRequestExecuter.swift +++ b/Sources/Jetworking/Client/Executor/RequestExecuter/AsyncRequestExecuter/AsyncRequestExecuter.swift @@ -14,8 +14,12 @@ final class AsyncRequestExecuter: RequestExecuter { return dataTask } - @available(iOS 15.0, macOS 12.0, *) + @available(iOS 13.0, macOS 10.15.0, *) func send(request: URLRequest, delegate: URLSessionTaskDelegate? = nil) async throws -> (Data?, URLResponse?) { - return try await session.data(for: request, delegate: delegate) + if #available(iOS 15.0, macOS 12.0, *) { + return try await session.data(for: request, delegate: delegate) + } else { + return try await session.data(for: request) + } } } diff --git a/Sources/Jetworking/Client/Executor/RequestExecuter/RequestExecuter.swift b/Sources/Jetworking/Client/Executor/RequestExecuter/RequestExecuter.swift index 28fe755..5758201 100644 --- a/Sources/Jetworking/Client/Executor/RequestExecuter/RequestExecuter.swift +++ b/Sources/Jetworking/Client/Executor/RequestExecuter/RequestExecuter.swift @@ -29,6 +29,6 @@ public protocol RequestExecuter { */ func send(request: URLRequest, _ completion: @escaping ((Data?, URLResponse?, Error?) -> Void)) -> CancellableRequest? - @available(iOS 15.0, macOS 12.0, *) + @available(iOS 13.0, macOS 10.15.0, *) func send(request: URLRequest, delegate: URLSessionTaskDelegate?) async throws -> (Data?, URLResponse?) } diff --git a/Sources/Jetworking/Client/Executor/RequestExecuter/SyncRequestExecuter/SyncRequestExecuter.swift b/Sources/Jetworking/Client/Executor/RequestExecuter/SyncRequestExecuter/SyncRequestExecuter.swift index 3291b59..b5794da 100644 --- a/Sources/Jetworking/Client/Executor/RequestExecuter/SyncRequestExecuter/SyncRequestExecuter.swift +++ b/Sources/Jetworking/Client/Executor/RequestExecuter/SyncRequestExecuter/SyncRequestExecuter.swift @@ -20,7 +20,7 @@ final class SyncRequestExecuter: RequestExecuter { return operation } - @available(iOS 15.0, macOS 12.0, *) + @available(iOS 13.0, macOS 10.15.0, *) func send(request: URLRequest, delegate: URLSessionTaskDelegate?) async throws -> (Data?, URLResponse?) { return (nil, nil) } diff --git a/Sources/Jetworking/Client/ResponseHandler.swift b/Sources/Jetworking/Client/ResponseHandler.swift index 1c183f2..815dc6b 100644 --- a/Sources/Jetworking/Client/ResponseHandler.swift +++ b/Sources/Jetworking/Client/ResponseHandler.swift @@ -29,7 +29,7 @@ final class ResponseHandler { evaluate(data: data, urlResponse: urlResponse, error: error, endpoint: endpoint, completionWrapper: makeDecodableCompletionWrapper, completion: completion) } - @available(iOS 15.0, macOS 12.0, *) + @available(iOS 13.0, macOS 10.15.0, *) func handleDecodableResponse( data: Data?, urlResponse: URLResponse?, @@ -130,7 +130,7 @@ final class ResponseHandler { } } - @available(iOS 13.0.0, macOS 12.0, *) + @available(iOS 13.0, macOS 10.15.0, *) private func evaluate( data: Data?, urlResponse: URLResponse?, diff --git a/Sources/Jetworking/Extensions/URLSession+Extension.swift b/Sources/Jetworking/Extensions/URLSession+Extension.swift new file mode 100644 index 0000000..644389e --- /dev/null +++ b/Sources/Jetworking/Extensions/URLSession+Extension.swift @@ -0,0 +1,35 @@ +import Foundation + +@available(iOS, deprecated: 15.0, message: "Use the built-in API instead") +public extension URLSession { + /// Start a data task with a `URLRequest` using async/await. + /// - parameter request: The `URLRequest` that the data task should perform. + /// - returns: A tuple containing the binary `Data` that was downloaded, + /// as well as a `URLResponse` representing the server's response. + /// - throws: Any error encountered while performing the data task. + @available(iOS 13.0, macOS 10.15.0, *) + func data(for request: URLRequest) async throws -> (Data, URLResponse) { + var dataTask: URLSessionDataTask? + let onCancel = { dataTask?.cancel() } + + return try await withTaskCancellationHandler( + handler: { + onCancel() + }, + operation: { + try await withCheckedThrowingContinuation { continuation in + dataTask = self.dataTask(with: request) { data, response, error in + guard let data = data, let response = response else { + let error = error ?? URLError(.badServerResponse) + return continuation.resume(throwing: error) + } + + continuation.resume(returning: (data, response)) + } + + dataTask?.resume() + } + } + ) + } +} diff --git a/Tests/JetworkingTests/Mocks/MockExecuter.swift b/Tests/JetworkingTests/Mocks/MockExecuter.swift index 204ca5c..86c1c16 100644 --- a/Tests/JetworkingTests/Mocks/MockExecuter.swift +++ b/Tests/JetworkingTests/Mocks/MockExecuter.swift @@ -54,7 +54,7 @@ final class MockExecuter: RequestExecuter { } } - @available(iOS 15.0, macOS 12.0, *) + @available(iOS 13.0, macOS 10.15.0, *) func send(request: URLRequest, delegate: URLSessionTaskDelegate?) async throws -> (Data?, URLResponse?) { return (nil, nil) }