Skip to content

Commit

Permalink
Major changes of Protocols and Async/Await in Apexy (#40)
Browse files Browse the repository at this point in the history
* Разделил протоколы. Переписал методы нового concurrency.

* Поправил запросы в ULRSession

* APIResult вынес в основной таргет

* Поправил заголовки файлов

* поправил образ и версию xcode

* Поправил распаковку результата

* Переработал методы для URLSession

* Вынес обработку результатов

* Вынес ResponseObserver

* Обновил README

* обновил доку

* Apply suggestions from code review

Co-authored-by: Ivan Vavilov <[email protected]>

* Async cancelation and thread switching fixed

* Thread safety fix + test fix + new tests for helper

* Specified type for continuation

* Fix cancelation check. Fix test. Fix using async/await in sync method

* Fix spacing in closure

Co-authored-by: Ivan Vavilov <[email protected]>
  • Loading branch information
laqiguy and vani2 authored Nov 25, 2022
1 parent 02e3454 commit c293b10
Show file tree
Hide file tree
Showing 19 changed files with 390 additions and 176 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ on:
jobs:
test:
name: Run tests
runs-on: macos-11
runs-on: macos-12
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Change Xcode
run: sudo xcode-select -s /Applications/Xcode_13.2.1.app
run: sudo xcode-select -s /Applications/Xcode_13.4.1.app
- name: Build and test
run: swift test --enable-code-coverage --disable-automatic-resolution
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
"repositoryURL": "https://github.com/Alamofire/Alamofire.git",
"state": {
"branch": null,
"revision": "becd9a729a37bdbef5bc39dc3c702b99f9e3d046",
"version": "5.2.2"
"revision": "8dd85aee02e39dd280c75eef88ffdb86eed4b07b",
"version": "5.6.2"
}
}
]
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,20 @@ client.request(endpoint) { (result: Result<Book, Error>)

The separation into `Client` and `Endpoint` allows you to separate the asynchronous code in `Client` from the synchronous code in `Endpoint`. Thus, the side effects are isolated in `Client`, and the pure functions in the non-mutable `Endpoint`.

### CombineClient

`CombineClient` - protocol that wrap up network request in `Combine`.

### ConcurrencyClient

`ConcurrencyClient` - protocol that wrap up network request in `Async/Await`.

* By default, new methods implemented as extensions of `Client`'s methods.
* `ApexyAlamofire` use built in implementation of `Async/Await` in `Alamofire`
* For `URLSession` new `Async/Await` methods was implemented using `URLSession`'s `AsyncAwait` extended implementation for iOS 14 and below. (look into `URLSession+Concurrency.swift` for more details)

`Client`, `CombineClient` and `ConcurrenyClient` are separated protocols. You can specify method that you are using by using specific protocol.

## Getting Started

Since most requests will receive JSON, it is necessary to make basic protocols at the module level. They will contain common requests logic for a specific API.
Expand Down
14 changes: 14 additions & 0 deletions README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,20 @@ client.request(endpoint) { (result: Result<Book, Error>)

Разделение на `Client` и `Endpoint` позволяет разделить асинхронный код в `Client` от синхронного кода в `Endpoint`. Таким образом сайд эффекты изолированы в одном месте `Client`, а чистые функции в немутабельных `Endpoint`.

### CombineClient

`CombineClient` - отдельный протокол, который содержит релизацию сетевый вызовов через Combine.

### ConcurrencyClient

`ConcurrencyClient` - отдельный протокол, который содержит релизацию сетевый вызовов через Async/Await.

* По умолчанию, новые методы релизованы как надстройки над существующими методами с замыканиями.
* Для `ApexyAlamofire` методы уже реализованы через методы `Alamofire`.
* Для `URLSession` добавлены через реализацию системных методов через Async/Await для версий ниже iOS 15.

`Client`, `CombineClient` и `ConcurrenyClient` - независимые протоколы. В зависимости от удобного для вас способа работы асинхронностью, вы можете выбрать конкретный протокол.

## Getting Started

Так как большинство запросов будут получать JSON, то на уровне модуля нужно сделать базовые протоколы. Они будут содержать в себе общую логику запросов для конкретной API.
Expand Down
21 changes: 21 additions & 0 deletions Sources/Apexy/APIResult.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// APIResult.swift
//
//
// Created by Aleksei Tiurnin on 17.08.2022.
//

import Foundation

public typealias APIResult<Value> = Swift.Result<Value, Error>

public extension APIResult {
var error: Error? {
switch self {
case .failure(let error):
return error
default:
return nil
}
}
}
29 changes: 0 additions & 29 deletions Sources/Apexy/Client.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import Foundation

public typealias APIResult<Value> = Swift.Result<Value, Error>

public protocol Client: AnyObject {

/// Send request to specified endpoint.
Expand All @@ -25,31 +23,4 @@ public protocol Client: AnyObject {
_ endpoint: T,
completionHandler: @escaping (APIResult<T.Content>) -> Void
) -> Progress where T: UploadEndpoint

/// Send request to specified endpoint.
/// - Returns: response data from the server for the request.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
func request<T>(_ endpoint: T) async throws -> T.Content where T: Endpoint

/// Upload data to specified endpoint.
/// - Returns: response data from the server for the upload.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
func upload<T>(_ endpoint: T) async throws -> T.Content where T: UploadEndpoint

}

@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public extension Client {

func request<T>(_ endpoint: T) async throws -> T.Content where T: Endpoint {
try await AsyncAwaitHelper.adaptToAsync(dataTaskClosure: { continuation in
request(endpoint, completionHandler: continuation.resume)
})
}

func upload<T>(_ endpoint: T) async throws -> T.Content where T: UploadEndpoint {
try await AsyncAwaitHelper.adaptToAsync(dataTaskClosure: { continuation in
upload(endpoint, completionHandler: continuation.resume)
})
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
#if canImport(Combine)
import Combine

/// Wrapper for Combine framework
public extension Client {
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public protocol CombineClient: AnyObject {

/// Send request to specified endpoint.
/// - Parameters:
/// - endpoint: endpoint of remote content.
/// - Returns: Publisher which you can subscribe to
func request<T>(_ endpoint: T) -> AnyPublisher<T.Content, Error> where T: Endpoint
}

@available(iOS 13.0, *)
@available(macOS 10.15, *)
@available(tvOS 13.0, *)
@available(watchOS 6.0, *)
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public extension Client where Self: CombineClient {
func request<T>(_ endpoint: T) -> AnyPublisher<T.Content, Error> where T: Endpoint {
Deferred<AnyPublisher<T.Content, Error>> {
let subject = PassthroughSubject<T.Content, Error>()
Expand All @@ -30,4 +35,5 @@ public extension Client {
.eraseToAnyPublisher()
}
}

#endif
24 changes: 24 additions & 0 deletions Sources/Apexy/Clients/ConcurrencyClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// ConcurrencyClient.swift
//
//
// Created by Aleksei Tiurnin on 16.08.2022.
//

import Foundation

@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
public protocol ConcurrencyClient: AnyObject {
/// Send request to specified endpoint.
/// - Parameters:
/// - endpoint: endpoint of remote content.
/// - Returns: response data from the server for the request.
func request<T>(_ endpoint: T) async throws -> T.Content where T: Endpoint

/// Upload data to specified endpoint.
/// - Parameters:
/// - endpoint: endpoint of remote content.
/// - Returns: response data from the server for the upload.
func upload<T>(_ endpoint: T) async throws -> T.Content where T: UploadEndpoint

}
2 changes: 1 addition & 1 deletion Sources/Apexy/Endpoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,5 @@ public protocol Endpoint {
}

public extension Endpoint {
func validate(_ request: URLRequest?, response: HTTPURLResponse, data: Data?) { }
func validate(_ request: URLRequest?, response: HTTPURLResponse, data: Data?) throws { }
}
36 changes: 0 additions & 36 deletions Sources/Apexy/ProgressWrapper.swift

This file was deleted.

10 changes: 10 additions & 0 deletions Sources/Apexy/ResponseObserver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//
// ResponseObserver.swift
//
//
// Created by Aleksei Tiurnin on 31.08.2022.
//

import Foundation

public typealias ResponseObserver = (URLRequest?, HTTPURLResponse?, Data?, Error?) -> Void
82 changes: 82 additions & 0 deletions Sources/ApexyAlamofire/AlamofireClient+Concurrency.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//
// AlamofireClient+Concurrency.swift
//
//
// Created by Aleksei Tiurnin on 15.08.2022.
//

import Alamofire
import Apexy
import Foundation

@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
extension AlamofireClient: ConcurrencyClient {

func observeResponse(
dataResponse: DataResponse<Data, AFError>,
error: Error?) {
self.responseObserver?(
dataResponse.request,
dataResponse.response,
dataResponse.data,
error)
}

open func request<T>(_ endpoint: T) async throws -> T.Content where T : Endpoint {

let anyRequest = AnyRequest(create: endpoint.makeRequest)
let request = sessionManager.request(anyRequest)
.validate { request, response, data in
Result(catching: { try endpoint.validate(request, response: response, data: data) })
}

let dataResponse = await request.serializingData().response
let result = APIResult<T.Content>(catching: { () throws -> T.Content in
do {
let data = try dataResponse.result.get()
return try endpoint.content(from: dataResponse.response, with: data)
} catch {
throw error.unwrapAlamofireValidationError()
}
})

Task.detached { [weak self, dataResponse, result] in
self?.observeResponse(dataResponse: dataResponse, error: result.error)
}

return try result.get()
}

open func upload<T>(_ endpoint: T) async throws -> T.Content where T : UploadEndpoint {

let urlRequest: URLRequest
let body: UploadEndpointBody
(urlRequest, body) = try endpoint.makeRequest()

let request: UploadRequest
switch body {
case .data(let data):
request = sessionManager.upload(data, with: urlRequest)
case .file(let url):
request = sessionManager.upload(url, with: urlRequest)
case .stream(let stream):
request = sessionManager.upload(stream, with: urlRequest)
}

let dataResponse = await request.serializingData().response
let result = APIResult<T.Content>(catching: { () throws -> T.Content in
do {
let data = try dataResponse.result.get()
return try endpoint.content(from: dataResponse.response, with: data)
} catch {
throw error.unwrapAlamofireValidationError()
}
})

Task.detached { [weak self, dataResponse, result] in
self?.observeResponse(dataResponse: dataResponse, error: result.error)
}

return try result.get()
}
}
30 changes: 8 additions & 22 deletions Sources/ApexyAlamofire/AlamofireClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,29 @@
// Copyright © 2019 RedMadRobot. All rights reserved.
//

import Alamofire
import Apexy
import Foundation
import Alamofire

/// API Client.
open class AlamofireClient: Client {

/// A closure used to observe result of every response from the server.
public typealias ResponseObserver = (URLRequest?, HTTPURLResponse?, Data?, Error?) -> Void
open class AlamofireClient: Client, CombineClient {

/// Session network manager.
private let sessionManager: Alamofire.Session
let sessionManager: Alamofire.Session

/// The queue on which the network response handler is dispatched.
private let responseQueue = DispatchQueue(
let responseQueue = DispatchQueue(
label: "Apexy.responseQueue",
qos: .utility)

/// The queue on which the completion handler is dispatched.
private let completionQueue: DispatchQueue
let completionQueue: DispatchQueue

/// This closure to be called after each response from the server for the request.
private let responseObserver: ResponseObserver?
let responseObserver: ResponseObserver?

/// Look more at Alamofire.RequestInterceptor.
public let requestInterceptor: RequestInterceptor
let requestInterceptor: RequestInterceptor

/// Creates new 'AlamofireClient' instance.
///
Expand Down Expand Up @@ -187,25 +184,14 @@ open class AlamofireClient: Client {
// MARK: - Helper

/// Wrapper for `URLRequestConvertible` from `Alamofire`.
private struct AnyRequest: Alamofire.URLRequestConvertible {
struct AnyRequest: Alamofire.URLRequestConvertible {
let create: () throws -> URLRequest

func asURLRequest() throws -> URLRequest {
return try create()
}
}

private extension APIResult {
var error: Error? {
switch self {
case .failure(let error):
return error
default:
return nil
}
}
}

public extension Error {
func unwrapAlamofireValidationError() -> Error {
guard let afError = asAFError else { return self }
Expand Down
Loading

0 comments on commit c293b10

Please sign in to comment.