-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Ivaylo Gashev
committed
Oct 7, 2022
1 parent
2c7301e
commit 6d89174
Showing
14 changed files
with
550 additions
and
174 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import Foundation | ||
|
||
/// Use this object to make network calls and receive decoded values using the new structured concurrency (async/await). | ||
public struct AsyncCaller { | ||
private let middleware: [URLRequestPlugable] | ||
private let urlSession: URLSession | ||
private let utility: CallerUtility | ||
|
||
/// Initialises an object which can make network calls. | ||
/// - Parameters: | ||
/// - urlSession: Session that would make the actual network call. | ||
/// - decoder: Decoder that would decode the received data from the network call. | ||
/// - middleware: Middleware that is injected in the networking events. | ||
public init(urlSession: URLSession = .shared, decoder: JSONDecoder, middleware: [URLRequestPlugable] = []) { | ||
self.urlSession = urlSession | ||
self.middleware = middleware | ||
self.utility = .init(decoder: decoder) | ||
} | ||
|
||
public func call<D: Decodable, DE: DecodableError>( | ||
using builder: URLRequestBuilder, | ||
errorType: DE.Type | ||
) async throws -> D { | ||
let request = try builder.build() | ||
middleware.forEach { $0.onRequest(request) } | ||
|
||
do { | ||
let (data, response) = try await urlSession.data(for: request) | ||
let tryMap = try utility.checkResponseForErrors(data: data, urlResponse: response, errorType: errorType) | ||
let tryMap2: D = try utility.decodeIfNecessary(tryMap) | ||
middleware.forEach { $0.onResponse(data: data, response: response) } | ||
return tryMap2 | ||
} catch { | ||
let mappedError = utility.mapError(error) | ||
middleware.forEach { $0.onError(mappedError, request: request) } | ||
throw mappedError | ||
} | ||
} | ||
|
||
public func call<E: DecodableError>( | ||
using builder: URLRequestBuilder, | ||
errorType: E.Type | ||
) async throws { | ||
let request = try builder.build() | ||
middleware.forEach { $0.onRequest(request) } | ||
|
||
do { | ||
let (data, response) = try await urlSession.data(for: request) | ||
let tryMap = try utility.checkResponseForErrors(data: data, urlResponse: response, errorType: errorType) | ||
try utility.tryMapEmptyResponseBody(data: tryMap) | ||
middleware.forEach { $0.onResponse(data: data, response: response) } | ||
} catch { | ||
let mappedError = utility.mapError(error) | ||
middleware.forEach { $0.onError(mappedError, request: request) } | ||
throw mappedError | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import Foundation | ||
|
||
public typealias DecodableError = Error & Decodable | ||
|
||
struct CallerUtility { | ||
let decoder: JSONDecoder | ||
|
||
/// Checks if any errors need to be thrown based on the response | ||
/// - Parameters: | ||
/// - data: The data of the response | ||
/// - urlResponse: The URLResponse | ||
/// - errorType: An Error type to be decoded from the body in non-success cases | ||
/// - Throws: `NetworkingError` | ||
/// - Returns: The data that was passed in | ||
func checkResponseForErrors<DE: DecodableError>( | ||
data: Data, | ||
urlResponse: URLResponse, | ||
errorType: DE.Type | ||
) throws -> Data { | ||
guard | ||
let httpResponse = urlResponse as? HTTPURLResponse, | ||
let status = HTTPStatus(rawValue: httpResponse.statusCode) | ||
else { | ||
throw NetworkingError.networking( | ||
status: .internalServerError, | ||
error: try? decoder.decode(DE.self, from: data) | ||
) | ||
} | ||
|
||
guard status.isSuccess else { | ||
throw NetworkingError.networking(status: status, error: try? decoder.decode(DE.self, from: data)) | ||
} | ||
|
||
return data | ||
} | ||
|
||
func mapError(_ error: Error) -> NetworkingError { | ||
switch error { | ||
case let decodingError as DecodingError: | ||
return NetworkingError.decoding(error: decodingError) | ||
case let networkingError as NetworkingError: | ||
return networkingError | ||
default: | ||
return .unknown(error) | ||
} | ||
} | ||
|
||
func tryMapEmptyResponseBody(data: Data) throws { | ||
guard !data.isEmpty else { | ||
return | ||
} | ||
|
||
let context = DecodingError.Context(codingPath: [], debugDescription: "Void expects empty body.") | ||
throw DecodingError.dataCorrupted(context) | ||
} | ||
|
||
func decodeIfNecessary<D: Decodable>(_ data: Data) throws -> D { | ||
// In cases where D is Data, we return the raw data instead of attempting to decode it | ||
if let data = data as? D { | ||
return data | ||
} | ||
// Otherwise - run through the decoder | ||
else { | ||
return try decoder.decode(D.self, from: data) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
import Combine | ||
import Foundation | ||
|
||
/// Use this object to make network calls and receive decoded values wrapped into Combine's `AnyPublisher`. | ||
public struct CombineCaller { | ||
public typealias AnyURLSessionDataPublisher = AnyPublisher<URLSession.DataTaskPublisher.Output, URLSession.DataTaskPublisher.Failure> | ||
|
||
private let middleware: [URLRequestPlugable] | ||
private let utility: CallerUtility | ||
|
||
/// Gets the data task publisher. | ||
private let getDataDataTaskPublisher: (URLRequest) -> AnyURLSessionDataPublisher | ||
|
||
/// Initialises an object which can make network calls. | ||
/// - Parameters: | ||
/// - urlSession: Session that would make the actual network call. | ||
/// - decoder: Decoder that would decode the received data from the network call. | ||
/// - middleware: Middleware that is injected in the networking events. | ||
public init(urlSession: URLSession = .shared, decoder: JSONDecoder, middleware: [URLRequestPlugable] = []) { | ||
self.init( | ||
decoder: decoder, | ||
getDataPublisher: { urlSession.dataTaskPublisher(for: $0).eraseToAnyPublisher() }, | ||
middleware: middleware | ||
) | ||
} | ||
|
||
/// Initialises an object which can make network calls. | ||
/// - Parameters: | ||
/// - decoder: Decoder that would decode the received data from the network call. | ||
/// - getDataPublisher: A closure that returns a data task publisher. | ||
init( | ||
decoder: JSONDecoder, | ||
getDataPublisher: @escaping (URLRequest) -> AnyURLSessionDataPublisher, | ||
middleware: [URLRequestPlugable] = [] | ||
) { | ||
self.middleware = middleware | ||
self.getDataDataTaskPublisher = getDataPublisher | ||
self.utility = .init(decoder: decoder) | ||
} | ||
|
||
/// Method which calls the network request. | ||
/// - Parameters: | ||
/// - request: The `URLRequest` that should be called. | ||
/// - errorType: The error type to be decoded from the body in non-success cases. | ||
/// - Returns: The result from the network call wrapped into `AnyPublisher`. | ||
public func call<D: Decodable, DE: DecodableError>( | ||
using request: URLRequest, | ||
errorType: DE.Type | ||
) -> AnyPublisher<D, NetworkingError> { | ||
getDataDataTaskPublisher(request) | ||
.attachMiddleware(middleware, for: request) | ||
.tryMap { try utility.checkResponseForErrors(data: $0.data, urlResponse: $0.response, errorType: errorType) } | ||
.tryMap(utility.decodeIfNecessary) | ||
.mapError(utility.mapError) | ||
.attachCompletionMiddleware(middleware, request: request) | ||
.eraseToAnyPublisher() | ||
} | ||
|
||
/// Method which calls the network request without expecting a response body. | ||
/// - Parameters: | ||
/// - request: The `URLRequest` that should be called. | ||
/// - errorType: The error type to be decoded from the body in non-success cases. | ||
/// - Returns: The result from the network call wrapped into `AnyPublisher`. | ||
public func call<DE: DecodableError>( | ||
using request: URLRequest, | ||
errorType: DE.Type | ||
) -> AnyPublisher<Void, NetworkingError> { | ||
getDataDataTaskPublisher(request) | ||
.attachMiddleware(middleware, for: request) | ||
.tryMap { try utility.checkResponseForErrors(data: $0.data, urlResponse: $0.response, errorType: errorType) } | ||
.tryMap(utility.tryMapEmptyResponseBody(data:)) | ||
.mapError(utility.mapError) | ||
.attachCompletionMiddleware(middleware, request: request) | ||
.eraseToAnyPublisher() | ||
} | ||
} | ||
|
||
public extension CombineCaller { | ||
/// Convenient method which calls the builded network request using the `URLRequestBuilder` object. | ||
/// The building and the error handling of the `URLRequest` are handled here. | ||
/// - Parameters: | ||
/// - builder: The builder from which the `URLRequest` will be constructed and called. | ||
/// - errorType: The error type to be decoded from the body in non-success cases. | ||
/// - Returns: The result from the network call wrapped into `AnyPublisher`. | ||
func call<D: Decodable, E: DecodableError>( | ||
using builder: URLRequestBuilder, | ||
errorType: E.Type | ||
) -> AnyPublisher<D, NetworkingError> { | ||
do { | ||
let urlRequest = try builder.build() | ||
return call(using: urlRequest, errorType: errorType) | ||
} catch { | ||
return Fail(error: utility.mapError(error)) | ||
.attachCompletionMiddleware(middleware, request: nil) | ||
.eraseToAnyPublisher() | ||
} | ||
} | ||
|
||
/// Convenient method which calls the builded network request using the `URLRequestBuilder` object without expecting a response body. | ||
/// The building and the error handling of the `URLRequest` are handled here. | ||
/// - Parameters: | ||
/// - builder: The builder from which the `URLRequest` will be constructed and called. | ||
/// - errorType: The error type to be decoded from the body in non-success cases. | ||
/// - Returns: The result from the network call wrapped into `AnyPublisher`. | ||
func call<E: DecodableError>( | ||
using builder: URLRequestBuilder, | ||
errorType: E.Type | ||
) -> AnyPublisher<Void, NetworkingError> { | ||
do { | ||
let urlRequest = try builder.build() | ||
return call(using: urlRequest, errorType: errorType) | ||
} catch { | ||
return Fail(error: utility.mapError(error)) | ||
.attachCompletionMiddleware(middleware, request: nil) | ||
.eraseToAnyPublisher() | ||
} | ||
} | ||
} | ||
|
||
private extension CombineCaller.AnyURLSessionDataPublisher { | ||
func attachMiddleware( | ||
_ middleware: [URLRequestPlugable], | ||
for request: URLRequest | ||
) -> Publishers.HandleEvents<Self> { | ||
handleEvents( | ||
receiveSubscription: { _ in | ||
middleware.forEach { $0.onRequest(request) } | ||
}, | ||
receiveOutput: { data, response in | ||
middleware.forEach { $0.onResponse(data: data, response: response) } | ||
} | ||
) | ||
} | ||
} | ||
|
||
private extension Publisher where Failure == NetworkingError { | ||
func attachCompletionMiddleware( | ||
_ middleware: [URLRequestPlugable], | ||
request: URLRequest? | ||
) -> Publishers.HandleEvents<Self> { | ||
handleEvents(receiveCompletion: { completion in | ||
switch completion { | ||
case .failure(let error): | ||
middleware.forEach { $0.onError(error, request: request) } | ||
default: | ||
return | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.