Skip to content

WIP: Adding async await API to GET, POST, PUT, PATCH, DELETE and simple request. #91

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: develop
Choose a base branch
from
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -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
Expand Down
259 changes: 220 additions & 39 deletions Sources/Jetworking/Client/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ public enum APIError: Error {
}

public final class Client {
public typealias RequestCompletion<ResponseType> = (HTTPURLResponse?, Result<ResponseType, Error>) -> Void

// MARK: - Properties
private lazy var sessionCache: SessionCache = .init(configuration: configuration)

Expand Down Expand Up @@ -96,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<ResponseType>(
forHttpMethod httpMethod: HTTPMethod,
and endpoint: Endpoint<ResponseType>,
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<ResponseType> = (HTTPURLResponse?, Result<ResponseType, Error>) -> Void

@discardableResult
public func get<ResponseType: Decodable>(
endpoint: Endpoint<ResponseType>,
Expand Down Expand Up @@ -380,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 }
}

return scheme == "http" || scheme == "https"
// MARK: - async / await API

extension Client {
public typealias RequestResult<ResponseType> = (HTTPURLResponse?, Result<ResponseType, Error>)

@available(iOS 13.0, macOS 10.15.0, *)
public func get<ResponseType: Decodable>(
endpoint: Endpoint<ResponseType>,
andAdditionalHeaderFields additionalHeaderFields: [String: String] = [:]
) async -> RequestResult<ResponseType> {
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<ResponseType>(
forHttpMethod httpMethod: HTTPMethod,
and endpoint: Endpoint<ResponseType>,
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 13.0, macOS 10.15.0, *)
@discardableResult
public func post<BodyType: Encodable, ResponseType: Decodable>(
endpoint: Endpoint<ResponseType>,
body: BodyType,
andAdditionalHeaderFields additionalHeaderFields: [String: String] = [:]
) async -> RequestResult<ResponseType> {
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 13.0, macOS 10.15.0, *)
@discardableResult
public func post<ResponseType: Decodable>(
endpoint: Endpoint<ResponseType>,
body: ExpressibleByNilLiteral? = nil,
andAdditionalHeaderFields additionalHeaderFields: [String: String] = [:]
) async -> RequestResult<ResponseType> {
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 13.0, macOS 10.15.0, *)
@discardableResult
public func put<BodyType: Encodable, ResponseType: Decodable>(
endpoint: Endpoint<ResponseType>,
body: BodyType,
andAdditionalHeaderFields additionalHeaderFields: [String: String] = [:]
) async -> RequestResult<ResponseType> {
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 13.0, macOS 10.15.0, *)
@discardableResult
public func patch<BodyType: Encodable, ResponseType: Decodable>(
endpoint: Endpoint<ResponseType>,
body: BodyType,
andAdditionalHeaderFields additionalHeaderFields: [String: String] = [:]
) async -> RequestResult<ResponseType> {
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 13.0, macOS 10.15.0, *)
@discardableResult
public func delete<ResponseType: Decodable>(
endpoint: Endpoint<ResponseType>,
parameter: [String: Any] = [:],
andAdditionalHeaderFields additionalHeaderFields: [String: String] = [:]
) async -> RequestResult<ResponseType> {
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 13.0, macOS 10.15.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,
Expand Down Expand Up @@ -491,6 +670,8 @@ extension Client: DownloadExecuterDelegate {
}
}

// MARK: - UploadExecuterDelegate

extension Client: UploadExecuterDelegate {
public func uploadExecuter(
_ uploadTask: URLSessionUploadTask,
Expand All @@ -501,13 +682,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 }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,13 @@ final class AsyncRequestExecuter: RequestExecuter {

return dataTask
}

@available(iOS 13.0, macOS 10.15.0, *)
func send(request: URLRequest, delegate: URLSessionTaskDelegate? = nil) async throws -> (Data?, URLResponse?) {
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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 13.0, macOS 10.15.0, *)
func send(request: URLRequest, delegate: URLSessionTaskDelegate?) async throws -> (Data?, URLResponse?)
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,9 @@ final class SyncRequestExecuter: RequestExecuter {

return operation
}

@available(iOS 13.0, macOS 10.15.0, *)
func send(request: URLRequest, delegate: URLSessionTaskDelegate?) async throws -> (Data?, URLResponse?) {
return (nil, nil)
}
}
Loading