Skip to content

Commit 6d89174

Browse files
author
Ivaylo Gashev
committed
added async support
1 parent 2c7301e commit 6d89174

14 files changed

Lines changed: 550 additions & 174 deletions

Package.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
// swift-tools-version:5.3
1+
// swift-tools-version:5.5
22

33
import PackageDescription
44

55
let package = Package(
66
name: "NetworkRequester",
77
platforms: [
8-
.iOS(.v11),
9-
.macOS(.v10_15),
8+
.iOS(.v13),
9+
.macOS(.v12),
1010
.tvOS(.v13),
1111
],
1212
products: [
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import Foundation
2+
3+
/// Use this object to make network calls and receive decoded values using the new structured concurrency (async/await).
4+
public struct AsyncCaller {
5+
private let middleware: [URLRequestPlugable]
6+
private let urlSession: URLSession
7+
private let utility: CallerUtility
8+
9+
/// Initialises an object which can make network calls.
10+
/// - Parameters:
11+
/// - urlSession: Session that would make the actual network call.
12+
/// - decoder: Decoder that would decode the received data from the network call.
13+
/// - middleware: Middleware that is injected in the networking events.
14+
public init(urlSession: URLSession = .shared, decoder: JSONDecoder, middleware: [URLRequestPlugable] = []) {
15+
self.urlSession = urlSession
16+
self.middleware = middleware
17+
self.utility = .init(decoder: decoder)
18+
}
19+
20+
public func call<D: Decodable, DE: DecodableError>(
21+
using builder: URLRequestBuilder,
22+
errorType: DE.Type
23+
) async throws -> D {
24+
let request = try builder.build()
25+
middleware.forEach { $0.onRequest(request) }
26+
27+
do {
28+
let (data, response) = try await urlSession.data(for: request)
29+
let tryMap = try utility.checkResponseForErrors(data: data, urlResponse: response, errorType: errorType)
30+
let tryMap2: D = try utility.decodeIfNecessary(tryMap)
31+
middleware.forEach { $0.onResponse(data: data, response: response) }
32+
return tryMap2
33+
} catch {
34+
let mappedError = utility.mapError(error)
35+
middleware.forEach { $0.onError(mappedError, request: request) }
36+
throw mappedError
37+
}
38+
}
39+
40+
public func call<E: DecodableError>(
41+
using builder: URLRequestBuilder,
42+
errorType: E.Type
43+
) async throws {
44+
let request = try builder.build()
45+
middleware.forEach { $0.onRequest(request) }
46+
47+
do {
48+
let (data, response) = try await urlSession.data(for: request)
49+
let tryMap = try utility.checkResponseForErrors(data: data, urlResponse: response, errorType: errorType)
50+
try utility.tryMapEmptyResponseBody(data: tryMap)
51+
middleware.forEach { $0.onResponse(data: data, response: response) }
52+
} catch {
53+
let mappedError = utility.mapError(error)
54+
middleware.forEach { $0.onError(mappedError, request: request) }
55+
throw mappedError
56+
}
57+
}
58+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import Foundation
2+
3+
public typealias DecodableError = Error & Decodable
4+
5+
struct CallerUtility {
6+
let decoder: JSONDecoder
7+
8+
/// Checks if any errors need to be thrown based on the response
9+
/// - Parameters:
10+
/// - data: The data of the response
11+
/// - urlResponse: The URLResponse
12+
/// - errorType: An Error type to be decoded from the body in non-success cases
13+
/// - Throws: `NetworkingError`
14+
/// - Returns: The data that was passed in
15+
func checkResponseForErrors<DE: DecodableError>(
16+
data: Data,
17+
urlResponse: URLResponse,
18+
errorType: DE.Type
19+
) throws -> Data {
20+
guard
21+
let httpResponse = urlResponse as? HTTPURLResponse,
22+
let status = HTTPStatus(rawValue: httpResponse.statusCode)
23+
else {
24+
throw NetworkingError.networking(
25+
status: .internalServerError,
26+
error: try? decoder.decode(DE.self, from: data)
27+
)
28+
}
29+
30+
guard status.isSuccess else {
31+
throw NetworkingError.networking(status: status, error: try? decoder.decode(DE.self, from: data))
32+
}
33+
34+
return data
35+
}
36+
37+
func mapError(_ error: Error) -> NetworkingError {
38+
switch error {
39+
case let decodingError as DecodingError:
40+
return NetworkingError.decoding(error: decodingError)
41+
case let networkingError as NetworkingError:
42+
return networkingError
43+
default:
44+
return .unknown(error)
45+
}
46+
}
47+
48+
func tryMapEmptyResponseBody(data: Data) throws {
49+
guard !data.isEmpty else {
50+
return
51+
}
52+
53+
let context = DecodingError.Context(codingPath: [], debugDescription: "Void expects empty body.")
54+
throw DecodingError.dataCorrupted(context)
55+
}
56+
57+
func decodeIfNecessary<D: Decodable>(_ data: Data) throws -> D {
58+
// In cases where D is Data, we return the raw data instead of attempting to decode it
59+
if let data = data as? D {
60+
return data
61+
}
62+
// Otherwise - run through the decoder
63+
else {
64+
return try decoder.decode(D.self, from: data)
65+
}
66+
}
67+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import Combine
2+
import Foundation
3+
4+
/// Use this object to make network calls and receive decoded values wrapped into Combine's `AnyPublisher`.
5+
public struct CombineCaller {
6+
public typealias AnyURLSessionDataPublisher = AnyPublisher<URLSession.DataTaskPublisher.Output, URLSession.DataTaskPublisher.Failure>
7+
8+
private let middleware: [URLRequestPlugable]
9+
private let utility: CallerUtility
10+
11+
/// Gets the data task publisher.
12+
private let getDataDataTaskPublisher: (URLRequest) -> AnyURLSessionDataPublisher
13+
14+
/// Initialises an object which can make network calls.
15+
/// - Parameters:
16+
/// - urlSession: Session that would make the actual network call.
17+
/// - decoder: Decoder that would decode the received data from the network call.
18+
/// - middleware: Middleware that is injected in the networking events.
19+
public init(urlSession: URLSession = .shared, decoder: JSONDecoder, middleware: [URLRequestPlugable] = []) {
20+
self.init(
21+
decoder: decoder,
22+
getDataPublisher: { urlSession.dataTaskPublisher(for: $0).eraseToAnyPublisher() },
23+
middleware: middleware
24+
)
25+
}
26+
27+
/// Initialises an object which can make network calls.
28+
/// - Parameters:
29+
/// - decoder: Decoder that would decode the received data from the network call.
30+
/// - getDataPublisher: A closure that returns a data task publisher.
31+
init(
32+
decoder: JSONDecoder,
33+
getDataPublisher: @escaping (URLRequest) -> AnyURLSessionDataPublisher,
34+
middleware: [URLRequestPlugable] = []
35+
) {
36+
self.middleware = middleware
37+
self.getDataDataTaskPublisher = getDataPublisher
38+
self.utility = .init(decoder: decoder)
39+
}
40+
41+
/// Method which calls the network request.
42+
/// - Parameters:
43+
/// - request: The `URLRequest` that should be called.
44+
/// - errorType: The error type to be decoded from the body in non-success cases.
45+
/// - Returns: The result from the network call wrapped into `AnyPublisher`.
46+
public func call<D: Decodable, DE: DecodableError>(
47+
using request: URLRequest,
48+
errorType: DE.Type
49+
) -> AnyPublisher<D, NetworkingError> {
50+
getDataDataTaskPublisher(request)
51+
.attachMiddleware(middleware, for: request)
52+
.tryMap { try utility.checkResponseForErrors(data: $0.data, urlResponse: $0.response, errorType: errorType) }
53+
.tryMap(utility.decodeIfNecessary)
54+
.mapError(utility.mapError)
55+
.attachCompletionMiddleware(middleware, request: request)
56+
.eraseToAnyPublisher()
57+
}
58+
59+
/// Method which calls the network request without expecting a response body.
60+
/// - Parameters:
61+
/// - request: The `URLRequest` that should be called.
62+
/// - errorType: The error type to be decoded from the body in non-success cases.
63+
/// - Returns: The result from the network call wrapped into `AnyPublisher`.
64+
public func call<DE: DecodableError>(
65+
using request: URLRequest,
66+
errorType: DE.Type
67+
) -> AnyPublisher<Void, NetworkingError> {
68+
getDataDataTaskPublisher(request)
69+
.attachMiddleware(middleware, for: request)
70+
.tryMap { try utility.checkResponseForErrors(data: $0.data, urlResponse: $0.response, errorType: errorType) }
71+
.tryMap(utility.tryMapEmptyResponseBody(data:))
72+
.mapError(utility.mapError)
73+
.attachCompletionMiddleware(middleware, request: request)
74+
.eraseToAnyPublisher()
75+
}
76+
}
77+
78+
public extension CombineCaller {
79+
/// Convenient method which calls the builded network request using the `URLRequestBuilder` object.
80+
/// The building and the error handling of the `URLRequest` are handled here.
81+
/// - Parameters:
82+
/// - builder: The builder from which the `URLRequest` will be constructed and called.
83+
/// - errorType: The error type to be decoded from the body in non-success cases.
84+
/// - Returns: The result from the network call wrapped into `AnyPublisher`.
85+
func call<D: Decodable, E: DecodableError>(
86+
using builder: URLRequestBuilder,
87+
errorType: E.Type
88+
) -> AnyPublisher<D, NetworkingError> {
89+
do {
90+
let urlRequest = try builder.build()
91+
return call(using: urlRequest, errorType: errorType)
92+
} catch {
93+
return Fail(error: utility.mapError(error))
94+
.attachCompletionMiddleware(middleware, request: nil)
95+
.eraseToAnyPublisher()
96+
}
97+
}
98+
99+
/// Convenient method which calls the builded network request using the `URLRequestBuilder` object without expecting a response body.
100+
/// The building and the error handling of the `URLRequest` are handled here.
101+
/// - Parameters:
102+
/// - builder: The builder from which the `URLRequest` will be constructed and called.
103+
/// - errorType: The error type to be decoded from the body in non-success cases.
104+
/// - Returns: The result from the network call wrapped into `AnyPublisher`.
105+
func call<E: DecodableError>(
106+
using builder: URLRequestBuilder,
107+
errorType: E.Type
108+
) -> AnyPublisher<Void, NetworkingError> {
109+
do {
110+
let urlRequest = try builder.build()
111+
return call(using: urlRequest, errorType: errorType)
112+
} catch {
113+
return Fail(error: utility.mapError(error))
114+
.attachCompletionMiddleware(middleware, request: nil)
115+
.eraseToAnyPublisher()
116+
}
117+
}
118+
}
119+
120+
private extension CombineCaller.AnyURLSessionDataPublisher {
121+
func attachMiddleware(
122+
_ middleware: [URLRequestPlugable],
123+
for request: URLRequest
124+
) -> Publishers.HandleEvents<Self> {
125+
handleEvents(
126+
receiveSubscription: { _ in
127+
middleware.forEach { $0.onRequest(request) }
128+
},
129+
receiveOutput: { data, response in
130+
middleware.forEach { $0.onResponse(data: data, response: response) }
131+
}
132+
)
133+
}
134+
}
135+
136+
private extension Publisher where Failure == NetworkingError {
137+
func attachCompletionMiddleware(
138+
_ middleware: [URLRequestPlugable],
139+
request: URLRequest?
140+
) -> Publishers.HandleEvents<Self> {
141+
handleEvents(receiveCompletion: { completion in
142+
switch completion {
143+
case .failure(let error):
144+
middleware.forEach { $0.onError(error, request: request) }
145+
default:
146+
return
147+
}
148+
})
149+
}
150+
}

Sources/NetworkRequester/HTTP/HTTPBody.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,15 @@ public struct HTTPBody {
2121
}
2222
}
2323
}
24+
25+
extension HTTPBody: Equatable {
26+
public static func == (lhs: HTTPBody, rhs: HTTPBody) -> Bool {
27+
do {
28+
let lhsData = try lhs.data()
29+
let rhsData = try rhs.data()
30+
return lhsData == rhsData
31+
} catch {
32+
return false
33+
}
34+
}
35+
}

Sources/NetworkRequester/HTTP/HTTPMethod.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// Represents an HTTP method of a request.
22
public enum HTTPMethod {
3-
case get, post, delete, patch
3+
case get, post, put, delete, patch
44
}
55

66
// MARK: - CustomStringConvertible
@@ -12,6 +12,8 @@ extension HTTPMethod: CustomStringConvertible {
1212
return "GET"
1313
case .post:
1414
return "POST"
15+
case .put:
16+
return "PUT"
1517
case .delete:
1618
return "DELETE"
1719
case .patch:

Sources/NetworkRequester/HTTP/HTTPStatus.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/// Represents an HTTP status of a request.
2-
public enum HTTPStatus: Int {
2+
public enum HTTPStatus: Int, Equatable {
33

44
// MARK: - 1xx Informational
55

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import protocol Foundation.LocalizedError
2+
import class Foundation.NSError
23

34
/// All possible error that could be thrown.
4-
public enum NetworkingError: LocalizedError {
5+
public enum NetworkingError: Error {
56
/// Thrown when constructing URL fails.
67
case buildingURL
78

@@ -12,8 +13,27 @@ public enum NetworkingError: LocalizedError {
1213
case decoding(error: DecodingError)
1314

1415
/// Thrown when the network request fails.
15-
case networking(HTTPStatus)
16+
case networking(status: HTTPStatus, error: Error?)
1617

17-
/// Thrown when an unknown error is thrown (no other error from the above has been catched).
18-
case unknown
18+
/// Thrown when an unknown error is thrown (no other error from the above has been catched),
19+
/// optionally forwarding an underlying error if there is one.
20+
case unknown(Error?)
21+
}
22+
23+
extension NetworkingError: Equatable {
24+
public static func == (lhs: NetworkingError, rhs: NetworkingError) -> Bool {
25+
switch (lhs, rhs) {
26+
case (.buildingURL, .buildingURL):
27+
return true
28+
case (.unknown(nil), .unknown(nil)):
29+
return true
30+
case (.unknown(.some(let lhs)), .unknown(.some(let rhs))):
31+
return (lhs as NSError) == (rhs as NSError)
32+
case (let .networking(lhsStatus, lhsError), let .networking(rhsStatus, rhsError)):
33+
guard lhsStatus == rhsStatus else { return false }
34+
return String(reflecting: lhsError) == String(reflecting: rhsError)
35+
default:
36+
return false
37+
}
38+
}
1939
}

Sources/NetworkRequester/URLQueryParameters.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,15 @@ public struct URLQueryParameters {
4242
self.items = { queryItems }
4343
}
4444
}
45+
46+
extension URLQueryParameters: CustomDebugStringConvertible {
47+
public var debugDescription: String {
48+
guard let queryParams = try? items() else {
49+
return ""
50+
}
51+
52+
return queryParams
53+
.map { "\($0.name): \($0.value ?? "")" }
54+
.joined(separator: "; ")
55+
}
56+
}

0 commit comments

Comments
 (0)