Merge pull request #312 from TouchInstinct/feature/token_interceptor
feat: ApiInteractor, TokenInterceptor, FingerprintsTrustEvaluator and more
This commit is contained in:
commit
5e43ca7fe2
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -1,5 +1,15 @@
|
|||
# Changelog
|
||||
|
||||
### 1.21.0
|
||||
|
||||
- **Update**: `AsyncEventHandler` was replaced with `EndpointRequestRetrier`
|
||||
- **Add**: `FingerprintsTrustEvaluator` and `FingerprintsProvider` for fingerprint-based host trust evaluation
|
||||
- **Add**: `DefaultTokenInterceptor` for queue-based token refresh across all requests of single api interactor (network service).
|
||||
- **Update**: `DefaultRecoverableJsonNetworkService` now returns collection of errors in result
|
||||
- **Update**: `CancellableTask` was renamed to `Cancellable`. Cancellable implementations has been moved from `TIMoyaNetworking` to `TIFoundationUtils`.
|
||||
- **Add**: `ApiInteractor` protocol with basic request/response methods
|
||||
|
||||
|
||||
### 1.20.0
|
||||
|
||||
- **Add**: OpenAPI security schemes support for EndpointRequest's.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = "LeadKit"
|
||||
s.version = "1.20.0"
|
||||
s.version = "1.21.0"
|
||||
s.summary = "iOS framework with a bunch of tools for rapid development"
|
||||
s.homepage = "https://github.com/TouchInstinct/LeadKit"
|
||||
s.license = "Apache License, Version 2.0"
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ let package = Package(
|
|||
|
||||
// MARK: - Networking
|
||||
|
||||
.target(name: "TINetworking", dependencies: ["TISwiftUtils", "Alamofire"], path: "TINetworking/Sources"),
|
||||
.target(name: "TINetworking", dependencies: ["TIFoundationUtils", "Alamofire"], path: "TINetworking/Sources"),
|
||||
.target(name: "TIMoyaNetworking", dependencies: ["TINetworking", "TIFoundationUtils", "Moya"], path: "TIMoyaNetworking"),
|
||||
.target(name: "TINetworkingCache", dependencies: ["TIFoundationUtils", "TINetworking", "Cache"], path: "TINetworkingCache/Sources"),
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIAppleMapUtils'
|
||||
s.version = '1.20.0'
|
||||
s.version = '1.21.0'
|
||||
s.summary = 'Set of helpers for map objects clustering and interacting using Apple MapKit.'
|
||||
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIAuth'
|
||||
s.version = '1.20.0'
|
||||
s.version = '1.21.0'
|
||||
s.summary = 'Login, registration, confirmation and other related actions'
|
||||
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -22,10 +22,10 @@
|
|||
|
||||
public final class ClosureAsyncOperation<Output, Failure: Error>: AsyncOperation<Output, Failure> {
|
||||
public typealias AsyncTaskClosure = () async -> Result<Output, Failure>
|
||||
public typealias CancellableTaskClosure = (@escaping (Result<Output, Failure>) -> Void) -> CancellableTask
|
||||
public typealias CancellableTaskClosure = (@escaping (Result<Output, Failure>) -> Void) -> Cancellable
|
||||
|
||||
private let cancellableTaskClosure: CancellableTaskClosure
|
||||
private var cancellableTask: CancellableTask?
|
||||
private var cancellableTask: Cancellable?
|
||||
|
||||
public init(cancellableTaskClosure: @escaping CancellableTaskClosure) {
|
||||
self.cancellableTaskClosure = cancellableTaskClosure
|
||||
|
|
@ -66,7 +66,7 @@ public final class ClosureAsyncOperation<Output, Failure: Error>: AsyncOperation
|
|||
}
|
||||
|
||||
public extension ClosureAsyncOperation {
|
||||
typealias NeverFailCancellableTaskClosure = (@escaping (Output) -> Void) -> CancellableTask
|
||||
typealias NeverFailCancellableTaskClosure = (@escaping (Output) -> Void) -> Cancellable
|
||||
|
||||
convenience init(neverFailCancellableTaskClosure: @escaping NeverFailCancellableTaskClosure) where Failure == Never {
|
||||
self.init(cancellableTaskClosure: { completion in
|
||||
|
|
|
|||
|
|
@ -20,10 +20,7 @@
|
|||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Moya
|
||||
import TIFoundationUtils
|
||||
|
||||
open class BaseCancellable: Cancellable, CancellableTask {
|
||||
open class BaseCancellable: Cancellable {
|
||||
private(set) public var isCancelled = false
|
||||
|
||||
open func cancel() {
|
||||
|
|
@ -20,10 +20,8 @@
|
|||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Moya
|
||||
|
||||
open class CancellableBag: BaseCancellable {
|
||||
var cancellables: [Cancellable] = []
|
||||
open class BaseCancellableBag: BaseCancellable {
|
||||
public var cancellables: [Cancellable] = []
|
||||
|
||||
public override init() {}
|
||||
|
||||
|
|
@ -33,6 +31,7 @@ open class CancellableBag: BaseCancellable {
|
|||
}
|
||||
|
||||
cancellables.forEach { $0.cancel() }
|
||||
cancellables.removeAll()
|
||||
|
||||
super.cancel()
|
||||
}
|
||||
|
|
@ -47,7 +46,7 @@ open class CancellableBag: BaseCancellable {
|
|||
}
|
||||
|
||||
public extension Cancellable {
|
||||
func add(to cancellableBag: CancellableBag) {
|
||||
func add(to cancellableBag: BaseCancellableBag) {
|
||||
cancellableBag.add(cancellable: self)
|
||||
}
|
||||
}
|
||||
|
|
@ -20,11 +20,14 @@
|
|||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Moya
|
||||
import Foundation
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
extension _Concurrency.Task: Cancellable {}
|
||||
public protocol Cancellable {
|
||||
func cancel()
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
extension Task: Cancellable {}
|
||||
|
||||
extension DispatchWorkItem: Cancellable {}
|
||||
extension Operation: Cancellable {}
|
||||
extension DispatchWorkItem: Cancellable {}
|
||||
|
|
@ -20,10 +20,6 @@
|
|||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Moya
|
||||
import TISwiftUtils
|
||||
import TIFoundationUtils
|
||||
|
||||
public struct Cancellables {
|
||||
public static func nonCancellable() -> Cancellable {
|
||||
NonCancellable()
|
||||
|
|
@ -32,8 +28,4 @@ public struct Cancellables {
|
|||
public static func scoped(scopeCancellableClosure: ScopeCancellable.ScopeCancellableClosure) -> Cancellable {
|
||||
ScopeCancellable(scopeCancellableClosure: scopeCancellableClosure)
|
||||
}
|
||||
|
||||
public static func nonCancellableTask() -> CancellableTask {
|
||||
NonCancellable()
|
||||
}
|
||||
}
|
||||
|
|
@ -20,10 +20,7 @@
|
|||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Moya
|
||||
import TIFoundationUtils
|
||||
|
||||
public struct NonCancellable: Cancellable, CancellableTask {
|
||||
public struct NonCancellable: Cancellable {
|
||||
public let isCancelled = true
|
||||
|
||||
public init() {}
|
||||
|
|
@ -22,17 +22,20 @@
|
|||
|
||||
import TISwiftUtils
|
||||
|
||||
@available(iOS 13.0.0, *)
|
||||
public struct AnyAsyncEventHandler<EventType, ResultType>: AsyncEventHandler {
|
||||
private let processClosure: AsyncClosure<EventType, ResultType>
|
||||
public final class ScopeCancellable: Cancellable {
|
||||
public typealias ScopeCancellableClosure = Closure<BaseCancellableBag, Cancellable>
|
||||
|
||||
public init<Handler: AsyncEventHandler>(handler: Handler)
|
||||
where Handler.EventType == EventType, Handler.ResultType == ResultType {
|
||||
private let cancellableBag: BaseCancellableBag
|
||||
|
||||
self.processClosure = handler.handle
|
||||
public init(cancellableBag: BaseCancellableBag = .init(),
|
||||
scopeCancellableClosure: ScopeCancellableClosure) {
|
||||
|
||||
self.cancellableBag = cancellableBag
|
||||
|
||||
cancellableBag.add(cancellable: scopeCancellableClosure(cancellableBag))
|
||||
}
|
||||
|
||||
public func handle(_ event: EventType) async -> ResultType {
|
||||
await processClosure(event)
|
||||
public func cancel() {
|
||||
cancellableBag.cancel()
|
||||
}
|
||||
}
|
||||
|
|
@ -20,17 +20,18 @@
|
|||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
@available(iOS 13.0.0, *)
|
||||
public protocol AsyncEventHandler {
|
||||
associatedtype EventType
|
||||
associatedtype ResultType
|
||||
public struct WeakTargetCancellable<T: AnyObject>: Cancellable {
|
||||
public typealias CancelClosure = (T?) -> Void
|
||||
|
||||
func handle(_ event: EventType) async -> ResultType
|
||||
}
|
||||
private let cancelClosure: CancelClosure
|
||||
private weak var target: T?
|
||||
|
||||
@available(iOS 13.0.0, *)
|
||||
public extension AsyncEventHandler {
|
||||
func asAnyAsyncEventHandler() -> AnyAsyncEventHandler<EventType, ResultType> {
|
||||
.init(handler: self)
|
||||
public init(target: T?, cancelClosure: @escaping CancelClosure) {
|
||||
self.target = target
|
||||
self.cancelClosure = cancelClosure
|
||||
}
|
||||
|
||||
public func cancel() {
|
||||
cancelClosure(target)
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
@available(iOS 11.0, *)
|
||||
open class UnarchiverKeyValueDecoder: CodableKeyValueDecoder {
|
||||
public init() {}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
@available(iOS 11.0, *)
|
||||
open class ArchiverKeyValueEncoder: CodableKeyValueEncoder {
|
||||
public init() {}
|
||||
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ public extension KeyedDecodingContainer {
|
|||
|
||||
// ISO8601
|
||||
|
||||
@available(iOS 11.0, *)
|
||||
public extension KeyedDecodingContainer {
|
||||
func decodeDate(forKey key: Key,
|
||||
userInfo: [CodingUserInfoKey: Any],
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIFoundationUtils'
|
||||
s.version = '1.20.0'
|
||||
s.version = '1.21.0'
|
||||
s.summary = 'Set of helpers for Foundation framework classes.'
|
||||
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
|
||||
s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
|
||||
|
||||
s.ios.deployment_target = '11.0'
|
||||
s.ios.deployment_target = '10.0'
|
||||
s.swift_versions = ['5.3']
|
||||
|
||||
s.source_files = s.name + '/**/Sources/**/*'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIGoogleMapUtils'
|
||||
s.version = '1.20.0'
|
||||
s.version = '1.21.0'
|
||||
s.summary = 'Set of helpers for map objects clustering and interacting using Google Maps SDK.'
|
||||
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIKeychainUtils'
|
||||
s.version = '1.20.0'
|
||||
s.version = '1.21.0'
|
||||
s.summary = 'Set of helpers for Keychain classes.'
|
||||
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIMapUtils'
|
||||
s.version = '1.20.0'
|
||||
s.version = '1.21.0'
|
||||
s.summary = 'Set of helpers for map objects clustering and interacting.'
|
||||
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ import TISwiftUtils
|
|||
import Foundation
|
||||
import TIFoundationUtils
|
||||
|
||||
open class DefaultJsonNetworkService {
|
||||
public typealias RequestResult<S: Decodable, AE: Decodable> = EndpointRequestResult<S, AE, MoyaError>
|
||||
open class DefaultJsonNetworkService: ApiInteractor {
|
||||
public typealias NetworkError = MoyaError
|
||||
|
||||
public var session: Session
|
||||
|
||||
|
|
@ -68,43 +68,11 @@ open class DefaultJsonNetworkService {
|
|||
defaultServer: openApi.defaultServer)
|
||||
}
|
||||
|
||||
@available(iOS 13.0.0, *)
|
||||
open func process<B: Encodable, S, F>(request: EndpointRequest<B, S>) async -> RequestResult<S, F> {
|
||||
await process(request: request,
|
||||
mapSuccess: Result.success,
|
||||
mapFailure: { .failure(.apiError($0)) },
|
||||
mapMoyaError: { .failure(.networkError($0)) })
|
||||
}
|
||||
|
||||
@available(iOS 13.0.0, *)
|
||||
open func process<B: Encodable, S: Decodable, F: Decodable, R>(request: EndpointRequest<B, S>,
|
||||
mapSuccess: @escaping Closure<S, R>,
|
||||
mapFailure: @escaping Closure<F, R>,
|
||||
mapMoyaError: @escaping Closure<MoyaError, R>) async -> R {
|
||||
|
||||
let cancellableBag = CancellableBag()
|
||||
|
||||
return await withTaskCancellationHandler(handler: {
|
||||
cancellableBag.cancel()
|
||||
}, operation: {
|
||||
await withCheckedContinuation { continuation in
|
||||
process(request: request,
|
||||
mapSuccess: mapSuccess,
|
||||
mapFailure: mapFailure,
|
||||
mapMoyaError: mapMoyaError) {
|
||||
|
||||
continuation.resume(returning: $0)
|
||||
}
|
||||
.add(to: cancellableBag)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
open func process<B: Encodable, S: Decodable, F: Decodable, R>(request: EndpointRequest<B, S>,
|
||||
mapSuccess: @escaping Closure<S, R>,
|
||||
mapFailure: @escaping Closure<F, R>,
|
||||
mapMoyaError: @escaping Closure<MoyaError, R>,
|
||||
completion: @escaping ParameterClosure<R>) -> Cancellable {
|
||||
mapNetworkError: @escaping Closure<MoyaError, R>,
|
||||
completion: @escaping ParameterClosure<R>) -> TIFoundationUtils.Cancellable {
|
||||
|
||||
ScopeCancellable { [serializationQueue, callbackQueue, preprocessors] scope in
|
||||
let workItem = DispatchWorkItem {
|
||||
|
|
@ -122,11 +90,11 @@ open class DefaultJsonNetworkService {
|
|||
scope.add(cancellable: self.process(request: serializedRequest,
|
||||
mapSuccess: mapSuccess,
|
||||
mapFailure: mapFailure,
|
||||
mapMoyaError: mapMoyaError,
|
||||
mapNetworkError: mapNetworkError,
|
||||
completion: completion))
|
||||
} catch {
|
||||
callbackQueue.async {
|
||||
completion(mapMoyaError(.encodableMapping(error)))
|
||||
completion(mapNetworkError(.encodableMapping(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -140,8 +108,8 @@ open class DefaultJsonNetworkService {
|
|||
open func process<S: Decodable, F: Decodable, R>(request: SerializedRequest,
|
||||
mapSuccess: @escaping Closure<S, R>,
|
||||
mapFailure: @escaping Closure<F, R>,
|
||||
mapMoyaError: @escaping Closure<MoyaError, R>,
|
||||
completion: @escaping ParameterClosure<R>) -> Cancellable {
|
||||
mapNetworkError: @escaping Closure<MoyaError, R>,
|
||||
completion: @escaping ParameterClosure<R>) -> TIFoundationUtils.Cancellable {
|
||||
|
||||
createProvider().request(request) { [jsonDecoder,
|
||||
callbackQueue,
|
||||
|
|
@ -185,7 +153,7 @@ open class DefaultJsonNetworkService {
|
|||
result = model
|
||||
pluginResult = .success(rawResponse)
|
||||
case let .failure(moyaError):
|
||||
result = mapMoyaError(moyaError)
|
||||
result = mapNetworkError(moyaError)
|
||||
pluginResult = .failure(moyaError)
|
||||
}
|
||||
|
||||
|
|
@ -193,13 +161,14 @@ open class DefaultJsonNetworkService {
|
|||
$0.didReceive(pluginResult, target: request)
|
||||
}
|
||||
case let .failure(moyaError):
|
||||
result = mapMoyaError(moyaError)
|
||||
result = mapNetworkError(moyaError)
|
||||
}
|
||||
|
||||
callbackQueue.async {
|
||||
completion(result)
|
||||
}
|
||||
}
|
||||
.asFoundationUtilsCancellable()
|
||||
}
|
||||
|
||||
public func register<T: RawRepresentable>(securityPreprocessors: [T: SecuritySchemePreprocessor]) where T.RawValue == String {
|
||||
|
|
|
|||
|
|
@ -20,14 +20,10 @@
|
|||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TINetworking
|
||||
import Moya
|
||||
import Foundation
|
||||
|
||||
public enum EndpointErrorResult<ApiError, NetworkError>: Error {
|
||||
case apiError(ApiError)
|
||||
case networkError(NetworkError)
|
||||
}
|
||||
|
||||
public extension EndpointErrorResult where NetworkError == MoyaError {
|
||||
var isNetworkConnectionProblem: Bool {
|
||||
guard case let .networkError(moyaError) = self,
|
||||
|
|
@ -21,14 +21,18 @@
|
|||
//
|
||||
|
||||
import Moya
|
||||
import TISwiftUtils
|
||||
import TIFoundationUtils
|
||||
|
||||
public final class ScopeCancellable: CancellableBag {
|
||||
public typealias ScopeCancellableClosure = Closure<ScopeCancellable, Cancellable>
|
||||
struct MoyaCancellableWrapper: TIFoundationUtils.Cancellable {
|
||||
let moyaCancellable: Moya.Cancellable
|
||||
|
||||
public init(scopeCancellableClosure: ScopeCancellableClosure) {
|
||||
super.init()
|
||||
|
||||
cancellables = [scopeCancellableClosure(self)]
|
||||
func cancel() {
|
||||
moyaCancellable.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
extension Moya.Cancellable {
|
||||
func asFoundationUtilsCancellable() -> TIFoundationUtils.Cancellable {
|
||||
MoyaCancellableWrapper(moyaCancellable: self)
|
||||
}
|
||||
}
|
||||
|
|
@ -26,55 +26,60 @@ import TISwiftUtils
|
|||
|
||||
@available(iOS 13.0.0, *)
|
||||
open class DefaultRecoverableJsonNetworkService<ApiError: Decodable & Error>: DefaultJsonNetworkService {
|
||||
public typealias EndpointResponse<S: Decodable> = EndpointRecoverableRequestResult<S, ApiError, MoyaError>
|
||||
public typealias ErrorType = EndpointErrorResult<ApiError, MoyaError>
|
||||
public typealias ErrorHandlerResultType = RecoverableErrorHandlerResult<ErrorType>
|
||||
public typealias ErrorHandlerType = AnyAsyncEventHandler<ErrorType, ErrorHandlerResultType>
|
||||
public typealias RequestRetrier = AnyEndpointRequestRetrier<ErrorType>
|
||||
|
||||
public private(set) var defaultErrorHandlers: [ErrorHandlerType] = []
|
||||
public private(set) var defaultRequestRetriers: [RequestRetrier] = []
|
||||
|
||||
open func process<B: Encodable, S>(recoverableRequest: EndpointRequest<B, S>,
|
||||
prependErrorHandlers: [ErrorHandlerType] = [],
|
||||
appendErrorHandlers: [ErrorHandlerType] = []) async ->
|
||||
RequestResult<S, ApiError> {
|
||||
prependRequestRetriers: [RequestRetrier] = [],
|
||||
appendRequestRetriers: [RequestRetrier] = []) async ->
|
||||
EndpointResponse<S> {
|
||||
|
||||
await process(recoverableRequest: recoverableRequest,
|
||||
errorHandlers: prependErrorHandlers + defaultErrorHandlers + appendErrorHandlers)
|
||||
errorHandlers: prependRequestRetriers + defaultRequestRetriers + appendRequestRetriers)
|
||||
}
|
||||
|
||||
open func process<B: Encodable, S>(recoverableRequest: EndpointRequest<B, S>,
|
||||
errorHandlers: [ErrorHandlerType]) async -> RequestResult<S, ApiError> {
|
||||
errorHandlers: [RequestRetrier]) async -> EndpointResponse<S> {
|
||||
|
||||
let result: RequestResult<S, ApiError> = await process(request: recoverableRequest)
|
||||
|
||||
if case let .failure(errorResponse) = result {
|
||||
var failures = [errorResponse]
|
||||
|
||||
for handler in errorHandlers {
|
||||
let handlerResult = await handler.handle(errorResponse)
|
||||
let handlerResult = await handler.validateAndRepair(errorResults: failures)
|
||||
|
||||
switch handlerResult {
|
||||
case let .forwardError(error):
|
||||
return .failure(error)
|
||||
|
||||
case .recoverRequest:
|
||||
return await process(recoverableRequest: recoverableRequest, errorHandlers: errorHandlers)
|
||||
|
||||
case .skipError:
|
||||
break
|
||||
case let .success(retryResult):
|
||||
switch retryResult {
|
||||
case .retry, .retryWithDelay:
|
||||
return await process(recoverableRequest: recoverableRequest, errorHandlers: errorHandlers)
|
||||
case .doNotRetry, .doNotRetryWithError:
|
||||
break
|
||||
}
|
||||
case let .failure(error):
|
||||
failures.append(error)
|
||||
}
|
||||
}
|
||||
|
||||
return .failure(.init(failures: failures))
|
||||
}
|
||||
|
||||
return result
|
||||
return result.mapError { .init(failures: [$0]) }
|
||||
}
|
||||
|
||||
public func register<ErrorHandler: AsyncErrorHandler>(defaultErrorHandler: ErrorHandler)
|
||||
where ErrorHandler.EventType == ErrorHandlerType.EventType, ErrorHandler.ResultType == ErrorHandlerType.ResultType {
|
||||
public func register<RequestRetrier: EndpointRequestRetrier>(defaultRequestRetrier: RequestRetrier)
|
||||
where RequestRetrier.ErrorResult == ErrorType {
|
||||
|
||||
defaultErrorHandlers.append(defaultErrorHandler.asAnyAsyncEventHandler())
|
||||
defaultRequestRetriers.append(defaultRequestRetrier.asAnyEndpointRequestRetrier())
|
||||
}
|
||||
|
||||
public func set<ErrorHandler: AsyncErrorHandler>(defaultErrorHandlers: ErrorHandler...)
|
||||
where ErrorHandler.EventType == ErrorHandlerType.EventType, ErrorHandler.ResultType == ErrorHandlerType.ResultType {
|
||||
public func set<RequestRetrier: EndpointRequestRetrier>(defaultRequestRetriers: RequestRetrier...)
|
||||
where RequestRetrier.ErrorResult == ErrorType {
|
||||
|
||||
self.defaultErrorHandlers = defaultErrorHandlers.map { $0.asAnyAsyncEventHandler() }
|
||||
self.defaultRequestRetriers = defaultRequestRetriers.map { $0.asAnyEndpointRequestRetrier() }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIMoyaNetworking'
|
||||
s.version = '1.20.0'
|
||||
s.version = '1.21.0'
|
||||
s.summary = 'Moya + Swagger network service.'
|
||||
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
//
|
||||
// Copyright (c) 2022 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
open class DefaultFingerprintsProvider: FingerprintsProvider {
|
||||
public var secureStorage: FingerprintsSecureStorage
|
||||
public var settingsStorage: FingerprintsSettingsStorage
|
||||
|
||||
public init(secureStorage: FingerprintsSecureStorage,
|
||||
settingsStorage: FingerprintsSettingsStorage,
|
||||
bundledFingerprints: [String: Set<String>]) {
|
||||
|
||||
self.secureStorage = secureStorage
|
||||
self.settingsStorage = settingsStorage
|
||||
|
||||
if settingsStorage.shouldResetFingerprints {
|
||||
self.secureStorage.knownPins = bundledFingerprints
|
||||
self.settingsStorage.shouldResetFingerprints = false
|
||||
}
|
||||
}
|
||||
|
||||
public func fingerprints(forHost host: String) -> Set<String> {
|
||||
secureStorage.knownPins[host] ?? []
|
||||
}
|
||||
|
||||
public func add(fingerprints: [String], forHost host: String) {
|
||||
let pinsForHost = (secureStorage.knownPins[host] ?? []).union(fingerprints)
|
||||
secureStorage.knownPins.updateValue(pinsForHost, forKey: host)
|
||||
}
|
||||
}
|
||||
|
|
@ -20,14 +20,7 @@
|
|||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol CancellableTask {
|
||||
func cancel()
|
||||
public protocol FingerprintsProvider {
|
||||
func fingerprints(forHost host: String) -> Set<String>
|
||||
func add(fingerprints: [String], forHost host: String)
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
extension Task: CancellableTask {}
|
||||
|
||||
extension Operation: CancellableTask {}
|
||||
extension DispatchWorkItem: CancellableTask {}
|
||||
|
|
@ -20,8 +20,6 @@
|
|||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
public enum RecoverableErrorHandlerResult<ErrorType: Error> {
|
||||
case skipError
|
||||
case recoverRequest
|
||||
case forwardError(ErrorType)
|
||||
public protocol FingerprintsSecureStorage {
|
||||
var knownPins: [String: Set<String>] { get set }
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// Copyright (c) 2022 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
public protocol FingerprintsSettingsStorage {
|
||||
/// Should be true by default (on app first run)
|
||||
var shouldResetFingerprints: Bool { get set }
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
//
|
||||
// Copyright (c) 2022 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Alamofire
|
||||
import Security
|
||||
import Foundation
|
||||
import CommonCrypto
|
||||
|
||||
open class FingerprintsTrustEvaluator: ServerTrustEvaluating {
|
||||
public enum PinValidationFailed: Error {
|
||||
case unableToExtractPin(trust: SecTrust)
|
||||
case serverPinMissing(knownFingerprints: Set<String>, serverFingerprint: String)
|
||||
}
|
||||
|
||||
private let fingerprintsProvider: FingerprintsProvider
|
||||
|
||||
public init(fingerprintsProvider: FingerprintsProvider) {
|
||||
self.fingerprintsProvider = fingerprintsProvider
|
||||
}
|
||||
|
||||
open func evaluate(_ trust: SecTrust, forHost host: String) throws {
|
||||
guard SecTrustGetCertificateCount(trust) > 0,
|
||||
let certificate = SecTrustGetCertificateAtIndex(trust, 0) else {
|
||||
|
||||
throw PinValidationFailed.unableToExtractPin(trust: trust)
|
||||
}
|
||||
|
||||
let certificateData = SecCertificateCopyData(certificate) as Data
|
||||
|
||||
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
||||
certificateData.withUnsafeBytes {
|
||||
_ = CC_SHA256($0.baseAddress, CC_LONG(certificateData.count), &hash)
|
||||
}
|
||||
|
||||
let certificateHash = Data(hash)
|
||||
|
||||
let certificatePin = certificateHash
|
||||
.map { String(format: "%02X", $0) }
|
||||
.joined(separator: ":")
|
||||
|
||||
let knownFingerprintsForHost = fingerprintsProvider.fingerprints(forHost: host)
|
||||
|
||||
guard knownFingerprintsForHost.contains(certificatePin) else {
|
||||
throw PinValidationFailed.serverPinMissing(knownFingerprints: knownFingerprintsForHost,
|
||||
serverFingerprint: certificatePin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
//
|
||||
// Copyright (c) 2022 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TISwiftUtils
|
||||
import TIFoundationUtils
|
||||
|
||||
public protocol ApiInteractor {
|
||||
associatedtype NetworkError
|
||||
|
||||
typealias RequestResult<S: Decodable, AE: Decodable> = EndpointRequestResult<S, AE, NetworkError>
|
||||
|
||||
func process<B: Encodable, S: Decodable, AE: Decodable, R>(request: EndpointRequest<B, S>,
|
||||
mapSuccess: @escaping Closure<S, R>,
|
||||
mapFailure: @escaping Closure<AE, R>,
|
||||
mapNetworkError: @escaping Closure<NetworkError, R>,
|
||||
completion: @escaping ParameterClosure<R>) -> Cancellable
|
||||
}
|
||||
|
||||
@available(iOS 13.0.0, *)
|
||||
public extension ApiInteractor {
|
||||
func process<B: Encodable, S, F>(request: EndpointRequest<B, S>) async -> RequestResult<S, F> {
|
||||
await process(request: request,
|
||||
mapSuccess: Result.success,
|
||||
mapFailure: { .failure(.apiError($0)) },
|
||||
mapNetworkError: { .failure(.networkError($0)) })
|
||||
}
|
||||
|
||||
func process<B: Encodable, S: Decodable, F: Decodable, R>(request: EndpointRequest<B, S>,
|
||||
mapSuccess: @escaping Closure<S, R>,
|
||||
mapFailure: @escaping Closure<F, R>,
|
||||
mapNetworkError: @escaping Closure<NetworkError, R>) async -> R {
|
||||
|
||||
let cancellableBag = BaseCancellableBag()
|
||||
|
||||
return await withTaskCancellationHandler(handler: {
|
||||
cancellableBag.cancel()
|
||||
}, operation: {
|
||||
await withCheckedContinuation { continuation in
|
||||
process(request: request,
|
||||
mapSuccess: mapSuccess,
|
||||
mapFailure: mapFailure,
|
||||
mapNetworkError: mapNetworkError) {
|
||||
|
||||
continuation.resume(returning: $0)
|
||||
}
|
||||
.add(to: cancellableBag)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -20,8 +20,7 @@
|
|||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
@available(iOS 13.0.0, *)
|
||||
public protocol AsyncErrorHandler: AsyncEventHandler where EventType: Error {}
|
||||
|
||||
@available(iOS 13.0.0, *)
|
||||
extension AnyAsyncEventHandler: AsyncErrorHandler where EventType: Error {}
|
||||
public enum EndpointErrorResult<ApiError, NetworkError>: Error {
|
||||
case apiError(ApiError)
|
||||
case networkError(NetworkError)
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// Copyright (c) 2022 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
public typealias EndpointRecoverableRequestResult<S: Decodable, AE: Decodable, NE> = Result<S, ErrorCollection<EndpointErrorResult<AE, NE>>>
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
//
|
||||
// Copyright (c) 2022 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
public struct ErrorCollection<E>: Error {
|
||||
public let failures: [E]
|
||||
|
||||
public init(failures: [E]) {
|
||||
self.failures = failures
|
||||
}
|
||||
|
||||
public func map<R>(transform: (E) -> R) -> ErrorCollection<R> {
|
||||
.init(failures: failures.map(transform))
|
||||
}
|
||||
|
||||
public func firstOr(_ default: E) -> E {
|
||||
failures.first ?? `default`
|
||||
}
|
||||
|
||||
public func lastOr(_ default: E) -> E {
|
||||
failures.last ?? `default`
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
//
|
||||
// Copyright (c) 2022 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Alamofire
|
||||
import TIFoundationUtils
|
||||
|
||||
public struct AnyEndpointRequestRetrier<ErrorResult: Error>: EndpointRequestRetrier {
|
||||
typealias ValidateAndRepairClosure = ([ErrorResult], @escaping (Result<RetryResult, ErrorResult>) -> Void) -> Cancellable
|
||||
|
||||
private let validateAndRepairClosure: ValidateAndRepairClosure
|
||||
|
||||
public init<RR: EndpointRequestRetrier>(retrier: RR) where RR.ErrorResult == ErrorResult {
|
||||
self.validateAndRepairClosure = retrier.validateAndRepair
|
||||
}
|
||||
|
||||
public func validateAndRepair(errorResults: [ErrorResult], completion: @escaping (EndpointRetryResult) -> Void) -> Cancellable {
|
||||
validateAndRepairClosure(errorResults, completion)
|
||||
}
|
||||
}
|
||||
|
||||
public extension EndpointRequestRetrier {
|
||||
func asAnyEndpointRequestRetrier() -> AnyEndpointRequestRetrier<ErrorResult> {
|
||||
.init(retrier: self)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
//
|
||||
// Copyright (c) 2022 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Alamofire
|
||||
import Foundation
|
||||
import TIFoundationUtils
|
||||
|
||||
open class DefaultTokenInterceptor<RefreshError: Error>: RequestInterceptor {
|
||||
public typealias IsTokenInvalidClosure = (HTTPURLResponse?, Error?) -> Bool
|
||||
public typealias RefreshTokenClosure = (@escaping (RefreshError?) -> Void) -> Cancellable
|
||||
|
||||
public typealias RequestModificationClosure = (URLRequest) -> URLRequest
|
||||
|
||||
let refreshLock = NSLock()
|
||||
|
||||
let isTokenInvalidClosure: IsTokenInvalidClosure
|
||||
let refreshTokenClosure: RefreshTokenClosure
|
||||
|
||||
public var defaultRetryStrategy: RetryResult = .doNotRetry
|
||||
public var requestModificationClosure: RequestModificationClosure?
|
||||
|
||||
public init(isTokenInvalidClosure: @escaping IsTokenInvalidClosure,
|
||||
refreshTokenClosure: @escaping RefreshTokenClosure,
|
||||
requestModificationClosure: RequestModificationClosure? = nil) {
|
||||
|
||||
self.isTokenInvalidClosure = isTokenInvalidClosure
|
||||
self.refreshTokenClosure = refreshTokenClosure
|
||||
self.requestModificationClosure = requestModificationClosure
|
||||
}
|
||||
|
||||
// MARK: - RequestAdapter
|
||||
|
||||
open func adapt(_ urlRequest: URLRequest,
|
||||
for session: Session,
|
||||
completion: @escaping (Result<URLRequest, Error>) -> Void) {
|
||||
|
||||
let adaptBag = BaseCancellableBag()
|
||||
|
||||
let adaptCompletion: (Result<URLRequest, RefreshError>) -> Void = {
|
||||
adaptBag.cancellables.removeAll()
|
||||
completion($0.mapError { $0 as Error })
|
||||
}
|
||||
|
||||
let modifiedRequest = requestModificationClosure?(urlRequest) ?? urlRequest
|
||||
|
||||
validateAndRepair(validationClosure: { true },
|
||||
completion: adaptCompletion,
|
||||
defaultCompletionResult: modifiedRequest,
|
||||
recoveredCompletionResult: modifiedRequest)
|
||||
.add(to: adaptBag)
|
||||
}
|
||||
|
||||
// MARK: - RequestRetrier
|
||||
|
||||
open func retry(_ request: Request,
|
||||
for session: Session,
|
||||
dueTo error: Error,
|
||||
completion: @escaping (RetryResult) -> Void) {
|
||||
|
||||
let retryBag = BaseCancellableBag()
|
||||
|
||||
let retryCompletion: (Result<RetryResult, RefreshError>) -> Void = {
|
||||
retryBag.cancellables.removeAll()
|
||||
switch $0 {
|
||||
case let .success(retryResult):
|
||||
completion(retryResult)
|
||||
case let .failure(refreshError):
|
||||
completion(.doNotRetryWithError(refreshError))
|
||||
}
|
||||
}
|
||||
|
||||
validateAndRepair(validationClosure: { isTokenInvalidClosure(request.response, error) },
|
||||
completion: retryCompletion,
|
||||
defaultCompletionResult: defaultRetryStrategy,
|
||||
recoveredCompletionResult: .retry)
|
||||
.add(to: retryBag)
|
||||
}
|
||||
|
||||
open func validateAndRepair<T>(validationClosure: () -> Bool,
|
||||
completion: @escaping (Result<T, RefreshError>) -> Void,
|
||||
defaultCompletionResult: T,
|
||||
recoveredCompletionResult: T) -> Cancellable {
|
||||
|
||||
refreshLock.lock()
|
||||
|
||||
if validationClosure() {
|
||||
return refreshTokenClosure { [refreshLock] in
|
||||
refreshLock.unlock()
|
||||
|
||||
if let error = $0 {
|
||||
completion(.failure(error))
|
||||
} else {
|
||||
completion(.success(recoveredCompletionResult))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
refreshLock.unlock()
|
||||
|
||||
completion(.success(defaultCompletionResult))
|
||||
|
||||
return Cancellables.nonCancellable()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
//
|
||||
// Copyright (c) 2022 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Alamofire
|
||||
import TIFoundationUtils
|
||||
|
||||
public protocol EndpointRequestRetrier {
|
||||
associatedtype ErrorResult: Error
|
||||
|
||||
typealias EndpointRetryResult = Result<RetryResult, ErrorResult>
|
||||
|
||||
func validateAndRepair(errorResults: [ErrorResult],
|
||||
completion: @escaping (EndpointRetryResult) -> Void) -> Cancellable
|
||||
}
|
||||
|
||||
@available(iOS 13.0.0, *)
|
||||
public extension EndpointRequestRetrier {
|
||||
func validateAndRepair(errorResults: [ErrorResult]) async -> EndpointRetryResult {
|
||||
let cancellableBag = BaseCancellableBag()
|
||||
|
||||
return await withTaskCancellationHandler(handler: {
|
||||
cancellableBag.cancel()
|
||||
}, operation: {
|
||||
await withCheckedContinuation { continuation in
|
||||
validateAndRepair(errorResults: errorResults) {
|
||||
continuation.resume(returning: $0)
|
||||
}
|
||||
.add(to: cancellableBag)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
//
|
||||
// Copyright (c) 2022 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Alamofire
|
||||
import TIFoundationUtils
|
||||
|
||||
open class EndpointResponseTokenInterceptor<AE, NE>: DefaultTokenInterceptor<EndpointErrorResult<AE, NE>>, EndpointRequestRetrier {
|
||||
public typealias IsTokenInvalidErrorResultClosure = (EndpointErrorResult<AE, NE>) -> Bool
|
||||
public typealias RepairResult = Result<RetryResult, EndpointErrorResult<AE, NE>>
|
||||
|
||||
private let isTokenInvalidErrorResultClosure: IsTokenInvalidErrorResultClosure
|
||||
|
||||
public init(isTokenInvalidClosure: @escaping IsTokenInvalidClosure,
|
||||
refreshTokenClosure: @escaping RefreshTokenClosure,
|
||||
isTokenInvalidErrorResultClosure: @escaping IsTokenInvalidErrorResultClosure,
|
||||
requestModificationClosure: RequestModificationClosure? = nil) {
|
||||
|
||||
self.isTokenInvalidErrorResultClosure = isTokenInvalidErrorResultClosure
|
||||
|
||||
super.init(isTokenInvalidClosure: isTokenInvalidClosure,
|
||||
refreshTokenClosure: refreshTokenClosure,
|
||||
requestModificationClosure: requestModificationClosure)
|
||||
}
|
||||
|
||||
// MARK: - EndpointRequestRetrier
|
||||
|
||||
public func validateAndRepair(errorResults: [EndpointErrorResult<AE, NE>],
|
||||
completion: @escaping (RepairResult) -> Void) -> Cancellable {
|
||||
|
||||
guard let firstErrorResult = errorResults.first else {
|
||||
completion(.success(.doNotRetry))
|
||||
|
||||
return Cancellables.nonCancellable()
|
||||
}
|
||||
|
||||
return validateAndRepair(validationClosure: { isTokenInvalidErrorResultClosure(firstErrorResult) },
|
||||
completion: completion,
|
||||
defaultCompletionResult: defaultRetryStrategy,
|
||||
recoveredCompletionResult: .retry)
|
||||
}
|
||||
}
|
||||
|
|
@ -20,8 +20,6 @@
|
|||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TINetworking
|
||||
|
||||
open class DefaultSecuritySchemePreprocessor: SecuritySchemePreprocessor {
|
||||
struct ValueNotProvidedError: Error {}
|
||||
|
||||
|
|
@ -20,8 +20,6 @@
|
|||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TINetworking
|
||||
|
||||
open class DefaultEndpointSecurityPreprocessor: EndpointRequestPreprocessor {
|
||||
enum PreprocessError: Error {
|
||||
case missingSecurityScheme(String, [String: SecurityScheme])
|
||||
|
|
@ -20,8 +20,6 @@
|
|||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TINetworking
|
||||
|
||||
public protocol EndpointRequestPreprocessor {
|
||||
func preprocess<B,S>(request: EndpointRequest<B,S>) throws -> EndpointRequest<B,S>
|
||||
}
|
||||
|
|
@ -20,8 +20,6 @@
|
|||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TINetworking
|
||||
|
||||
public protocol SecuritySchemePreprocessor {
|
||||
func preprocess<B,S>(request: EndpointRequest<B,S>, using security: SecurityScheme) throws -> EndpointRequest<B,S>
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TINetworking'
|
||||
s.version = '1.20.0'
|
||||
s.version = '1.21.0'
|
||||
s.summary = 'Swagger-frendly networking layer helpers.'
|
||||
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
@ -12,7 +12,6 @@ Pod::Spec.new do |s|
|
|||
|
||||
s.source_files = s.name + '/Sources/**/*'
|
||||
|
||||
s.dependency 'TISwiftUtils', s.version.to_s
|
||||
s.dependency 'TIFoundationUtils', s.version.to_s
|
||||
s.dependency 'Alamofire', "~> 5.4"
|
||||
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TINetworkingCache'
|
||||
s.version = '1.20.0'
|
||||
s.version = '1.21.0'
|
||||
s.summary = 'Caching results of EndpointRequests.'
|
||||
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIPagination'
|
||||
s.version = '1.20.0'
|
||||
s.version = '1.21.0'
|
||||
s.summary = 'Generic pagination component.'
|
||||
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TISwiftUICore'
|
||||
s.version = '1.20.0'
|
||||
s.version = '1.21.0'
|
||||
s.summary = 'Core UI elements: protocols, views and helpers..'
|
||||
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TISwiftUtils'
|
||||
s.version = '1.20.0'
|
||||
s.version = '1.21.0'
|
||||
s.summary = 'Bunch of useful helpers for Swift development.'
|
||||
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TITableKitUtils'
|
||||
s.version = '1.20.0'
|
||||
s.version = '1.21.0'
|
||||
s.summary = 'Set of helpers for TableKit classes.'
|
||||
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TITransitions'
|
||||
s.version = '1.20.0'
|
||||
s.version = '1.21.0'
|
||||
s.summary = 'Set of custom transitions to present controller. '
|
||||
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIUIElements'
|
||||
s.version = '1.20.0'
|
||||
s.version = '1.21.0'
|
||||
s.summary = 'Bunch of useful protocols and views.'
|
||||
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIUIKitCore'
|
||||
s.version = '1.20.0'
|
||||
s.version = '1.21.0'
|
||||
s.summary = 'Core UI elements: protocols, views and helpers.'
|
||||
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIYandexMapUtils'
|
||||
s.version = '1.20.0'
|
||||
s.version = '1.21.0'
|
||||
s.summary = 'Set of helpers for map objects clustering and interacting using Yandex Maps SDK.'
|
||||
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
|
|
|
|||
Loading…
Reference in New Issue