Skip to content

Commit

Permalink
added async support
Browse files Browse the repository at this point in the history
  • Loading branch information
Ivaylo Gashev committed Oct 7, 2022
1 parent 2c7301e commit 6d89174
Show file tree
Hide file tree
Showing 14 changed files with 550 additions and 174 deletions.
6 changes: 3 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// swift-tools-version:5.3
// swift-tools-version:5.5

import PackageDescription

let package = Package(
name: "NetworkRequester",
platforms: [
.iOS(.v11),
.macOS(.v10_15),
.iOS(.v13),
.macOS(.v12),
.tvOS(.v13),
],
products: [
Expand Down
58 changes: 58 additions & 0 deletions Sources/NetworkRequester/Callers/AsyncCaller.swift
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
}
}
}
67 changes: 67 additions & 0 deletions Sources/NetworkRequester/Callers/CallerUtility.swift
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)
}
}
}
150 changes: 150 additions & 0 deletions Sources/NetworkRequester/Callers/CombineCaller.swift
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
}
})
}
}
12 changes: 12 additions & 0 deletions Sources/NetworkRequester/HTTP/HTTPBody.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,15 @@ public struct HTTPBody {
}
}
}

extension HTTPBody: Equatable {
public static func == (lhs: HTTPBody, rhs: HTTPBody) -> Bool {
do {
let lhsData = try lhs.data()
let rhsData = try rhs.data()
return lhsData == rhsData
} catch {
return false
}
}
}
4 changes: 3 additions & 1 deletion Sources/NetworkRequester/HTTP/HTTPMethod.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// Represents an HTTP method of a request.
public enum HTTPMethod {
case get, post, delete, patch
case get, post, put, delete, patch
}

// MARK: - CustomStringConvertible
Expand All @@ -12,6 +12,8 @@ extension HTTPMethod: CustomStringConvertible {
return "GET"
case .post:
return "POST"
case .put:
return "PUT"
case .delete:
return "DELETE"
case .patch:
Expand Down
2 changes: 1 addition & 1 deletion Sources/NetworkRequester/HTTP/HTTPStatus.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/// Represents an HTTP status of a request.
public enum HTTPStatus: Int {
public enum HTTPStatus: Int, Equatable {

// MARK: - 1xx Informational

Expand Down
28 changes: 24 additions & 4 deletions Sources/NetworkRequester/NetworkingError.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import protocol Foundation.LocalizedError
import class Foundation.NSError

/// All possible error that could be thrown.
public enum NetworkingError: LocalizedError {
public enum NetworkingError: Error {
/// Thrown when constructing URL fails.
case buildingURL

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

/// Thrown when the network request fails.
case networking(HTTPStatus)
case networking(status: HTTPStatus, error: Error?)

/// Thrown when an unknown error is thrown (no other error from the above has been catched).
case unknown
/// Thrown when an unknown error is thrown (no other error from the above has been catched),
/// optionally forwarding an underlying error if there is one.
case unknown(Error?)
}

extension NetworkingError: Equatable {
public static func == (lhs: NetworkingError, rhs: NetworkingError) -> Bool {
switch (lhs, rhs) {
case (.buildingURL, .buildingURL):
return true
case (.unknown(nil), .unknown(nil)):
return true
case (.unknown(.some(let lhs)), .unknown(.some(let rhs))):
return (lhs as NSError) == (rhs as NSError)
case (let .networking(lhsStatus, lhsError), let .networking(rhsStatus, rhsError)):
guard lhsStatus == rhsStatus else { return false }
return String(reflecting: lhsError) == String(reflecting: rhsError)
default:
return false
}
}
}
12 changes: 12 additions & 0 deletions Sources/NetworkRequester/URLQueryParameters.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,15 @@ public struct URLQueryParameters {
self.items = { queryItems }
}
}

extension URLQueryParameters: CustomDebugStringConvertible {
public var debugDescription: String {
guard let queryParams = try? items() else {
return ""
}

return queryParams
.map { "\($0.name): \($0.value ?? "")" }
.joined(separator: "; ")
}
}
Loading

0 comments on commit 6d89174

Please sign in to comment.