200 lines
7.9 KiB
Swift
200 lines
7.9 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?
|
|
|
|
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<T: Decodable>(_ decodableType: T.Type, keyPath: String? = nil, service: ServiceType, stubBehavior: StubBehavior = .never, completion: @escaping NetworkResultDecodableCompletionClosure<T>) -> 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<T>.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<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, 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<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 {
|
|
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)
|
|
}
|