Files

214 lines
8.1 KiB
Swift

//
// NetworkRouter.swift
// Cyberlock
//
// Created by Jura on 8/21/19.
// Copyright © 2019 Omicronmedia. All rights reserved.
//
import Foundation
public typealias NetworkResultCompletionClosure = (_ result: Result<Response, Error>) -> Void
public typealias NetworkResultDecodableCompletionClosure<T: Decodable> = (_ 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<T: Decodable>(_ decodableType: T.Type, keyPath: String? = nil, service: ServiceType, stubBehavior: StubBehavior = .never, completion: @escaping NetworkResultDecodableCompletionClosure<T>) -> 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<T>.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<URLRequest, NetworkRouterError> = 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<URLRequest, Error> = 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<Response, Error> = 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)
}