// // 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? internal var environment: NetworkEnvironment? internal var mirrorProvider = MirrorProvider() private let plugins: [NetworkPluginType] private var requests = [CancellationToken: Cancellable]() public init(environment: NetworkEnvironment?, plugins: [NetworkPluginType] = []) { self.environment = environment self.plugins = plugins } convenience public init() { self.init(environment: nil) } public static func addPlugin(_ plugin: NetworkPluginType) { NetworkRouter.globalPlugins.append(plugin) } @discardableResult public func request(_ service: ServiceType, stubBehavior: StubBehavior = .never, completion: @escaping NetworkResultCompletionClosure) -> CancellationToken? { self.mirrorProvider.reset() 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? { self.mirrorProvider.reset() return self.sendRequest(service, stubBehavior: self.environment?.stub ?? 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 let decoded = try decoder.decode(ResponseResult.self, from: response.data) return (decoded.result, response) } else { let decoded = try decoder.decode(decodableType, from: response.data) return (decoded, 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() } } // swiftlint:disable function_body_length @discardableResult private func sendRequest(_ service: ServiceType, stubBehavior: StubBehavior, token: String? = nil, 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, token: token) { [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 { if let env = self.mirrorProvider.nextMirror() { self.environment = env self.sendRequest(service, stubBehavior: stubBehavior, token: token, completion: completion) return } else { self.mirrorProvider.blockMirrors = true 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: self.environment?.sampleData(for: service)) 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) }