Skip to content
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