Skip to content

Commit

Permalink
Merge pull request #63 from strvcom/feat/web-hosted-docs
Browse files Browse the repository at this point in the history
[feat] Web hosted documentation
  • Loading branch information
matejmolnar authored Feb 15, 2024
2 parents 337da1f + daead52 commit 9687308
Show file tree
Hide file tree
Showing 793 changed files with 1,555 additions and 667 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@ import Networking
import Foundation

final class SampleAuthorizationManager: AuthorizationManaging {
// MARK: Public properties
let storage: AuthorizationStorageManaging = SampleAuthorizationStorageManager()
// MARK: Private properties
/// For refresh token logic we create new instance of APIManager without injecting `AuthorizationTokenInterceptor` to avoid cycling in refreshes
/// We use mock data to simulate real API requests here

// For refresh token logic we create new instance of APIManager without
// injecting `AuthorizationTokenInterceptor` in order to avoid cycling in refreshes.
// We use mock data to simulate real API requests here.
private let apiManager: APIManager = {
APIManager(
responseProvider: MockResponseProvider(with: Bundle.main, sessionId: "2023-01-31T15:08:08Z"),
Expand Down
18 changes: 18 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,24 @@
"version" : "1.2.3"
}
},
{
"identity" : "swift-docc-plugin",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-docc-plugin",
"state" : {
"revision" : "26ac5758409154cc448d7ab82389c520fa8a8247",
"version" : "1.3.0"
}
},
{
"identity" : "swift-docc-symbolkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-docc-symbolkit",
"state" : {
"revision" : "b45d1f2ed151d057b54504d653e0da5552844e34",
"version" : "1.0.0"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
Expand Down
6 changes: 5 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@ let package = Package(
targets: ["Networking"]
)
],
dependencies: [.package(url: "https://github.com/realm/SwiftLint.git", exact: "0.53.0")],
dependencies: [
.package(url: "https://github.com/realm/SwiftLint.git", exact: "0.53.0"),
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "Networking",
swiftSettings: [.unsafeFlags(["-emit-extension-block-symbols"])],
plugins: [.plugin(name: "SwiftLintPlugin", package: "SwiftLint")]
),
.testTarget(
Expand Down
397 changes: 54 additions & 343 deletions README.md

Large diffs are not rendered by default.

53 changes: 41 additions & 12 deletions Sources/Networking/Core/APIManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,37 @@

import Foundation

/// Default API manager
/** Default API manager which is responsible for the creation and management of network requests.

You can define your own custom `APIManager` if needed by conforming to ``APIManaging``.

## Initialisation
There are two ways to initialise the `APIManager` object:
1. ``init(urlSession:requestAdapters:responseProcessors:errorProcessors:)`` - uses a `URLSession` as the response provider (typical usage).
2. ``init(responseProvider:requestAdapters:responseProcessors:errorProcessors:)`` - uses a custom response provider by conforming to ``ResponseProviding``. An example of a custom provider is ``MockResponseProvider``, which can be used for UI tests to interact with mocked data saved through ``EndpointRequestStorageProcessor``. To utilise them, simply move the stored session folder into the Asset catalogue.

## Making requests
There are two methods for making requests provided by the ``APIManaging`` protocol:
1. ``request(_:retryConfiguration:)-1usms`` - ``Response`` is a typealias for URLSession's default (data, response) tuple.
2. ``request(_:decoder:retryConfiguration:)`` - Result is custom decodable object

```swift
let userResponse: UserResponse = try await apiManager.request(UserRouter.getUser)
```

## Retry-ability
You can provide a custom after failure ``RetryConfiguration``, specifying the count of retries, delay and a handler that determines whether the request should be tried again. Otherwise, ``RetryConfiguration/default`` configuration is used.

```swift
let retryConfiguration = RetryConfiguration(retries: 2, delay: .constant(1)) { error in
// custom logic here
}
let userResponse: UserResponse = try await apiManager.request(
UserRouter.getUser,
retryConfiguration: retryConfiguration
)
```
*/
open class APIManager: APIManaging, Retryable {
private let requestAdapters: [RequestAdapting]
private let responseProcessors: [ResponseProcessing]
Expand All @@ -22,7 +52,7 @@ open class APIManager: APIManaging, Retryable {
responseProcessors: [ResponseProcessing] = [StatusCodeProcessor.shared],
errorProcessors: [ErrorProcessing] = []
) {
/// generate session id in readable format
// generate session id in readable format
if #unavailable(iOS 15) {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
Expand All @@ -43,7 +73,7 @@ open class APIManager: APIManaging, Retryable {
responseProcessors: [ResponseProcessing] = [StatusCodeProcessor.shared],
errorProcessors: [ErrorProcessing] = []
) {
/// generate session id in readable format
// generate session id in readable format
if #unavailable(iOS 15) {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
Expand All @@ -59,39 +89,38 @@ open class APIManager: APIManaging, Retryable {

@discardableResult
open func request(_ endpoint: Requestable, retryConfiguration: RetryConfiguration?) async throws -> Response {
/// create identifiable request from endpoint
// create identifiable request from endpoint
let endpointRequest = EndpointRequest(endpoint, sessionId: sessionId)
return try await request(endpointRequest, retryConfiguration: retryConfiguration)
}
}

// MARK: Private
private extension APIManager {
func request(_ endpointRequest: EndpointRequest, retryConfiguration: RetryConfiguration?) async throws -> Response {
do {
/// create original url request
// create original url request
var request = try endpointRequest.endpoint.asRequest()

/// adapt request with all adapters
// adapt request with all adapters
request = try await requestAdapters.adapt(request, for: endpointRequest)

/// get response for given request (usually fires a network request via URLSession)
// get response for given request (usually fires a network request via URLSession)
var response = try await responseProvider.response(for: request)

/// process request
// process request
response = try await responseProcessors.process(response, with: request, for: endpointRequest)

/// reset retry count
// reset retry count
await retryCounter.reset(for: endpointRequest.id)

return response
} catch {
do {
/// If retry fails (retryCount is 0 or Task.sleep throwed), catch the error and process it with `ErrorProcessing` plugins.
// If retry fails (retryCount is 0 or Task.sleep throwed), catch the error and process it with `ErrorProcessing` plugins.
try await sleepIfRetry(for: error, endpointRequest: endpointRequest, retryConfiguration: retryConfiguration)
return try await request(endpointRequest, retryConfiguration: retryConfiguration)
} catch {
/// error processing
// error processing
throw await errorProcessors.process(error, for: endpointRequest)
}
}
Expand Down
6 changes: 3 additions & 3 deletions Sources/Networking/Core/APIManaging.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Foundation

/// A definition of an API layer with methods for handling API requests.
public protocol APIManaging {
/// A default JSONDecoder used for all requests.
/// A default `JSONDecoder` used for all requests.
var defaultDecoder: JSONDecoder { get }

/// Creates a network request for an API endpoint defined by ``Requestable``.
Expand Down Expand Up @@ -76,7 +76,7 @@ public extension APIManaging {
/// Default implementation trying to decode data from response.
/// - Parameters:
/// - endpoint: API endpoint requestable definition.
/// - decoder: a JSONDecoder used for decoding the response data.
/// - decoder: a `JSONDecoder` used for decoding the response data.
/// - retryConfiguration: configuration for retrying behavior.
/// - Returns: an object decoded from the response data.
func request<DecodableResponse: Decodable>(
Expand All @@ -93,6 +93,6 @@ public extension APIManaging {
// MARK: - JSONDecoder static extension

private extension JSONDecoder {
/// A static JSONDecoder instance used by default implementation of APIManaging
/// A static `JSONDecoder` instance used by default implementation of `APIManaging`
static let `default` = JSONDecoder()
}
27 changes: 23 additions & 4 deletions Sources/Networking/Core/Download/DownloadAPIManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,25 @@
import Foundation
import Combine

/// Default Download API manager
/** Default download API manager which is responsible for the creation and management of network file downloads.

You can define your own custom `DownloadAPIManager` if needed by conforming to ``DownloadAPIManaging``.

The initialisation is equivalent to ``APIManager/init(urlSession:requestAdapters:responseProcessors:errorProcessors:)``, except the session is created for the user based on a given `URLSessionConfiguration` ``init(urlSessionConfiguration:requestAdapters:responseProcessors:errorProcessors:)``.

## Usage

1. Request download for a given endpoint with ``downloadRequest(_:resumableData:retryConfiguration:)`` or ``DownloadAPIManaging/downloadRequest(_:resumableData:retryConfiguration:)-5dbs2`` It creates a `URLSessionDownloadTask` and returns it along with ``Response``. The ``Response`` does not include the actual downloaded file, it solely an HTTP response received after the download is initiated.
2. The ``allTasks`` property enables you to keep track of current tasks in progress.
3. In order to observe progress of a specific task you can obtain an `AsyncStream` of ``Foundation/URLSessionTask/DownloadState`` with ``progressStream(for:)``.
Example:
```swift
for try await downloadState in downloadAPIManager.shared.progressStream(for: task) {
...
}
```
4. In case you are not using a singleton instance don't forget to call ``invalidateSession(shouldFinishTasks:)`` once the instance is not needed anymore in order to prevent memory leaks, since the `DownloadAPIManager` is not automatically deallocated from memory because of a `URLSession` holding a reference to it.
*/
open class DownloadAPIManager: NSObject, Retryable {
private let requestAdapters: [RequestAdapting]
private let responseProcessors: [ResponseProcessing]
Expand Down Expand Up @@ -56,7 +74,7 @@ open class DownloadAPIManager: NSObject, Retryable {
}
}

// MARK: Public API
// MARK: - Public
extension DownloadAPIManager: DownloadAPIManaging {
public func invalidateSession(shouldFinishTasks: Bool = false) {
if shouldFinishTasks {
Expand Down Expand Up @@ -100,7 +118,7 @@ extension DownloadAPIManager: DownloadAPIManaging {
}
}

// MARK: Private
// MARK: - Private
private extension DownloadAPIManager {
func downloadRequest(
_ endpointRequest: EndpointRequest,
Expand Down Expand Up @@ -192,7 +210,7 @@ private extension DownloadAPIManager {
}
}

// MARK: URLSession Delegate
// MARK: - URLSession Delegate
extension DownloadAPIManager: URLSessionDelegate, URLSessionDownloadDelegate {
public func urlSession(_: URLSession, downloadTask: URLSessionDownloadTask, didWriteData _: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
Task {
Expand Down Expand Up @@ -231,6 +249,7 @@ extension DownloadAPIManager: URLSessionDelegate, URLSessionDownloadDelegate {
}
}

// MARK: - URL extensions
extension URL {
enum FileError: Error {
case documentsDirectoryUnavailable
Expand Down
10 changes: 6 additions & 4 deletions Sources/Networking/Core/Download/DownloadAPIManaging.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@

import Foundation

// MARK: - Defines Download API managing
/// A download result consisting of `URLSessionDownloadTask` and ``Response``
public typealias DownloadResult = (URLSessionDownloadTask, Response)

// MARK: - Defines Download API managing

/// A definition of an API layer with methods for handling data downloading.
/// Recommended to be used as singleton.
/// If you wish to use multiple instances, make sure you manually invalidate url session by calling the `invalidateSession` method.
///
/// Recommended to be used as singleton. If you wish to use multiple instances, make sure you manually invalidate url session by calling the `invalidateSession` method.
public protocol DownloadAPIManaging {
/// List of all currently ongoing download tasks.
var allTasks: [URLSessionDownloadTask] { get async }
Expand All @@ -27,7 +29,7 @@ public protocol DownloadAPIManaging {
/// - endpoint: API endpoint requestable definition.
/// - resumableData: Optional data the download request will be resumed with.
/// - retryConfiguration: Configuration for retrying behaviour.
/// - Returns: A download result consisting of `URLSessionDownloadTask` and `Response`
/// - Returns: A download result consisting of `URLSessionDownloadTask` and ``Response``
func downloadRequest(
_ endpoint: Requestable,
resumableData: Data?,
Expand Down
2 changes: 0 additions & 2 deletions Sources/Networking/Core/EndpointRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@

import Foundation

// MARK: - Struct wrapping one call to the API endpoint

/// A wrapper structure which contains API endpoint with additional info about the session within which it's being called and an API call identifier.
public struct EndpointRequest: Identifiable {
public let id: String
Expand Down
2 changes: 1 addition & 1 deletion Sources/Networking/Core/ErrorProcessing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public protocol ErrorProcessing {
func process(_ error: Error, for endpointRequest: EndpointRequest) async -> Error
}

// MARK: - Array extension to avoid boilerplate
/// Array extension to avoid boilerplate
public extension Array where Element == ErrorProcessing {
/// Applies the process method to all objects in a sequence.
/// - Parameters:
Expand Down
4 changes: 1 addition & 3 deletions Sources/Networking/Core/RequestDataType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@

import Foundation

// MARK: - Defines various request data types to be sent in body

/// A data type of request body.
/// Defines various request data types to be sent in body.
public enum RequestDataType {
/// Encodable data type, sets HTTP header content type to application/json. Optionally hide sensitive request data from logs.
case encodable(Encodable, encoder: JSONEncoder = JSONEncoder(), hideFromLogs: Bool = false)
Expand Down
2 changes: 0 additions & 2 deletions Sources/Networking/Core/RequestInterceptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,5 @@

import Foundation

// MARK: - Define modifiers working before & after request

/// A modifier which adapts a request and also processes a response.
public typealias RequestInterceptor = RequestAdapting & ResponseProcessing & ErrorProcessing
60 changes: 58 additions & 2 deletions Sources/Networking/Core/Requestable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,65 @@

import Foundation

// MARK: - Endpoint definition
/** A type that represents an API endpoint.

By conforming to the ``Requestable`` protocol, you can define endpoint definitions containing the elementary HTTP request components necessary to create valid HTTP requests.
<br>**Recommendation:** Follow the `Router` naming convention to explicitly indicate the usage of a router pattern.

### Example
```swift
enum UserRouter {
case getUser
case updateUser(UpdateUserRequest)
}

extension UserRouter: Requestable {
// The base URL address used for the HTTP call.
var baseURL: URL {
URL(string: Constants.baseHost)!
}

// Path will be appended to the base URL.
var path: String {
switch self {
case .getUser, .updateUser:
return "/user"
}
}

// HTTPMethod used for each endpoint.
var method: HTTPMethod {
switch self {
case .getUser:
return .get
case .updateUser:
return .post
}
}

// Optional body data encoded in JSON by default.
var dataType: RequestDataType? {
switch self {
case .getUser:
return nil
case let .updateUser(data):
return .encodable(data)
}
}

// Optional authentication requirement if AuthorizationInterceptor is used.
var isAuthenticationRequired: Bool {
switch self {
case .getUser, .updateUser:
return true
}
}
}
```

Some of the properties have default implementations defined in the `Requestable+Convenience` extension.
*/

/// A type that represents an API endpoint.
public protocol Requestable: EndpointIdentifiable {
/// The host URL of REST API.
var baseURL: URL { get }
Expand Down
2 changes: 0 additions & 2 deletions Sources/Networking/Core/RequestableError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@

import Foundation

// MARK: - Defines errors for endpoints composing URL request

/// An Error that occurs during the creation of `URLRequest` from ``Requestable``.
public enum RequestableError: Error {
/// An indication that the properties in ``Requestable`` cannot form valid `URLComponents`.
Expand Down
Loading

0 comments on commit 9687308

Please sign in to comment.