From 8fc1ebab770b753e8ce1771e9e25a60ef3ba4a1e Mon Sep 17 00:00:00 2001 From: Ivan Smolin Date: Wed, 15 Jun 2022 10:48:26 +0300 Subject: [PATCH] feat: ApiInteractor, TokenInterceptor, FingerprintsTrustEvaluator and more --- CHANGELOG.md | 10 ++ LeadKit.podspec | 2 +- Package.swift | 2 +- TIAppleMapUtils/TIAppleMapUtils.podspec | 2 +- TIAuth/TIAuth.podspec | 2 +- .../Sources/ClosureAsyncOperation.swift | 6 +- .../Sources}/BaseCancellable.swift | 5 +- .../Sources/BaseCancellableBag.swift | 9 +- .../Cancellables/Sources/Cancellable.swift | 11 +- .../Cancellables/Sources}/Cancellables.swift | 8 -- .../Sources}/NonCancellable.swift | 5 +- .../Sources/ScopeCancellable.swift | 19 +-- .../Sources/WeakTargetCancellable.swift | 21 +-- .../Decoders/UnarchiverKeyValueDecoder.swift | 1 + .../Encoders/ArchiverKeyValueEncoder.swift | 1 + .../KeyedDecodingContainer+DateDecoding.swift | 1 + TIFoundationUtils/TIFoundationUtils.podspec | 4 +- TIGoogleMapUtils/TIGoogleMapUtils.podspec | 2 +- TIKeychainUtils/TIKeychainUtils.podspec | 2 +- TIMapUtils/TIMapUtils.podspec | 2 +- .../DefaultJsonNetworkService.swift | 53 ++------ ...tErrorResult+NetworkConnectionError.swift} | 6 +- ...llable+TIFoundationUtilsCancellable.swift} | 18 ++- ...DefaultRecoverableJsonNetworkService.swift | 53 ++++---- TIMoyaNetworking/TIMoyaNetworking.podspec | 2 +- .../DefaultFingerprintsProvider.swift | 48 +++++++ .../FingerprintsProvider.swift | 13 +- .../FingerprintsSecureStorage.swift | 6 +- .../FingerprintsSettingsStorage.swift | 26 ++++ .../FingerprintsTrustEvaluator.swift | 68 ++++++++++ .../{ => Alamofire}/SessionFactory.swift | 0 .../Sources/ApiInteractor/ApiInteractor.swift | 69 ++++++++++ .../ApiInteractor/EndpointErrorResult.swift | 9 +- .../EndpointRecoverableRequestResult.swift | 23 ++++ .../EndpointRequestResult.swift | 0 .../ApiInteractor/ErrorCollection.swift | 41 ++++++ .../AnyEndpointRequestRetrier.swift | 44 +++++++ .../DefaultTokenInterceptor.swift | 123 ++++++++++++++++++ .../Interceptors/EndpointRequestRetrier.swift | 51 ++++++++ .../EndpointResponseTokenInterceptor.swift | 60 +++++++++ ...tEndpointSecurityRequestPreprocessor.swift | 2 - ...ltSecuritySchemesRequestPreprocessor.swift | 2 - .../EndpointRequestPreprocessor.swift | 2 - .../EndpointSecurityRequestPreprocessor.swift | 2 - TINetworking/TINetworking.podspec | 5 +- TINetworkingCache/TINetworkingCache.podspec | 2 +- TIPagination/TIPagination.podspec | 2 +- TISwiftUICore/TISwiftUICore.podspec | 2 +- TISwiftUtils/TISwiftUtils.podspec | 2 +- TITableKitUtils/TITableKitUtils.podspec | 2 +- TITransitions/TITransitions.podspec | 2 +- TIUIElements/TIUIElements.podspec | 2 +- TIUIKitCore/TIUIKitCore.podspec | 2 +- TIYandexMapUtils/TIYandexMapUtils.podspec | 2 +- 54 files changed, 686 insertions(+), 173 deletions(-) rename {TIMoyaNetworking/Sources/NetworkService/Cancellables => TIFoundationUtils/Cancellables/Sources}/BaseCancellable.swift (92%) rename TIMoyaNetworking/Sources/NetworkService/Cancellables/CancellableBag.swift => TIFoundationUtils/Cancellables/Sources/BaseCancellableBag.swift (89%) rename TIMoyaNetworking/Sources/NetworkService/Cancellables/Cancellable+Conformances.swift => TIFoundationUtils/Cancellables/Sources/Cancellable.swift (93%) rename {TIMoyaNetworking/Sources/NetworkService/Cancellables => TIFoundationUtils/Cancellables/Sources}/Cancellables.swift (90%) rename {TIMoyaNetworking/Sources/NetworkService/Cancellables => TIFoundationUtils/Cancellables/Sources}/NonCancellable.swift (92%) rename TIMoyaNetworking/Sources/RecoverableNetworkService/AsyncEventHandler/AnyAsyncEventHandler.swift => TIFoundationUtils/Cancellables/Sources/ScopeCancellable.swift (69%) rename TIMoyaNetworking/Sources/RecoverableNetworkService/AsyncEventHandler/AsyncEventHandler.swift => TIFoundationUtils/Cancellables/Sources/WeakTargetCancellable.swift (74%) rename TIMoyaNetworking/Sources/NetworkService/{EndpointErrorResult.swift => EndpointErrorResult+NetworkConnectionError.swift} (92%) rename TIMoyaNetworking/Sources/NetworkService/{Cancellables/ScopeCancellable.swift => Internals/MoyaCancellable+TIFoundationUtilsCancellable.swift} (76%) create mode 100644 TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/DefaultFingerprintsProvider.swift rename TIFoundationUtils/AsyncOperation/Sources/CancellableTask.swift => TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/FingerprintsProvider.swift (83%) rename TIMoyaNetworking/Sources/RecoverableNetworkService/AsyncEventHandler/RecoverableErrorHandlerResult.swift => TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/FingerprintsSecureStorage.swift (89%) create mode 100644 TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/FingerprintsSettingsStorage.swift create mode 100644 TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/FingerprintsTrustEvaluator.swift rename TINetworking/Sources/{ => Alamofire}/SessionFactory.swift (100%) create mode 100644 TINetworking/Sources/ApiInteractor/ApiInteractor.swift rename TIMoyaNetworking/Sources/RecoverableNetworkService/AsyncEventHandler/AsyncErrorHandler.swift => TINetworking/Sources/ApiInteractor/EndpointErrorResult.swift (84%) create mode 100644 TINetworking/Sources/ApiInteractor/EndpointRecoverableRequestResult.swift rename {TIMoyaNetworking/Sources/NetworkService => TINetworking/Sources/ApiInteractor}/EndpointRequestResult.swift (100%) create mode 100644 TINetworking/Sources/ApiInteractor/ErrorCollection.swift create mode 100644 TINetworking/Sources/Interceptors/AnyEndpointRequestRetrier.swift create mode 100644 TINetworking/Sources/Interceptors/DefaultTokenInterceptor.swift create mode 100644 TINetworking/Sources/Interceptors/EndpointRequestRetrier.swift create mode 100644 TINetworking/Sources/Interceptors/EndpointResponseTokenInterceptor.swift rename {TIMoyaNetworking/Sources/NetworkService/Plugins => TINetworking/Sources}/RequestPreprocessors/DefaultEndpointSecurityRequestPreprocessor.swift (99%) rename {TIMoyaNetworking/Sources/NetworkService/Plugins => TINetworking/Sources}/RequestPreprocessors/DefaultSecuritySchemesRequestPreprocessor.swift (99%) rename {TIMoyaNetworking/Sources/NetworkService/Plugins => TINetworking/Sources}/RequestPreprocessors/EndpointRequestPreprocessor.swift (98%) rename {TIMoyaNetworking/Sources/NetworkService/Plugins => TINetworking/Sources}/RequestPreprocessors/EndpointSecurityRequestPreprocessor.swift (98%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bf888ad..bbcffada 100644 --- a/CHANGELOG.md +++ b/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. diff --git a/LeadKit.podspec b/LeadKit.podspec index 6b04d07f..e83503e0 100644 --- a/LeadKit.podspec +++ b/LeadKit.podspec @@ -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" diff --git a/Package.swift b/Package.swift index 0b91e79d..0edf613e 100644 --- a/Package.swift +++ b/Package.swift @@ -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"), diff --git a/TIAppleMapUtils/TIAppleMapUtils.podspec b/TIAppleMapUtils/TIAppleMapUtils.podspec index ae4d5a2b..0e1baab4 100644 --- a/TIAppleMapUtils/TIAppleMapUtils.podspec +++ b/TIAppleMapUtils/TIAppleMapUtils.podspec @@ -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' } diff --git a/TIAuth/TIAuth.podspec b/TIAuth/TIAuth.podspec index 75f5eaa0..42f082b1 100644 --- a/TIAuth/TIAuth.podspec +++ b/TIAuth/TIAuth.podspec @@ -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' } diff --git a/TIFoundationUtils/AsyncOperation/Sources/ClosureAsyncOperation.swift b/TIFoundationUtils/AsyncOperation/Sources/ClosureAsyncOperation.swift index 06e16cc6..485d52c5 100644 --- a/TIFoundationUtils/AsyncOperation/Sources/ClosureAsyncOperation.swift +++ b/TIFoundationUtils/AsyncOperation/Sources/ClosureAsyncOperation.swift @@ -22,10 +22,10 @@ public final class ClosureAsyncOperation: AsyncOperation { public typealias AsyncTaskClosure = () async -> Result - public typealias CancellableTaskClosure = (@escaping (Result) -> Void) -> CancellableTask + public typealias CancellableTaskClosure = (@escaping (Result) -> 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: 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 diff --git a/TIMoyaNetworking/Sources/NetworkService/Cancellables/BaseCancellable.swift b/TIFoundationUtils/Cancellables/Sources/BaseCancellable.swift similarity index 92% rename from TIMoyaNetworking/Sources/NetworkService/Cancellables/BaseCancellable.swift rename to TIFoundationUtils/Cancellables/Sources/BaseCancellable.swift index 05e2e690..9797e918 100644 --- a/TIMoyaNetworking/Sources/NetworkService/Cancellables/BaseCancellable.swift +++ b/TIFoundationUtils/Cancellables/Sources/BaseCancellable.swift @@ -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() { diff --git a/TIMoyaNetworking/Sources/NetworkService/Cancellables/CancellableBag.swift b/TIFoundationUtils/Cancellables/Sources/BaseCancellableBag.swift similarity index 89% rename from TIMoyaNetworking/Sources/NetworkService/Cancellables/CancellableBag.swift rename to TIFoundationUtils/Cancellables/Sources/BaseCancellableBag.swift index bfddf2ff..13e7c78d 100644 --- a/TIMoyaNetworking/Sources/NetworkService/Cancellables/CancellableBag.swift +++ b/TIFoundationUtils/Cancellables/Sources/BaseCancellableBag.swift @@ -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) } } diff --git a/TIMoyaNetworking/Sources/NetworkService/Cancellables/Cancellable+Conformances.swift b/TIFoundationUtils/Cancellables/Sources/Cancellable.swift similarity index 93% rename from TIMoyaNetworking/Sources/NetworkService/Cancellables/Cancellable+Conformances.swift rename to TIFoundationUtils/Cancellables/Sources/Cancellable.swift index 82d1a13a..8c35d4e7 100644 --- a/TIMoyaNetworking/Sources/NetworkService/Cancellables/Cancellable+Conformances.swift +++ b/TIFoundationUtils/Cancellables/Sources/Cancellable.swift @@ -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 {} diff --git a/TIMoyaNetworking/Sources/NetworkService/Cancellables/Cancellables.swift b/TIFoundationUtils/Cancellables/Sources/Cancellables.swift similarity index 90% rename from TIMoyaNetworking/Sources/NetworkService/Cancellables/Cancellables.swift rename to TIFoundationUtils/Cancellables/Sources/Cancellables.swift index 87955661..7d805493 100644 --- a/TIMoyaNetworking/Sources/NetworkService/Cancellables/Cancellables.swift +++ b/TIFoundationUtils/Cancellables/Sources/Cancellables.swift @@ -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() - } } diff --git a/TIMoyaNetworking/Sources/NetworkService/Cancellables/NonCancellable.swift b/TIFoundationUtils/Cancellables/Sources/NonCancellable.swift similarity index 92% rename from TIMoyaNetworking/Sources/NetworkService/Cancellables/NonCancellable.swift rename to TIFoundationUtils/Cancellables/Sources/NonCancellable.swift index aa200f3e..a8b39095 100644 --- a/TIMoyaNetworking/Sources/NetworkService/Cancellables/NonCancellable.swift +++ b/TIFoundationUtils/Cancellables/Sources/NonCancellable.swift @@ -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() {} diff --git a/TIMoyaNetworking/Sources/RecoverableNetworkService/AsyncEventHandler/AnyAsyncEventHandler.swift b/TIFoundationUtils/Cancellables/Sources/ScopeCancellable.swift similarity index 69% rename from TIMoyaNetworking/Sources/RecoverableNetworkService/AsyncEventHandler/AnyAsyncEventHandler.swift rename to TIFoundationUtils/Cancellables/Sources/ScopeCancellable.swift index 85858a70..b078af47 100644 --- a/TIMoyaNetworking/Sources/RecoverableNetworkService/AsyncEventHandler/AnyAsyncEventHandler.swift +++ b/TIFoundationUtils/Cancellables/Sources/ScopeCancellable.swift @@ -22,17 +22,20 @@ import TISwiftUtils -@available(iOS 13.0.0, *) -public struct AnyAsyncEventHandler: AsyncEventHandler { - private let processClosure: AsyncClosure +public final class ScopeCancellable: Cancellable { + public typealias ScopeCancellableClosure = Closure - public init(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() } } diff --git a/TIMoyaNetworking/Sources/RecoverableNetworkService/AsyncEventHandler/AsyncEventHandler.swift b/TIFoundationUtils/Cancellables/Sources/WeakTargetCancellable.swift similarity index 74% rename from TIMoyaNetworking/Sources/RecoverableNetworkService/AsyncEventHandler/AsyncEventHandler.swift rename to TIFoundationUtils/Cancellables/Sources/WeakTargetCancellable.swift index c5774269..12a14f72 100644 --- a/TIMoyaNetworking/Sources/RecoverableNetworkService/AsyncEventHandler/AsyncEventHandler.swift +++ b/TIFoundationUtils/Cancellables/Sources/WeakTargetCancellable.swift @@ -20,17 +20,18 @@ // THE SOFTWARE. // -@available(iOS 13.0.0, *) -public protocol AsyncEventHandler { - associatedtype EventType - associatedtype ResultType +public struct WeakTargetCancellable: 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 { - .init(handler: self) + public init(target: T?, cancelClosure: @escaping CancelClosure) { + self.target = target + self.cancelClosure = cancelClosure + } + + public func cancel() { + cancelClosure(target) } } diff --git a/TIFoundationUtils/CodableKeyValueStorage/Sources/Decoders/UnarchiverKeyValueDecoder.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/Decoders/UnarchiverKeyValueDecoder.swift index 49e0d5a7..afd4a96e 100644 --- a/TIFoundationUtils/CodableKeyValueStorage/Sources/Decoders/UnarchiverKeyValueDecoder.swift +++ b/TIFoundationUtils/CodableKeyValueStorage/Sources/Decoders/UnarchiverKeyValueDecoder.swift @@ -22,6 +22,7 @@ import Foundation +@available(iOS 11.0, *) open class UnarchiverKeyValueDecoder: CodableKeyValueDecoder { public init() {} diff --git a/TIFoundationUtils/CodableKeyValueStorage/Sources/Encoders/ArchiverKeyValueEncoder.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/Encoders/ArchiverKeyValueEncoder.swift index 80a8a954..30c916fd 100644 --- a/TIFoundationUtils/CodableKeyValueStorage/Sources/Encoders/ArchiverKeyValueEncoder.swift +++ b/TIFoundationUtils/CodableKeyValueStorage/Sources/Encoders/ArchiverKeyValueEncoder.swift @@ -22,6 +22,7 @@ import Foundation +@available(iOS 11.0, *) open class ArchiverKeyValueEncoder: CodableKeyValueEncoder { public init() {} diff --git a/TIFoundationUtils/DateFormatting/Sources/KeyedDecodingContainer+DateDecoding.swift b/TIFoundationUtils/DateFormatting/Sources/KeyedDecodingContainer+DateDecoding.swift index 8c55e6e3..6f4d65f8 100644 --- a/TIFoundationUtils/DateFormatting/Sources/KeyedDecodingContainer+DateDecoding.swift +++ b/TIFoundationUtils/DateFormatting/Sources/KeyedDecodingContainer+DateDecoding.swift @@ -105,6 +105,7 @@ public extension KeyedDecodingContainer { // ISO8601 +@available(iOS 11.0, *) public extension KeyedDecodingContainer { func decodeDate(forKey key: Key, userInfo: [CodingUserInfoKey: Any], diff --git a/TIFoundationUtils/TIFoundationUtils.podspec b/TIFoundationUtils/TIFoundationUtils.podspec index 981fe47d..0ad14462 100644 --- a/TIFoundationUtils/TIFoundationUtils.podspec +++ b/TIFoundationUtils/TIFoundationUtils.podspec @@ -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/**/*' diff --git a/TIGoogleMapUtils/TIGoogleMapUtils.podspec b/TIGoogleMapUtils/TIGoogleMapUtils.podspec index d927a348..7c37ceed 100644 --- a/TIGoogleMapUtils/TIGoogleMapUtils.podspec +++ b/TIGoogleMapUtils/TIGoogleMapUtils.podspec @@ -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' } diff --git a/TIKeychainUtils/TIKeychainUtils.podspec b/TIKeychainUtils/TIKeychainUtils.podspec index 73c1d651..cd3afa39 100644 --- a/TIKeychainUtils/TIKeychainUtils.podspec +++ b/TIKeychainUtils/TIKeychainUtils.podspec @@ -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' } diff --git a/TIMapUtils/TIMapUtils.podspec b/TIMapUtils/TIMapUtils.podspec index 56a31464..b9fefb90 100644 --- a/TIMapUtils/TIMapUtils.podspec +++ b/TIMapUtils/TIMapUtils.podspec @@ -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' } diff --git a/TIMoyaNetworking/Sources/NetworkService/DefaultJsonNetworkService.swift b/TIMoyaNetworking/Sources/NetworkService/DefaultJsonNetworkService.swift index 2ebfa489..4c45f1be 100644 --- a/TIMoyaNetworking/Sources/NetworkService/DefaultJsonNetworkService.swift +++ b/TIMoyaNetworking/Sources/NetworkService/DefaultJsonNetworkService.swift @@ -27,8 +27,8 @@ import TISwiftUtils import Foundation import TIFoundationUtils -open class DefaultJsonNetworkService { - public typealias RequestResult = EndpointRequestResult +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(request: EndpointRequest) async -> RequestResult { - await process(request: request, - mapSuccess: Result.success, - mapFailure: { .failure(.apiError($0)) }, - mapMoyaError: { .failure(.networkError($0)) }) - } - - @available(iOS 13.0.0, *) open func process(request: EndpointRequest, mapSuccess: @escaping Closure, mapFailure: @escaping Closure, - mapMoyaError: @escaping Closure) 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(request: EndpointRequest, - mapSuccess: @escaping Closure, - mapFailure: @escaping Closure, - mapMoyaError: @escaping Closure, - completion: @escaping ParameterClosure) -> Cancellable { + mapNetworkError: @escaping Closure, + completion: @escaping ParameterClosure) -> 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(request: SerializedRequest, mapSuccess: @escaping Closure, mapFailure: @escaping Closure, - mapMoyaError: @escaping Closure, - completion: @escaping ParameterClosure) -> Cancellable { + mapNetworkError: @escaping Closure, + completion: @escaping ParameterClosure) -> 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(securityPreprocessors: [T: SecuritySchemePreprocessor]) where T.RawValue == String { diff --git a/TIMoyaNetworking/Sources/NetworkService/EndpointErrorResult.swift b/TIMoyaNetworking/Sources/NetworkService/EndpointErrorResult+NetworkConnectionError.swift similarity index 92% rename from TIMoyaNetworking/Sources/NetworkService/EndpointErrorResult.swift rename to TIMoyaNetworking/Sources/NetworkService/EndpointErrorResult+NetworkConnectionError.swift index 05cc5f82..50403be4 100644 --- a/TIMoyaNetworking/Sources/NetworkService/EndpointErrorResult.swift +++ b/TIMoyaNetworking/Sources/NetworkService/EndpointErrorResult+NetworkConnectionError.swift @@ -20,14 +20,10 @@ // THE SOFTWARE. // +import TINetworking import Moya import Foundation -public enum EndpointErrorResult: Error { - case apiError(ApiError) - case networkError(NetworkError) -} - public extension EndpointErrorResult where NetworkError == MoyaError { var isNetworkConnectionProblem: Bool { guard case let .networkError(moyaError) = self, diff --git a/TIMoyaNetworking/Sources/NetworkService/Cancellables/ScopeCancellable.swift b/TIMoyaNetworking/Sources/NetworkService/Internals/MoyaCancellable+TIFoundationUtilsCancellable.swift similarity index 76% rename from TIMoyaNetworking/Sources/NetworkService/Cancellables/ScopeCancellable.swift rename to TIMoyaNetworking/Sources/NetworkService/Internals/MoyaCancellable+TIFoundationUtilsCancellable.swift index 92b8f082..d0ac7fc7 100644 --- a/TIMoyaNetworking/Sources/NetworkService/Cancellables/ScopeCancellable.swift +++ b/TIMoyaNetworking/Sources/NetworkService/Internals/MoyaCancellable+TIFoundationUtilsCancellable.swift @@ -21,14 +21,18 @@ // import Moya -import TISwiftUtils +import TIFoundationUtils -public final class ScopeCancellable: CancellableBag { - public typealias ScopeCancellableClosure = Closure +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) } } diff --git a/TIMoyaNetworking/Sources/RecoverableNetworkService/DefaultRecoverableJsonNetworkService.swift b/TIMoyaNetworking/Sources/RecoverableNetworkService/DefaultRecoverableJsonNetworkService.swift index ee4585c8..eaaa8988 100644 --- a/TIMoyaNetworking/Sources/RecoverableNetworkService/DefaultRecoverableJsonNetworkService.swift +++ b/TIMoyaNetworking/Sources/RecoverableNetworkService/DefaultRecoverableJsonNetworkService.swift @@ -26,55 +26,60 @@ import TISwiftUtils @available(iOS 13.0.0, *) open class DefaultRecoverableJsonNetworkService: DefaultJsonNetworkService { + public typealias EndpointResponse = EndpointRecoverableRequestResult public typealias ErrorType = EndpointErrorResult - public typealias ErrorHandlerResultType = RecoverableErrorHandlerResult - public typealias ErrorHandlerType = AnyAsyncEventHandler + public typealias RequestRetrier = AnyEndpointRequestRetrier - public private(set) var defaultErrorHandlers: [ErrorHandlerType] = [] + public private(set) var defaultRequestRetriers: [RequestRetrier] = [] open func process(recoverableRequest: EndpointRequest, - prependErrorHandlers: [ErrorHandlerType] = [], - appendErrorHandlers: [ErrorHandlerType] = []) async -> - RequestResult { + prependRequestRetriers: [RequestRetrier] = [], + appendRequestRetriers: [RequestRetrier] = []) async -> + EndpointResponse { await process(recoverableRequest: recoverableRequest, - errorHandlers: prependErrorHandlers + defaultErrorHandlers + appendErrorHandlers) + errorHandlers: prependRequestRetriers + defaultRequestRetriers + appendRequestRetriers) } open func process(recoverableRequest: EndpointRequest, - errorHandlers: [ErrorHandlerType]) async -> RequestResult { + errorHandlers: [RequestRetrier]) async -> EndpointResponse { let result: RequestResult = 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(defaultErrorHandler: ErrorHandler) - where ErrorHandler.EventType == ErrorHandlerType.EventType, ErrorHandler.ResultType == ErrorHandlerType.ResultType { + public func register(defaultRequestRetrier: RequestRetrier) + where RequestRetrier.ErrorResult == ErrorType { - defaultErrorHandlers.append(defaultErrorHandler.asAnyAsyncEventHandler()) + defaultRequestRetriers.append(defaultRequestRetrier.asAnyEndpointRequestRetrier()) } - public func set(defaultErrorHandlers: ErrorHandler...) - where ErrorHandler.EventType == ErrorHandlerType.EventType, ErrorHandler.ResultType == ErrorHandlerType.ResultType { + public func set(defaultRequestRetriers: RequestRetrier...) + where RequestRetrier.ErrorResult == ErrorType { - self.defaultErrorHandlers = defaultErrorHandlers.map { $0.asAnyAsyncEventHandler() } + self.defaultRequestRetriers = defaultRequestRetriers.map { $0.asAnyEndpointRequestRetrier() } } } diff --git a/TIMoyaNetworking/TIMoyaNetworking.podspec b/TIMoyaNetworking/TIMoyaNetworking.podspec index 9b427432..78b60e31 100644 --- a/TIMoyaNetworking/TIMoyaNetworking.podspec +++ b/TIMoyaNetworking/TIMoyaNetworking.podspec @@ -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' } diff --git a/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/DefaultFingerprintsProvider.swift b/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/DefaultFingerprintsProvider.swift new file mode 100644 index 00000000..e5877e29 --- /dev/null +++ b/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/DefaultFingerprintsProvider.swift @@ -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]) { + + self.secureStorage = secureStorage + self.settingsStorage = settingsStorage + + if settingsStorage.shouldResetFingerprints { + self.secureStorage.knownPins = bundledFingerprints + self.settingsStorage.shouldResetFingerprints = false + } + } + + public func fingerprints(forHost host: String) -> Set { + secureStorage.knownPins[host] ?? [] + } + + public func add(fingerprints: [String], forHost host: String) { + let pinsForHost = (secureStorage.knownPins[host] ?? []).union(fingerprints) + secureStorage.knownPins.updateValue(pinsForHost, forKey: host) + } +} diff --git a/TIFoundationUtils/AsyncOperation/Sources/CancellableTask.swift b/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/FingerprintsProvider.swift similarity index 83% rename from TIFoundationUtils/AsyncOperation/Sources/CancellableTask.swift rename to TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/FingerprintsProvider.swift index 356e9949..871f46de 100644 --- a/TIFoundationUtils/AsyncOperation/Sources/CancellableTask.swift +++ b/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/FingerprintsProvider.swift @@ -20,14 +20,7 @@ // THE SOFTWARE. // -import Foundation - -public protocol CancellableTask { - func cancel() +public protocol FingerprintsProvider { + func fingerprints(forHost host: String) -> Set + func add(fingerprints: [String], forHost host: String) } - -@available(iOS 13.0, *) -extension Task: CancellableTask {} - -extension Operation: CancellableTask {} -extension DispatchWorkItem: CancellableTask {} diff --git a/TIMoyaNetworking/Sources/RecoverableNetworkService/AsyncEventHandler/RecoverableErrorHandlerResult.swift b/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/FingerprintsSecureStorage.swift similarity index 89% rename from TIMoyaNetworking/Sources/RecoverableNetworkService/AsyncEventHandler/RecoverableErrorHandlerResult.swift rename to TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/FingerprintsSecureStorage.swift index 4424c386..99fa9e69 100644 --- a/TIMoyaNetworking/Sources/RecoverableNetworkService/AsyncEventHandler/RecoverableErrorHandlerResult.swift +++ b/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/FingerprintsSecureStorage.swift @@ -20,8 +20,6 @@ // THE SOFTWARE. // -public enum RecoverableErrorHandlerResult { - case skipError - case recoverRequest - case forwardError(ErrorType) +public protocol FingerprintsSecureStorage { + var knownPins: [String: Set] { get set } } diff --git a/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/FingerprintsSettingsStorage.swift b/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/FingerprintsSettingsStorage.swift new file mode 100644 index 00000000..daf4909a --- /dev/null +++ b/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/FingerprintsSettingsStorage.swift @@ -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 } +} diff --git a/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/FingerprintsTrustEvaluator.swift b/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/FingerprintsTrustEvaluator.swift new file mode 100644 index 00000000..36b23638 --- /dev/null +++ b/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/FingerprintsTrustEvaluator.swift @@ -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, 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) + } + } +} + diff --git a/TINetworking/Sources/SessionFactory.swift b/TINetworking/Sources/Alamofire/SessionFactory.swift similarity index 100% rename from TINetworking/Sources/SessionFactory.swift rename to TINetworking/Sources/Alamofire/SessionFactory.swift diff --git a/TINetworking/Sources/ApiInteractor/ApiInteractor.swift b/TINetworking/Sources/ApiInteractor/ApiInteractor.swift new file mode 100644 index 00000000..c0874d3a --- /dev/null +++ b/TINetworking/Sources/ApiInteractor/ApiInteractor.swift @@ -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 = EndpointRequestResult + + func process(request: EndpointRequest, + mapSuccess: @escaping Closure, + mapFailure: @escaping Closure, + mapNetworkError: @escaping Closure, + completion: @escaping ParameterClosure) -> Cancellable +} + +@available(iOS 13.0.0, *) +public extension ApiInteractor { + func process(request: EndpointRequest) async -> RequestResult { + await process(request: request, + mapSuccess: Result.success, + mapFailure: { .failure(.apiError($0)) }, + mapNetworkError: { .failure(.networkError($0)) }) + } + + func process(request: EndpointRequest, + mapSuccess: @escaping Closure, + mapFailure: @escaping Closure, + mapNetworkError: @escaping Closure) 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) + } + }) + } +} diff --git a/TIMoyaNetworking/Sources/RecoverableNetworkService/AsyncEventHandler/AsyncErrorHandler.swift b/TINetworking/Sources/ApiInteractor/EndpointErrorResult.swift similarity index 84% rename from TIMoyaNetworking/Sources/RecoverableNetworkService/AsyncEventHandler/AsyncErrorHandler.swift rename to TINetworking/Sources/ApiInteractor/EndpointErrorResult.swift index 2afccd02..4e3f9b36 100644 --- a/TIMoyaNetworking/Sources/RecoverableNetworkService/AsyncEventHandler/AsyncErrorHandler.swift +++ b/TINetworking/Sources/ApiInteractor/EndpointErrorResult.swift @@ -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: Error { + case apiError(ApiError) + case networkError(NetworkError) +} diff --git a/TINetworking/Sources/ApiInteractor/EndpointRecoverableRequestResult.swift b/TINetworking/Sources/ApiInteractor/EndpointRecoverableRequestResult.swift new file mode 100644 index 00000000..550f1310 --- /dev/null +++ b/TINetworking/Sources/ApiInteractor/EndpointRecoverableRequestResult.swift @@ -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 = Result>> diff --git a/TIMoyaNetworking/Sources/NetworkService/EndpointRequestResult.swift b/TINetworking/Sources/ApiInteractor/EndpointRequestResult.swift similarity index 100% rename from TIMoyaNetworking/Sources/NetworkService/EndpointRequestResult.swift rename to TINetworking/Sources/ApiInteractor/EndpointRequestResult.swift diff --git a/TINetworking/Sources/ApiInteractor/ErrorCollection.swift b/TINetworking/Sources/ApiInteractor/ErrorCollection.swift new file mode 100644 index 00000000..05345b87 --- /dev/null +++ b/TINetworking/Sources/ApiInteractor/ErrorCollection.swift @@ -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: Error { + public let failures: [E] + + public init(failures: [E]) { + self.failures = failures + } + + public func map(transform: (E) -> R) -> ErrorCollection { + .init(failures: failures.map(transform)) + } + + public func firstOr(_ default: E) -> E { + failures.first ?? `default` + } + + public func lastOr(_ default: E) -> E { + failures.last ?? `default` + } +} diff --git a/TINetworking/Sources/Interceptors/AnyEndpointRequestRetrier.swift b/TINetworking/Sources/Interceptors/AnyEndpointRequestRetrier.swift new file mode 100644 index 00000000..567a6cac --- /dev/null +++ b/TINetworking/Sources/Interceptors/AnyEndpointRequestRetrier.swift @@ -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: EndpointRequestRetrier { + typealias ValidateAndRepairClosure = ([ErrorResult], @escaping (Result) -> Void) -> Cancellable + + private let validateAndRepairClosure: ValidateAndRepairClosure + + public init(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 { + .init(retrier: self) + } +} diff --git a/TINetworking/Sources/Interceptors/DefaultTokenInterceptor.swift b/TINetworking/Sources/Interceptors/DefaultTokenInterceptor.swift new file mode 100644 index 00000000..ddf6b3a5 --- /dev/null +++ b/TINetworking/Sources/Interceptors/DefaultTokenInterceptor.swift @@ -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: 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) -> Void) { + + let adaptBag = BaseCancellableBag() + + let adaptCompletion: (Result) -> 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) -> 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(validationClosure: () -> Bool, + completion: @escaping (Result) -> 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() + } + } +} diff --git a/TINetworking/Sources/Interceptors/EndpointRequestRetrier.swift b/TINetworking/Sources/Interceptors/EndpointRequestRetrier.swift new file mode 100644 index 00000000..89d1a06a --- /dev/null +++ b/TINetworking/Sources/Interceptors/EndpointRequestRetrier.swift @@ -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 + + 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) + } + }) + } +} diff --git a/TINetworking/Sources/Interceptors/EndpointResponseTokenInterceptor.swift b/TINetworking/Sources/Interceptors/EndpointResponseTokenInterceptor.swift new file mode 100644 index 00000000..b3897bb7 --- /dev/null +++ b/TINetworking/Sources/Interceptors/EndpointResponseTokenInterceptor.swift @@ -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: DefaultTokenInterceptor>, EndpointRequestRetrier { + public typealias IsTokenInvalidErrorResultClosure = (EndpointErrorResult) -> Bool + public typealias RepairResult = Result> + + 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], + 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) + } +} diff --git a/TIMoyaNetworking/Sources/NetworkService/Plugins/RequestPreprocessors/DefaultEndpointSecurityRequestPreprocessor.swift b/TINetworking/Sources/RequestPreprocessors/DefaultEndpointSecurityRequestPreprocessor.swift similarity index 99% rename from TIMoyaNetworking/Sources/NetworkService/Plugins/RequestPreprocessors/DefaultEndpointSecurityRequestPreprocessor.swift rename to TINetworking/Sources/RequestPreprocessors/DefaultEndpointSecurityRequestPreprocessor.swift index ddacee28..dc461c6f 100644 --- a/TIMoyaNetworking/Sources/NetworkService/Plugins/RequestPreprocessors/DefaultEndpointSecurityRequestPreprocessor.swift +++ b/TINetworking/Sources/RequestPreprocessors/DefaultEndpointSecurityRequestPreprocessor.swift @@ -20,8 +20,6 @@ // THE SOFTWARE. // -import TINetworking - open class DefaultSecuritySchemePreprocessor: SecuritySchemePreprocessor { struct ValueNotProvidedError: Error {} diff --git a/TIMoyaNetworking/Sources/NetworkService/Plugins/RequestPreprocessors/DefaultSecuritySchemesRequestPreprocessor.swift b/TINetworking/Sources/RequestPreprocessors/DefaultSecuritySchemesRequestPreprocessor.swift similarity index 99% rename from TIMoyaNetworking/Sources/NetworkService/Plugins/RequestPreprocessors/DefaultSecuritySchemesRequestPreprocessor.swift rename to TINetworking/Sources/RequestPreprocessors/DefaultSecuritySchemesRequestPreprocessor.swift index 66290349..6e40adb1 100644 --- a/TIMoyaNetworking/Sources/NetworkService/Plugins/RequestPreprocessors/DefaultSecuritySchemesRequestPreprocessor.swift +++ b/TINetworking/Sources/RequestPreprocessors/DefaultSecuritySchemesRequestPreprocessor.swift @@ -20,8 +20,6 @@ // THE SOFTWARE. // -import TINetworking - open class DefaultEndpointSecurityPreprocessor: EndpointRequestPreprocessor { enum PreprocessError: Error { case missingSecurityScheme(String, [String: SecurityScheme]) diff --git a/TIMoyaNetworking/Sources/NetworkService/Plugins/RequestPreprocessors/EndpointRequestPreprocessor.swift b/TINetworking/Sources/RequestPreprocessors/EndpointRequestPreprocessor.swift similarity index 98% rename from TIMoyaNetworking/Sources/NetworkService/Plugins/RequestPreprocessors/EndpointRequestPreprocessor.swift rename to TINetworking/Sources/RequestPreprocessors/EndpointRequestPreprocessor.swift index af190a50..2a1a3764 100644 --- a/TIMoyaNetworking/Sources/NetworkService/Plugins/RequestPreprocessors/EndpointRequestPreprocessor.swift +++ b/TINetworking/Sources/RequestPreprocessors/EndpointRequestPreprocessor.swift @@ -20,8 +20,6 @@ // THE SOFTWARE. // -import TINetworking - public protocol EndpointRequestPreprocessor { func preprocess(request: EndpointRequest) throws -> EndpointRequest } diff --git a/TIMoyaNetworking/Sources/NetworkService/Plugins/RequestPreprocessors/EndpointSecurityRequestPreprocessor.swift b/TINetworking/Sources/RequestPreprocessors/EndpointSecurityRequestPreprocessor.swift similarity index 98% rename from TIMoyaNetworking/Sources/NetworkService/Plugins/RequestPreprocessors/EndpointSecurityRequestPreprocessor.swift rename to TINetworking/Sources/RequestPreprocessors/EndpointSecurityRequestPreprocessor.swift index 8fcc1965..3ba79ad1 100644 --- a/TIMoyaNetworking/Sources/NetworkService/Plugins/RequestPreprocessors/EndpointSecurityRequestPreprocessor.swift +++ b/TINetworking/Sources/RequestPreprocessors/EndpointSecurityRequestPreprocessor.swift @@ -20,8 +20,6 @@ // THE SOFTWARE. // -import TINetworking - public protocol SecuritySchemePreprocessor { func preprocess(request: EndpointRequest, using security: SecurityScheme) throws -> EndpointRequest } diff --git a/TINetworking/TINetworking.podspec b/TINetworking/TINetworking.podspec index b404d0d1..abe56038 100644 --- a/TINetworking/TINetworking.podspec +++ b/TINetworking/TINetworking.podspec @@ -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 diff --git a/TINetworkingCache/TINetworkingCache.podspec b/TINetworkingCache/TINetworkingCache.podspec index aa7c649f..8f87a5f7 100644 --- a/TINetworkingCache/TINetworkingCache.podspec +++ b/TINetworkingCache/TINetworkingCache.podspec @@ -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' } diff --git a/TIPagination/TIPagination.podspec b/TIPagination/TIPagination.podspec index 286e29b3..6c4f81cd 100644 --- a/TIPagination/TIPagination.podspec +++ b/TIPagination/TIPagination.podspec @@ -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' } diff --git a/TISwiftUICore/TISwiftUICore.podspec b/TISwiftUICore/TISwiftUICore.podspec index 3c858af2..b67348a0 100644 --- a/TISwiftUICore/TISwiftUICore.podspec +++ b/TISwiftUICore/TISwiftUICore.podspec @@ -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' } diff --git a/TISwiftUtils/TISwiftUtils.podspec b/TISwiftUtils/TISwiftUtils.podspec index 2c3d9099..d0812a96 100644 --- a/TISwiftUtils/TISwiftUtils.podspec +++ b/TISwiftUtils/TISwiftUtils.podspec @@ -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' } diff --git a/TITableKitUtils/TITableKitUtils.podspec b/TITableKitUtils/TITableKitUtils.podspec index d3445cf2..438966cb 100644 --- a/TITableKitUtils/TITableKitUtils.podspec +++ b/TITableKitUtils/TITableKitUtils.podspec @@ -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' } diff --git a/TITransitions/TITransitions.podspec b/TITransitions/TITransitions.podspec index cf3ad8f5..f054a296 100644 --- a/TITransitions/TITransitions.podspec +++ b/TITransitions/TITransitions.podspec @@ -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' } diff --git a/TIUIElements/TIUIElements.podspec b/TIUIElements/TIUIElements.podspec index bb3f97ef..00576842 100644 --- a/TIUIElements/TIUIElements.podspec +++ b/TIUIElements/TIUIElements.podspec @@ -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' } diff --git a/TIUIKitCore/TIUIKitCore.podspec b/TIUIKitCore/TIUIKitCore.podspec index 017d9b52..9f979669 100644 --- a/TIUIKitCore/TIUIKitCore.podspec +++ b/TIUIKitCore/TIUIKitCore.podspec @@ -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' } diff --git a/TIYandexMapUtils/TIYandexMapUtils.podspec b/TIYandexMapUtils/TIYandexMapUtils.podspec index 523ca586..b12086f8 100644 --- a/TIYandexMapUtils/TIYandexMapUtils.podspec +++ b/TIYandexMapUtils/TIYandexMapUtils.podspec @@ -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' }