// // NetworkRouter.swift // Cyberlock // // Created by Jura on 8/21/19. // Copyright © 2019 Omicronmedia. All rights reserved. // import Foundation public typealias NetworkResultCompletionClosure = (_ result: Result) -> Void public typealias NetworkResultDecodableCompletionClosure = (_ result: Result<(T, Response), Error>) -> Void public enum StubBehavior { case never case immediate case delayed(seconds: TimeInterval) } let responseContext = CodingUserInfoKey(rawValue: "response")! final public class NetworkRouter { private static var globalPlugins: [NetworkPluginType] = [] public var customHeaders: HTTPHeaders? private(set) var environment: NetworkEnvironmentSwitchable private let plugins: [NetworkPluginType] private var requests = [CancellationToken: Cancellable]() // MARK: - Init public init(environment: NetworkEnvironmentSwitchable, plugins: [NetworkPluginType] = []) { self.environment = environment self.plugins = plugins } // MARK: - Plugins public static func addPlugin(_ plugin: NetworkPluginType) { NetworkRouter.globalPlugins.append(plugin) } // MARK: - Interface @discardableResult public func request(_ service: ServiceType, stubBehavior: StubBehavior = .never, completion: @escaping NetworkResultCompletionClosure) -> CancellationToken? { let mainThreadCompletion = { result in DispatchQueue.main.async { completion(result) } } return self.sendRequest(service, stubBehavior: stubBehavior, completion: mainThreadCompletion) } @discardableResult public func requestDecodable(_ decodableType: T.Type, keyPath: String? = nil, service: ServiceType, stubBehavior: StubBehavior = .never, completion: @escaping NetworkResultDecodableCompletionClosure) -> CancellationToken? { return self.sendRequest(service, stubBehavior: stubBehavior) { result in result.flatMap { response -> (T, Response) in let decoder = JSONDecoder() decoder.userInfo[responseContext] = response if let keyPath = keyPath, keyPath.count > 0 { decoder.userInfo[decodingContext] = keyPath do { let decoded = try decoder.decode(ResponseResult.self, from: response.data) return (decoded.result, response) } catch { throw NetworkRouterError.decode(native: error, response: response) } } else { do { let decoded = try decoder.decode(decodableType, from: response.data) return (decoded, response) } catch { throw NetworkRouterError.decode(native: error, response: response) } } } >>> { result in DispatchQueue.main.async { completion(result) } } } } public func cancel(_ token: CancellationToken) { guard let cancellable = self.requests[token] else { return } synced(self) { self.requests.removeValue(forKey: token) } cancellable.cancel() } public func cancelAllRequests() { let cancellables = self.requests.values synced(self) { self.requests.removeAll() } cancellables.forEach { $0.cancel() } } public func hasNextEnvironment() -> Bool { self.environment.hasNext } public func switchEnvironment() -> Bool { guard self.hasNextEnvironment() , let environment = self.environment.nextRecord() else { self.environment = self.environment.reset() return false } self.environment = environment return true } // MARK: - Private // swiftlint:disable function_body_length private func sendRequest(_ service: ServiceType, stubBehavior: StubBehavior, completion: @escaping NetworkResultCompletionClosure) -> CancellationToken? { let request: URLRequest do { request = try self.buildRequest(service, env: self.environment) } catch { completion(Result.failure(NetworkRouterError.invalidRequest)) return nil } let result: Result = NetworkRouter.networkShouldProcess(priority: NetworkRouter.globalPlugins, secondary: self.plugins, service: service, data: request) if let error = result.maybeError { completion(Result.failure(error)) return nil } let modified: Result = NetworkRouter.networkModifyRequest(priority: NetworkRouter.globalPlugins, secondary: self.plugins, service: service, request: request) guard let netRequest = modified.optional else { if let error = modified.maybeError { completion(Result.failure(error)) } else { completion(Result.failure(NetworkRouterError.cancelled)) } return nil } let cancellable = NetworkTask(request: netRequest) { [weak self] token, result in guard let self = self else { completion(Result.failure(NetworkRouterError.cancelled)) return } synced(self) { self.requests.removeValue(forKey: token) } guard let modified: Result = NetworkRouter.networkModifyResponse(priority: self.plugins, secondary: NetworkRouter.globalPlugins, service: service, result: result) else { completion(Result.failure(NetworkRouterError.cancelled)) return } let processed = NetworkRouter.networkShouldProcess(priority: self.plugins, secondary: NetworkRouter.globalPlugins, service: service, data: modified) if let error = processed.maybeError { completion(Result.failure(error)) return } guard let netResult = processed.optional else { if let error = processed.maybeError { completion(Result.failure(error)) } else { completion(Result.failure(NetworkRouterError.cancelled)) } return } completion(netResult) } synced(self) { self.requests[cancellable.token] = cancellable } cancellable.start(with: stubBehavior, sampleResponse: service.sampleResponse) return cancellable.token } // swiftlint:enable function_body_length } // MARK: - Synchronization private func synced(_ lock: Any?, closure: () -> Void) { guard let lock = lock else { return } objc_sync_enter(lock) closure() objc_sync_exit(lock) }