Skip to content

Commit

Permalink
some updates
Browse files Browse the repository at this point in the history
  • Loading branch information
dankinsoid committed Mar 24, 2024
1 parent a000c6e commit 9633d73
Show file tree
Hide file tree
Showing 11 changed files with 293 additions and 124 deletions.
32 changes: 25 additions & 7 deletions Sources/SwiftAPIClient/Clients/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,23 +37,41 @@ public extension APIClient.Configs {
}
}

public extension APIClientCaller where Result == AsyncValue<Value>, Response == Data {
public extension APIClientCaller where Result == AsyncThrowingValue<Value>, Response == Data {

static var http: APIClientCaller {
.http { request, configs in
var request = request
if request.httpBodyStream != nil {
configs.logger.warning("httpBodyStream is not supported, use `.body(file:) modifier")
}
let isUpload = request.httpBody != nil || configs.file != nil
if request.httpMethod == nil {
request.httpMethod = HTTPMethod.get.rawValue
request.httpMethod = isUpload ? HTTPMethod.post.rawValue : HTTPMethod.get.rawValue
}
if request.httpBodyStream != nil {
configs.logger.warning("HTTPBodyStream is not supported with a http caller. Use httpUpload instead.")

if request.httpBody != nil, configs.file != nil {
configs.logger.warning("Both body data and body file are set for the request \(request).")
}

let task: UploadTask?
if let body = request.httpBody {
task = .data(body)
} else if let file = configs.file?(configs) {
task = .file(file)
} else {
task = nil
}
if let task {
return try await configs.httpUploadClient.upload(request, task, configs)
} else {
return try await configs.httpClient.data(request, configs)
}
return try await configs.httpClient.data(request, configs)
}
}
}

extension APIClientCaller where Result == AsyncValue<Value>, Response == Data {
extension APIClientCaller where Result == AsyncThrowingValue<Value>, Response == Data {

static func http(
task: @escaping @Sendable (URLRequest, APIClient.Configs) async throws -> (Data, HTTPURLResponse)
Expand All @@ -66,7 +84,7 @@ extension APIClientCaller where Result == AsyncValue<Value>, Response == Data {
}
}

extension APIClientCaller where Result == AsyncValue<Value> {
extension APIClientCaller where Result == AsyncThrowingValue<Value> {

static func http(
task: @escaping @Sendable (URLRequest, APIClient.Configs) async throws -> (Response, HTTPURLResponse),
Expand Down
30 changes: 29 additions & 1 deletion Sources/SwiftAPIClient/Clients/HTTPDownloadClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,28 @@ public extension APIClient {
func httpDownloadClient(_ client: HTTPDownloadClient) -> APIClient {
configs(\.httpDownloadClient, client)
}

/// Observe the download progress of the request.
func trackDownload(_ action: @escaping (_ progress: Double) -> Void) -> Self {
trackDownload { totalBytesWritten, totalBytesExpectedToWrite in
guard totalBytesExpectedToWrite > 0 else {
action(1)
return
}
action(Double(totalBytesWritten) / Double(totalBytesExpectedToWrite))
}
}

/// Observe the download progress of the request.
func trackDownload(_ action: @escaping (_ totalBytesWritten: Int64, _ totalBytesExpectedToWrite: Int64) -> Void) -> Self {
configs {
let current = $0.downloadTracker
$0.downloadTracker = { totalBytesWritten, totalBytesExpectedToWrite in
current(totalBytesWritten, totalBytesExpectedToWrite)
action(totalBytesWritten, totalBytesExpectedToWrite)
}
}
}
}

public extension APIClient.Configs {
Expand All @@ -36,9 +58,15 @@ public extension APIClient.Configs {
get { self[\.httpDownloadClient] ?? .urlSession }
set { self[\.httpDownloadClient] = newValue }
}

/// The closure that provides the data for the request.
var downloadTracker: (_ totalBytesWritten: Int64, _ totalBytesExpectedToWrite: Int64) -> Void {
get { self[\.downloadTracker] ?? { _, _ in } }
set { self[\.downloadTracker] = newValue }
}
}

public extension APIClientCaller where Result == AsyncValue<Value>, Response == URL {
public extension APIClientCaller where Result == AsyncThrowingValue<Value>, Response == URL {

static var httpDownload: APIClientCaller {
.http { request, configs in
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftAPIClient/Clients/HTTPPublisher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Foundation
public extension APIClientCaller where Result == AnyPublisher<Value, Error>, Response == Data {

static var httpPublisher: APIClientCaller {
APIClientCaller<Response, Value, AsyncValue<Value>>.http.map { value in
APIClientCaller<Response, Value, AsyncThrowingValue<Value>>.http.map { value in
Publishers.Task {
try await value()
}
Expand Down
61 changes: 38 additions & 23 deletions Sources/SwiftAPIClient/Clients/HTTPUploadClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,25 @@ public struct HTTPUploadClient {
}
}

public enum UploadTask: Equatable {
public enum UploadTask: Hashable {

case file(URL)
case data(Data)
case stream
}

public extension APIClient.Configs {

/// The closure that provides the file URL for the request.
var file: ((APIClient.Configs) -> URL)? {
get { self[\.file] }
set { self[\.file] = newValue }
}

/// The closure that is called when the upload progress is updated.
var uploadTracker: (_ totalBytesSent: Int64, _ totalBytesExpectedToSend: Int64) -> Void {
get { self[\.uploadTracker] ?? { _, _ in } }
set { self[\.uploadTracker] = newValue }
}
}

public extension APIClient {
Expand All @@ -32,6 +46,28 @@ public extension APIClient {
func httpUploadClient(_ client: HTTPUploadClient) -> APIClient {
configs(\.httpUploadClient, client)
}

/// Observe the upload progress of the request.
func trackUpload(_ action: @escaping (_ progress: Double) -> Void) -> Self {
trackUpload { totalBytesSent, totalBytesExpectedToSend in
guard totalBytesExpectedToSend > 0 else {
action(1)
return
}
action(Double(totalBytesSent) / Double(totalBytesExpectedToSend))
}
}

/// Observe the upload progress of the request.
func trackUpload(_ action: @escaping (_ totalBytesSent: Int64, _ totalBytesExpectedToSend: Int64) -> Void) -> Self {
configs {
let current = $0.uploadTracker
$0.uploadTracker = { totalBytesSent, totalBytesExpectedToSend in
current(totalBytesSent, totalBytesExpectedToSend)
action(totalBytesSent, totalBytesExpectedToSend)
}
}
}
}

public extension APIClient.Configs {
Expand All @@ -44,24 +80,3 @@ public extension APIClient.Configs {
set { self[\.httpUploadClient] = newValue }
}
}

public extension APIClientCaller where Result == AsyncValue<Value>, Response == Data {

static func httpUpload(_ task: UploadTask = .stream) -> APIClientCaller {
.http { request, configs in
var request = request
if request.httpMethod == nil {
request.httpMethod = HTTPMethod.post.rawValue
}
if task == .stream, request.httpBodyStream == nil {
if let body = request.httpBody {
request.httpBody = nil
request.httpBodyStream = InputStream(data: body)
} else {
configs.logger.warning("There is no httpBodyStream in the request \(request).")
}
}
return try await configs.httpUploadClient.upload(request, task, configs)
}
}
}
38 changes: 36 additions & 2 deletions Sources/SwiftAPIClient/Clients/NetworkClientCaller.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,31 @@ public extension APIClient {
/// let value: SomeModel = try await client.call(.http, as: .decodable)
/// ```
func call<Response, Value, Result>(
_ caller: APIClientCaller<Response, Value, AsyncValue<Result>>,
_ caller: APIClientCaller<Response, Value, AsyncThrowingValue<Result>>,
as serializer: Serializer<Response, Value>,
fileID: String = #fileID,
line: UInt = #line
) async throws -> Result {
try await call(caller, as: serializer, fileID: fileID, line: line)()
}

/// Asynchronously performs a network call using the provided caller and serializer.
/// - Parameters:
/// - caller: A `APIClientCaller` instance.
/// - Returns: The result of the network call.
///
/// Example
/// ```swift
/// let url = try await client.call(.httpDownload)
/// ```
func call<Value, Result>(
_ caller: APIClientCaller<Value, Value, AsyncThrowingValue<Result>>,
fileID: String = #fileID,
line: UInt = #line
) async throws -> Result {
try await call(caller, as: .identity, fileID: fileID, line: line)()
}

/// Asynchronously performs a network call using the http caller and decodable serializer.
/// - Returns: The result of the network call.
func call<Result: Decodable>(fileID: String = #fileID, line: UInt = #line) async throws -> Result {
Expand All @@ -128,7 +145,24 @@ public extension APIClient {
try await call(fileID: fileID, line: line)
}

/// Performs a synchronous network call using the provided caller and serializer.
/// Performs a network call using the provided caller and serializer.
/// - Parameters:
/// - caller: A `APIClientCaller` instance.
/// - Returns: The result of the network call.
///
/// Example
/// ```swift
/// try client.call(.httpPublisher).sink { data in ... }
/// ```
func call<Value, Result>(
_ caller: APIClientCaller<Value, Value, Result>,
fileID: String = #fileID,
line: UInt = #line
) throws -> Result {
try call(caller, as: .identity, fileID: fileID, line: line)
}

/// Performs a network call using the provided caller and serializer.
/// - Parameters:
/// - caller: A `APIClientCaller` instance.
/// - serializer: A `Serializer` to process the response.
Expand Down
6 changes: 2 additions & 4 deletions Sources/SwiftAPIClient/Clients/URLSession+Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ public extension HTTPClient {
public extension HTTPUploadClient {

static var urlSession: Self {
HTTPUploadClient { request, uploadTask, configs in
HTTPUploadClient { request, task, configs in
try await asyncMethod { completion in
configs.urlSession.uploadTask(with: request, task: uploadTask, completionHandler: completion)
configs.urlSession.uploadTask(with: request, task: task, completionHandler: completion)
}
}
}
Expand All @@ -61,8 +61,6 @@ private extension URLSession {
return uploadTask(with: request, from: data, completionHandler: completionHandler)
case let .file(url):
return uploadTask(with: request, fromFile: url, completionHandler: completionHandler)
case .stream:
return uploadTask(withStreamedRequest: request)
}
}
}
Expand Down
92 changes: 92 additions & 0 deletions Sources/SwiftAPIClient/Modifiers/BackgroundModifiers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#if canImport(UIKit)
import UIKit

public extension APIClient {

/// Execute the http request in the background task.
///
/// To know more about background task, see [Apple Documentation](https://developer.apple.com/documentation/backgroundtasks)
func backgroundTask() -> Self {
httpClientMiddleware(BackgroundTaskMiddleware())
}

/// Retry the http request when enter foreground if it was failed in the background.
func retryIfFailedInBackground() -> Self {
httpClientMiddleware(RetryOnEnterForegroundMiddleware())
}
}

private struct BackgroundTaskMiddleware: HTTPClientMiddleware {

func execute<T>(
request: URLRequest,
configs: APIClient.Configs,
next: (URLRequest, APIClient.Configs) async throws -> (T, HTTPURLResponse)
) async throws -> (T, HTTPURLResponse) {
let id = await UIApplication.shared.beginBackgroundTask(
withName: "Background Task for \(request.url?.absoluteString ?? "")"
)
guard id != .invalid else {
return try await next(request, configs)
}
do {
let result = try await next(request, configs)
await UIApplication.shared.endBackgroundTask(id)
return result
} catch {
await UIApplication.shared.endBackgroundTask(id)
throw error
}
}
}

private struct RetryOnEnterForegroundMiddleware: HTTPClientMiddleware {

func execute<T>(
request: URLRequest,
configs: APIClient.Configs,
next: (URLRequest, APIClient.Configs) async throws -> (T, HTTPURLResponse)
) async throws -> (T, HTTPURLResponse) {
let wasInBackground = WasInBackgroundService()
let isInBackground = await UIApplication.shared.applicationState == .background
if !isInBackground {
await wasInBackground.start()
}
do {
return try await next(request, configs)
} catch {
if await wasInBackground.wasInBackground {
return try await next(request, configs)
}
throw error
}
}
}

private final actor WasInBackgroundService {

public private(set) var wasInBackground = false
private var observer: NSObjectProtocol?

public func start() async {
observer = await NotificationCenter.default.addObserver(
forName: UIApplication.didEnterBackgroundNotification,
object: nil,
queue: nil
) { [weak self] _ in
guard let self else { return }
Task {
await self.setTrue()
}
}
}

public func reset() {
wasInBackground = false
}

private func setTrue() {
wasInBackground = true
}
}
#endif
Loading

0 comments on commit 9633d73

Please sign in to comment.