Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,12 @@ protocol OrganizationLoading: ContentLoading {
var state: LoadingState<Organization> { get }
}

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
final class OrganizationLoader: WebLoader<Organization>, OrganizationLoading {
func load() {
guard startLoading() else { return }
request(OrganizationEndpoint()) { result in
// imitation of waiting for the request for 3 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
self.finishLoading(result)
}
Task {
await request(OrganizationEndpoint())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,12 @@ protocol RepoLoading: ContentLoading {
var state: LoadingState<[Repository]> { get }
}

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
final class RepositoriesLoader: WebLoader<[Repository]>, RepoLoading {
func load() {
guard startLoading() else { return }
request(RepositoriesEndpoint()) { result in
// imitation of waiting for the request for 5 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
self.finishLoading(result)
}
Task {
await request(RepositoriesEndpoint())
}
}
}
5 changes: 4 additions & 1 deletion Documentation/loader.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ protocol UserProfileLoading: ContentLoading {
var state: LoadingState<UserProfile> { get }
}

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
final class UserProfileLoader: WebLoader<UserProfile>, UserProfileLoading {
func load() {
guard startLoading() else { return }
request(UserProfileEndpoint())
Task {
await request(UserProfileEndpoint())
}
}
}
```
Expand Down
5 changes: 4 additions & 1 deletion Documentation/loader_ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ protocol UserProfileLoading: ContentLoading {
var state: LoadingState<UserProfile> { get }
}

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
final class UserProfileLoader: WebLoader<UserProfile>, UserProfileLoading {
func load() {
guard startLoading() else { return }
request(UserProfileEndpoint())
Task {
await request(UserProfileEndpoint())
}
}
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ protocol BookService {

final class BookServiceImpl: BookService {

let apiClient: ConcurrencyClient
let apiClient: Client

init(apiClient: ConcurrencyClient) {
init(apiClient: Client) {
self.apiClient = apiClient
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ protocol FileService {

final class FileServiceImpl: FileService {

let apiClient: ConcurrencyClient
let apiClient: Client

init(apiClient: ConcurrencyClient) {
init(apiClient: Client) {
self.apiClient = apiClient
}

Expand Down
2 changes: 1 addition & 1 deletion Example/Example/Sources/Business Logic/ServiceLayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ final class ServiceLayer {

static let shared = ServiceLayer()

private(set) lazy var apiClient: ConcurrencyClient = AlamofireClient(
private(set) lazy var apiClient: Client = AlamofireClient(
baseURL: URL(string: "https://library.mock-object.redmadserver.com/api/v1/")!,
configuration: .ephemeral,
responseObserver: { [weak self] request, response, data, error in
Expand Down
29 changes: 13 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,36 +93,28 @@ public struct BookEndpoint: Endpoint {
}
}

let client = Client ...
let client: Client = ...

let endpoint = BookEndpoint(id: "1")
client.request(endpoint) { (result: Result<Book, Error>)
print(result)
}
let book = try await client.request(endpoint)
print(book)
```

## Client

`Client` - an object with only one method for executing `Endpoint`.
- It's easy to mock, because it has only one method.
`Client` - an object with methods for executing `Endpoint` using modern async/await syntax.
- It's easy to mock, because it has a simple interface.
- It's easy to send several `Endpoint`.
- Easily wraps into decorators or adapters. For example, you can wrap in `Combine` and you don't have to make wrappers for each request.
- Supports both `request` and `upload` operations with async/await.

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.
`Client` and `CombineClient` are separated protocols. You can specify method that you are using by using specific protocol.

## Getting Started

Expand Down Expand Up @@ -222,7 +214,7 @@ public struct DeleteBookEndpoint: VoidEndpoint, URLRequestBuildable {

### Sending a large amount of data to the server

You can use `UploadEndpoint` to send files or large amounts of data. In the `makeRequest()` method you need to return `URLRequest` and the data you are uploading, it can be a file `.file(URL)`, a data `.data(Data)` or a stream `.stream(InputStream)`. To execute the request, call the `Client.upload(endpoint: completionHandler:)` method. Use `Progress` object to track the progress of the data upload or cancel the request.
You can use `UploadEndpoint` to send files or large amounts of data. In the `makeRequest()` method you need to return `URLRequest` and the data you are uploading, it can be a file `.file(URL)`, a data `.data(Data)` or a stream `.stream(InputStream)`. To execute the request, call the `Client.upload(endpoint:)` method using async/await.

```swift
public struct FileUploadEndpoint: UploadEndpoint {
Expand All @@ -247,6 +239,11 @@ public struct FileUploadEndpoint: UploadEndpoint {
return (request, .file(fileUrl))
}
}

// Usage with async/await
let client: Client = ...
let endpoint = FileUploadEndpoint(fileUrl: fileURL)
try await client.upload(endpoint)
```

## Network Layer Organization
Expand Down
29 changes: 13 additions & 16 deletions README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,36 +94,28 @@ public struct BookEndpoint: Endpoint {
}
}

let client = Client ...
let client: Client = ...

let endpoint = BookEndpoint(id: "1")
client.request(endpoint) { (result: Result<Book, Error>)
print(result)
}
let book = try await client.request(endpoint)
print(book)
```

## Client

`Client` - объект с одним методом способный выполнить `Endpoint`.
- Легко мокается, так как у него один метод.
`Client` - объект с методами для выполнения `Endpoint` используя современный синтаксис async/await.
- Легко мокается, так как у него простой интерфейс.
- Легко отправить через него несколько разных `Endpoint`.
- Легко оборачивается в декораторы или адаптеры. Например можно обернуть в `Combine` и вам не придется делать обертки для каждого запроса.
- Поддерживает как `request`, так и `upload` операции с async/await.

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

### CombineClient

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

### ConcurrencyClient

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

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

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

## Getting Started

Expand Down Expand Up @@ -223,7 +215,7 @@ public struct DeleteBookEndpoint: VoidEndpoint, URLRequestBuildable {

### Отправка данных на сервер

Для отправки файлов или больших объемов данных вы можете использовать `UploadEndpoint`. В методе `makeRequest()` необходимо вернуть `URLRequest` и загружаемые данные, это может быть файл `.file(URL)`, данные `.data(Data)` или поток `.stream(InputStream)`. Для выполнения запроса вызовите метод `Client.upload(endpoint:, completionHandler:)`. С помощью объекта `Progress` вы сможете отслеживать прогресс загрузки данных либо отменить запрос.
Для отправки файлов или больших объемов данных вы можете использовать `UploadEndpoint`. В методе `makeRequest()` необходимо вернуть `URLRequest` и загружаемые данные, это может быть файл `.file(URL)`, данные `.data(Data)` или поток `.stream(InputStream)`. Для выполнения запроса вызовите метод `Client.upload(endpoint:)` используя async/await.

```swift
public struct FileUploadEndpoint: UploadEndpoint {
Expand All @@ -248,6 +240,11 @@ public struct FileUploadEndpoint: UploadEndpoint {
return (request, .file(fileUrl))
}
}

// Использование с async/await
let client: Client = ...
let endpoint = FileUploadEndpoint(fileUrl: fileURL)
try await client.upload(endpoint)
```

## Организация сетевого слоя
Expand Down
25 changes: 8 additions & 17 deletions Sources/Apexy/Client.swift
Original file line number Diff line number Diff line change
@@ -1,26 +1,17 @@
import Foundation

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

/// Send request to specified endpoint.
///
/// - Parameters:
/// - endpoint: endpoint of remote content.
/// - completionHandler: The completion closure to be executed when request is completed.
/// - Returns: The progress of fetching the response data from the server for the request.
func request<T>(
_ endpoint: T,
completionHandler: @escaping (APIResult<T.Content>) -> Void
) -> Progress where T: Endpoint
/// - 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: The remote endpoint and data to upload.
/// - completionHandler: The completion closure to be executed when request is completed.
/// - Returns: The progress of uploading data to the server.
func upload<T>(
_ endpoint: T,
completionHandler: @escaping (APIResult<T.Content>) -> Void
) -> Progress where T: UploadEndpoint
/// - 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

}
42 changes: 27 additions & 15 deletions Sources/Apexy/Clients/CombineClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,40 @@ public protocol CombineClient: AnyObject {
/// - endpoint: endpoint of remote content.
/// - Returns: Publisher which you can subscribe to
func request<T>(_ endpoint: T) -> AnyPublisher<T.Content, Error> where T: Endpoint

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

@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>()

let progress = self.request(endpoint) { (result: Result<T.Content, Error>) in
switch result {
case .success(let content):
subject.send(content)
subject.send(completion: .finished)
case .failure(let error):
subject.send(completion: .failure(error))
Future<T.Content, Error> { promise in
Task {
do {
let content = try await self.request(endpoint)
promise(.success(content))
} catch {
promise(.failure(error))
}
}
}
.eraseToAnyPublisher()
}

func upload<T>(_ endpoint: T) -> AnyPublisher<T.Content, Error> where T: UploadEndpoint {
Future<T.Content, Error> { promise in
Task {
do {
let content = try await self.upload(endpoint)
promise(.success(content))
} catch {
promise(.failure(error))
}
}

return subject.handleEvents(receiveCancel: {
progress.cancel()
subject.send(completion: .finished)
}).eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
Expand Down
24 changes: 0 additions & 24 deletions Sources/Apexy/Clients/ConcurrencyClient.swift

This file was deleted.

Loading