Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Abstract NetworkService functionality #5

Merged
merged 9 commits into from
Jan 28, 2018
10 changes: 10 additions & 0 deletions Sources/Hyperspace/HTTP/HTTP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,16 @@ public struct HTTP {
public var dataString: String? {
return data.flatMap { String(data: $0, encoding: .utf8) }
}

/// Initialize a new HTTP.Response with any given HTTP status code and Data.
///
/// - Parameters:
/// - code: The raw HTTP status code for this response.
/// - data: The raw Data associated with the HTTP response, if any data was provided.
public init(code: Int, data: Data?) {
self.code = code
self.data = data
}
}
}
// swiftlint:enable nesting
Expand Down
4 changes: 2 additions & 2 deletions Sources/Hyperspace/Service/Backend/BackendService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public class BackendService {
// MARK: - BackendService Conformance to BackendServiceProtocol

extension BackendService: BackendServiceProtocol {
public func execute<T: NetworkRequest, U>(request: T, completion: @escaping BackendServiceCompletion<U>) where T.ResponseType == U {
public func execute<T: NetworkRequest>(request: T, completion: @escaping BackendServiceCompletion<T.ResponseType>) {
networkService.execute(request: request.urlRequest) { [weak self] (result) in
switch result {
case .success(let result):
Expand All @@ -46,7 +46,7 @@ extension BackendService: BackendServiceProtocol {
networkService.cancelTask(for: request)
}

private func handleResponseData<T: NetworkRequest, U>(_ data: Data, for request: T, completion: @escaping BackendServiceCompletion<U>) where T.ResponseType == U {
private func handleResponseData<T: NetworkRequest>(_ data: Data, for request: T, completion: @escaping BackendServiceCompletion<T.ResponseType>) {
let transformResult = request.transformData(data)

DispatchQueue.main.async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ public protocol BackendServiceProtocol {
/// - Parameters:
/// - request: The NetworkRequest to be executed.
/// - completion: The completion block to invoke when execution has finished.
func execute<T: NetworkRequest, U>(request: T, completion: @escaping BackendServiceCompletion<U>) where T.ResponseType == U
func execute<T: NetworkRequest>(request: T, completion: @escaping BackendServiceCompletion<T.ResponseType>)

/// Executes the NetworkRequest, calling the provided typed error completion block when finished.
///
/// - Parameters:
/// - request: The NetworkRequest to be executed.
/// - completion: The completion block (with a specifcally typed error) to invoke when execution has finished.
func execute<T: NetworkRequest, U, E: BackendServiceErrorInitializable>(request: T, completion: @escaping TypedBackendServiceCompletion<U, E>) where T.ResponseType == U, T.ErrorType == E
func execute<T: NetworkRequest, E: BackendServiceErrorInitializable>(request: T, completion: @escaping TypedBackendServiceCompletion<T.ResponseType, E>) where T.ErrorType == E

/// Cancels the task for the given request (if it is currently running).
func cancelTask(for request: URLRequest)
Expand All @@ -57,8 +57,8 @@ public protocol BackendServiceProtocol {
// MARK: - Default Typed Implementation

extension BackendServiceProtocol {
public func execute<T: NetworkRequest, U, E: BackendServiceErrorInitializable>(request: T, completion: @escaping TypedBackendServiceCompletion<U, E>) where T.ResponseType == U, T.ErrorType == E {
execute(request: request) { (result: Result<U, BackendServiceError>) in
public func execute<T: NetworkRequest, E: BackendServiceErrorInitializable>(request: T, completion: @escaping TypedBackendServiceCompletion<T.ResponseType, E>) where T.ErrorType == E {
execute(request: request) { (result: Result<T.ResponseType, BackendServiceError>) in
completion(result.flatMapError { .failure(E($0)) })
}
}
Expand Down
93 changes: 63 additions & 30 deletions Sources/Hyperspace/Service/Network/NetworkService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
//

import Foundation
import Result

/// Adopts the NetworkServiceProtocol to perform HTTP communication via the execution of URLRequests.
public class NetworkService {
Expand All @@ -43,41 +44,17 @@ public class NetworkService {

extension NetworkService: NetworkServiceProtocol {
public func execute(request: URLRequest, completion: @escaping NetworkServiceCompletion) {
let task = session.dataTask(with: request, completionHandler: { [weak self] (data, response, error) in
let task = session.dataTask(with: request) { (data, response, error) in
guard let statusCode = (response as? HTTPURLResponse)?.statusCode else {
let responseError = self?.invalidHTTPResponseError(for: error) ?? .unknownError
let result = NetworkServiceFailure(error: responseError, response: nil)

completion(.failure(result))
let networkFailure = NetworkServiceHelper.networkServiceFailure(for: error)
completion(.failure(networkFailure))
return
}

let httpResponse = HTTP.Response(code: statusCode, data: data)

switch httpResponse.status {
case .unknown:
let result = NetworkServiceFailure(error: .unknownStatusCode, response: httpResponse)
completion(.failure(result))
case .success:
guard let data = data else {
let result = NetworkServiceFailure(error: .noData, response: httpResponse)
completion(.failure(result))
return
}

let result = NetworkServiceSuccess(data: data, response: httpResponse)
completion(.success(result))
case .redirection:
let result = NetworkServiceFailure(error: .redirection, response: httpResponse)
completion(.failure(result))
case .clientError(let clientError):
let result = NetworkServiceFailure(error: .clientError(clientError), response: httpResponse)
completion(.failure(result))
case .serverError(let serverError):
let result = NetworkServiceFailure(error: .serverError(serverError), response: httpResponse)
completion(.failure(result))
}
})
let networkResult = NetworkServiceHelper.networkServiceResult(for: httpResponse)
completion(networkResult)
}

tasks[request] = task
task.resume()
Expand All @@ -91,3 +68,59 @@ extension NetworkService: NetworkServiceProtocol {
tasks.forEach { cancelTask(for: $0.key) }
}
}

/// A small helper struct which deals with the conversion of Data, URLResponse and Error objects to NetworkService successes and failures.
public struct NetworkServiceHelper {

/// Used to convert a client error, such as those returned by a NetworkSessionDataTask, into a NetworkServiceResult.
///
/// - Parameter clientError: The error returned by the NetworkSessionDataTask.
/// - Returns: The outcome NetworkServiceResult dictated by the error.
public static func networkServiceFailure(for clientError: Error?) -> NetworkServiceFailure {
let responseError = invalidHTTPResponseError(for: clientError)
return NetworkServiceFailure(error: responseError, response: nil)
}

/// Used to convert a valid HTTP.Response object, such as those returned by a NetworkSessionDataTask, into a NetworkServiceResult.
///
/// - Parameter response: The HTTP.Response object returned by the NetworkSessionDataTask.
/// - Returns: The outcome NetworkServiceResult dictated by the HTTP.Response.
public static func networkServiceResult(for response: HTTP.Response) -> NetworkServiceResult {
switch response.status {
case .unknown:
return .failure(NetworkServiceFailure(error: .unknownStatusCode, response: response))
case .success:
guard let data = response.data else {
return .failure(NetworkServiceFailure(error: .noData, response: response))
}

return .success(NetworkServiceSuccess(data: data, response: response))
case .redirection:
return .failure(NetworkServiceFailure(error: .redirection, response: response))
case .clientError(let clientError):
return .failure(NetworkServiceFailure(error: .clientError(clientError), response: response))
case .serverError(let serverError):
return .failure(NetworkServiceFailure(error: .serverError(serverError), response: response))
}
}
}

private extension NetworkServiceHelper {

private static func invalidHTTPResponseError(for error: Error?) -> NetworkServiceError {
let networkError: NetworkServiceError = (error as NSError?).flatMap {
switch ($0.domain, $0.code) {
case (NSURLErrorDomain, NSURLErrorNotConnectedToInternet):
return .noInternetConnection
case (NSURLErrorDomain, NSURLErrorTimedOut):
return .timedOut
case (NSURLErrorDomain, NSURLErrorCancelled):
return .cancelled
default:
return .unknownError
}
} ?? .unknownError

return networkError
}
}
13 changes: 8 additions & 5 deletions Sources/Hyperspace/Service/Network/NetworkServiceProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,21 @@ public enum NetworkServiceError: Error {

/// Represents the successful result of executing a NetworkRequest using a NetworkService.
public struct NetworkServiceSuccess {
let data: Data
let response: HTTP.Response
public let data: Data
public let response: HTTP.Response
}

/// Represents the failed result of executing a NetworkRequest using a NetworkService.
public struct NetworkServiceFailure: Error {
let error: NetworkServiceError
let response: HTTP.Response?
public let error: NetworkServiceError
public let response: HTTP.Response?
}

/// Represents the possible resulting values of a NetworkRequest using a NetworkService.
public typealias NetworkServiceResult = Result<NetworkServiceSuccess, NetworkServiceFailure>

/// Upon completion of executing a NetworkRequest using a NetworkService, the NetworkServiceResult is returned.
public typealias NetworkServiceCompletion = (Result<NetworkServiceSuccess, NetworkServiceFailure>) -> Void
public typealias NetworkServiceCompletion = (NetworkServiceResult) -> Void

/// Represents something that can execute a URLRequest.
public protocol NetworkServiceProtocol {
Expand Down