feat: `SingleValueStorage` implementations + `AppInstallLifetimeSingleValueStorage` for automatically removing keychain items on app reinstall.

`DefaultRecoverableJsonNetworkService` supports iOS 12.
This commit is contained in:
Ivan Smolin 2023-05-24 10:26:34 +03:00
parent 0e0a8d733e
commit 5ca564476a
19 changed files with 526 additions and 104 deletions

View File

@ -1,5 +1,10 @@
# Changelog
### 1.45.0
- **Added**: `SingleValueStorage` implementations + `AppInstallLifetimeSingleValueStorage` for automatically removing keychain items on app reinstall.
- **Update**: `DefaultRecoverableJsonNetworkService` supports iOS 12.
### 1.44.0
- **Added**: HTTP status codes to `EndpointErrorResult.apiError` responses

View File

@ -91,7 +91,7 @@ let package = Package(
.target(name: "OTPSwiftView", dependencies: ["TIUIElements"], path: "OTPSwiftView/Sources"),
.target(name: "TITransitions", path: "TITransitions/Sources"),
.target(name: "TIPagination", dependencies: ["Cursors", "TISwiftUtils"], path: "TIPagination/Sources"),
.target(name: "TIAuth", dependencies: ["TIFoundationUtils", "TIUIKitCore", "KeychainAccess"], path: "TIAuth/Sources"),
.target(name: "TIAuth", dependencies: ["TIFoundationUtils", "TIUIKitCore", "TIKeychainUtils"], path: "TIAuth/Sources"),
.target(name: "TIEcommerce", dependencies: ["TIFoundationUtils", "TISwiftUtils", "TINetworking", "TIUIKitCore", "TIUIElements"], path: "TIEcommerce/Sources"),
.target(name: "TITextProcessing",
dependencies: [.product(name: "Antlr4", package: "antlr4")],

View File

@ -22,12 +22,13 @@
import KeychainAccess
import Foundation
import TIFoundationUtils
import LocalAuthentication
open class DefaultEncryptedTokenKeyStorage: SingleValueAuthKeychainStorage<Data> {
open class Defaults: SingleValueAuthKeychainStorage<StringEncryptionResult>.Defaults {
public static var encryptedTokenKeyStorageKey: String {
keychainServiceIdentifier + ".encryptedTokenKey"
public static var encryptedTokenKeyStorageKey: StorageKey<Data> {
.init(rawValue: keychainServiceIdentifier + ".encryptedTokenKey")
}
public static var reusableLAContext: LAContext {
@ -40,11 +41,11 @@ open class DefaultEncryptedTokenKeyStorage: SingleValueAuthKeychainStorage<Data>
public init(keychain: Keychain = Keychain(service: Defaults.keychainServiceIdentifier),
localAuthContext: LAContext = Defaults.reusableLAContext,
settingsStorage: AuthSettingsStorage = DefaultAuthSettingsStorage(),
encryptedTokenKeyStorageKey: String = Defaults.encryptedTokenKeyStorageKey) {
encryptedTokenKeyStorageKey: StorageKey<Data> = Defaults.encryptedTokenKeyStorageKey) {
let getValueClosure: GetValueClosure = { keychain, storageKey in
do {
guard let value = try keychain.getData(storageKey) else {
guard let value = try keychain.getData(storageKey.rawValue) else {
return .failure(.valueNotFound)
}
@ -56,7 +57,7 @@ open class DefaultEncryptedTokenKeyStorage: SingleValueAuthKeychainStorage<Data>
let setValueClosure: SetValueClosure = { keychain, value, storageKey in
do {
return .success(try keychain.set(value, key: storageKey))
return .success(try keychain.set(value, key: storageKey.rawValue))
} catch {
return .failure(.unableToWriteData(underlyingError: error))
}

View File

@ -21,22 +21,23 @@
//
import Foundation
import TIFoundationUtils
import KeychainAccess
open class DefaultEncryptedTokenStorage: SingleValueAuthKeychainStorage<StringEncryptionResult> {
open class Defaults: SingleValueAuthKeychainStorage<StringEncryptionResult>.Defaults {
public static var encryptedTokenStorageKey: String {
keychainServiceIdentifier + ".encryptedToken"
public static var encryptedTokenStorageKey: StorageKey<StringEncryptionResult> {
.init(rawValue: keychainServiceIdentifier + ".encryptedToken")
}
}
public init(keychain: Keychain = Keychain(service: Defaults.keychainServiceIdentifier),
settingsStorage: AuthSettingsStorage = DefaultAuthSettingsStorage(),
encryptedTokenStorageKey: String = Defaults.encryptedTokenStorageKey) {
encryptedTokenStorageKey: StorageKey<StringEncryptionResult> = Defaults.encryptedTokenStorageKey) {
let getValueClosure: GetValueClosure = { keychain, storageKey in
do {
guard let value = try keychain.getData(storageKey) else {
guard let value = try keychain.getData(storageKey.rawValue) else {
return .failure(.valueNotFound)
}
@ -52,7 +53,7 @@ open class DefaultEncryptedTokenStorage: SingleValueAuthKeychainStorage<StringEn
let setValueClosure: SetValueClosure = { keychain, value, storageKey in
do {
return .success(try keychain.set(value.asStorableData(), key: storageKey))
return .success(try keychain.set(value.asStorableData(), key: storageKey.rawValue))
} catch {
return .failure(.unableToWriteData(underlyingError: error))
}

View File

@ -23,52 +23,43 @@
import TIFoundationUtils
import KeychainAccess
import Foundation
import TIKeychainUtils
open class SingleValueAuthKeychainStorage<ValueType>: SingleValueStorage {
open class SingleValueAuthKeychainStorage<ValueType>: BaseSingleValueKeychainStorage<ValueType> {
open class Defaults {
public static var keychainServiceIdentifier: String {
Bundle.main.bundleIdentifier ?? "ru.touchin.TIAuth"
}
}
public typealias GetValueClosure = (Keychain, String) -> Result<ValueType, StorageError>
public typealias SetValueClosure = (Keychain, ValueType, String) -> Result<Void, StorageError>
public let keychain: Keychain
public let settingsStorage: AuthSettingsStorage
public let storageKey: String
public let getValueClosure: GetValueClosure
public let setValueClosure: SetValueClosure
public init(keychain: Keychain = Keychain(service: Defaults.keychainServiceIdentifier),
settingsStorage: AuthSettingsStorage = DefaultAuthSettingsStorage(),
storageKey: String,
storageKey: StorageKey<ValueType>,
getValueClosure: @escaping GetValueClosure,
setValueClosure: @escaping SetValueClosure) {
self.keychain = keychain
self.settingsStorage = settingsStorage
self.storageKey = storageKey
self.getValueClosure = getValueClosure
self.setValueClosure = setValueClosure
super.init(keychain: keychain,
storageKey: storageKey,
getValueClosure: getValueClosure,
setValueClosure: setValueClosure)
}
// MARK: - SingleValueStorage
open func hasStoredValue() -> Bool {
!settingsStorage.shouldResetStoredAuthData && ((try? keychain.contains(storageKey)) ?? false)
open override func hasStoredValue() -> Bool {
!settingsStorage.shouldResetStoredAuthData && super.hasStoredValue()
}
open func store(value: ValueType) -> Result<Void, StorageError> {
return setValueClosure(keychain, value, storageKey)
}
open func getValue() -> Result<ValueType, StorageError> {
open override func getValue() -> Result<ValueType, StorageError> {
guard !settingsStorage.shouldResetStoredAuthData else {
let result: Result<ValueType, StorageError>
do {
try keychain.remove(storageKey)
try storage.remove(storageKey.rawValue)
settingsStorage.shouldResetStoredAuthData = false
result = .failure(.valueNotFound)
@ -79,6 +70,6 @@ open class SingleValueAuthKeychainStorage<ValueType>: SingleValueStorage {
return result
}
return getValueClosure(keychain, storageKey)
return super.getValue()
}
}

View File

@ -13,6 +13,6 @@ Pod::Spec.new do |s|
s.source_files = s.name + '/Sources/**/*'
s.dependency 'TIFoundationUtils', s.version.to_s
s.dependency 'TIKeychainUtils', s.version.to_s
s.dependency 'TIUIKitCore', s.version.to_s
s.dependency 'KeychainAccess', "~> 4.2"
end

View File

@ -25,6 +25,10 @@ open class BaseCancellable: Cancellable {
public init() {}
deinit {
cancel()
}
open func cancel() {
isCancelled = true
}

View File

@ -40,9 +40,9 @@ open class AppReinstallChecker {
}
public init(defaultsStorage: UserDefaults = .standard,
storageKey: String) {
storageKey: StorageKey<Bool>) {
self.defaultsStorage = defaultsStorage
self.storageKey = storageKey
self.storageKey = storageKey.rawValue
}
}

View File

@ -0,0 +1,38 @@
//
// Copyright (c) 2023 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 Foundation
open class BaseSingleValueDefaultsStorage<ValueType>: BaseSingleValueStorage<ValueType, UserDefaults> {
public init(defaults: UserDefaults,
storageKey: StorageKey<ValueType>,
getValueClosure: @escaping GetValueClosure,
setValueClosure: @escaping SetValueClosure) {
super.init(storage: defaults,
storageKey: storageKey,
hasValueClosure: { .success($0.object(forKey: $1.rawValue) != nil) },
deleteValueClosure: { .success($0.removeObject(forKey: $1.rawValue)) },
getValueClosure: getValueClosure,
setValueClosure: setValueClosure)
}
}

View File

@ -0,0 +1,68 @@
//
// Copyright (c) 2023 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 BaseSingleValueStorage<ValueType, StorageType>: SingleValueStorage {
public typealias HasValueClosure = (StorageType, StorageKey<ValueType>) -> Result<Bool, StorageError>
public typealias GetValueClosure = (StorageType, StorageKey<ValueType>) -> Result<ValueType, StorageError>
public typealias SetValueClosure = (StorageType, ValueType, StorageKey<ValueType>) -> Result<Void, StorageError>
public typealias DeleteValueClosure = (StorageType, StorageKey<ValueType>) -> Result<Void, StorageError>
public let storage: StorageType
public let storageKey: StorageKey<ValueType>
public let hasValueClosure: HasValueClosure
public let deleteValueClosure: DeleteValueClosure
public let getValueClosure: GetValueClosure
public let setValueClosure: SetValueClosure
public init(storage: StorageType,
storageKey: StorageKey<ValueType>,
hasValueClosure: @escaping HasValueClosure,
deleteValueClosure: @escaping DeleteValueClosure,
getValueClosure: @escaping GetValueClosure,
setValueClosure: @escaping SetValueClosure) {
self.storage = storage
self.storageKey = storageKey
self.hasValueClosure = hasValueClosure
self.deleteValueClosure = deleteValueClosure
self.getValueClosure = getValueClosure
self.setValueClosure = setValueClosure
}
// MARK: - SingleValueStorage
open func hasStoredValue() -> Bool {
(try? hasValueClosure(storage, storageKey).get()) ?? false
}
open func store(value: ValueType) -> Result<Void, StorageError> {
setValueClosure(storage, value, storageKey)
}
open func getValue() -> Result<ValueType, StorageError> {
getValueClosure(storage, storageKey)
}
public func deleteValue() -> Result<Void, StorageError> {
deleteValueClosure(storage, storageKey)
}
}

View File

@ -27,4 +27,5 @@ public protocol SingleValueStorage {
func hasStoredValue() -> Bool
func store(value: ValueType) -> Result<Void, ErrorType>
func getValue() -> Result<ValueType, ErrorType>
func deleteValue() -> Result<Void, ErrorType>
}

View File

@ -0,0 +1,40 @@
//
// Copyright (c) 2023 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 Foundation
public final class StringValueDefaultsStorage: BaseSingleValueDefaultsStorage<String> {
public init(defaults: UserDefaults, storageKey: StorageKey<String>) {
let getValueClosure: GetValueClosure = { defaults, storageKey in
guard let value = defaults.string(forKey: storageKey.rawValue) else {
return .failure(.valueNotFound)
}
return .success(value)
}
super.init(defaults: defaults,
storageKey: storageKey,
getValueClosure: getValueClosure,
setValueClosure: { .success($0.set($1, forKey: $2.rawValue)) })
}
}

View File

@ -0,0 +1,82 @@
//
// Copyright (c) 2023 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 TIFoundationUtils
open class AppInstallLifetimeSingleValueStorage<Storage: SingleValueStorage>: SingleValueStorage where Storage.ErrorType == StorageError {
public let appReinstallChecker: AppReinstallChecker
public let wrappedStorage: Storage
public init(storage: Storage,
appReinstallChecker: AppReinstallChecker) {
self.wrappedStorage = storage
self.appReinstallChecker = appReinstallChecker
}
// MARK: - SingleValueStorage
open func hasStoredValue() -> Bool {
if appReinstallChecker.isAppFirstRun {
return false
}
return wrappedStorage.hasStoredValue()
}
open func getValue() -> Result<Storage.ValueType, Storage.ErrorType> {
if appReinstallChecker.isAppFirstRun {
let result = wrappedStorage.deleteValue()
if case .success = result {
appReinstallChecker.isAppFirstRun = false
}
return result.flatMap { .failure(StorageError.valueNotFound) }
} else {
return wrappedStorage.getValue()
}
}
open func store(value: Storage.ValueType) -> Result<Void, Storage.ErrorType> {
let result = wrappedStorage.store(value: value)
if case .success = result {
appReinstallChecker.isAppFirstRun = false
}
return result
}
public func deleteValue() -> Result<Void, Storage.ErrorType> {
wrappedStorage.deleteValue()
}
}
public extension SingleValueStorage {
func appInstallLifetimeStorage(reinstallChecker: AppReinstallChecker) -> AppInstallLifetimeSingleValueStorage<Self>
where Self.ErrorType == ErrorType {
AppInstallLifetimeSingleValueStorage(storage: self,
appReinstallChecker: reinstallChecker)
}
}

View File

@ -0,0 +1,50 @@
//
// Copyright (c) 2023 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 TIFoundationUtils
import KeychainAccess
open class BaseSingleValueKeychainStorage<ValueType>: BaseSingleValueStorage<ValueType, Keychain> {
public init(keychain: Keychain,
storageKey: StorageKey<ValueType>,
getValueClosure: @escaping GetValueClosure,
setValueClosure: @escaping SetValueClosure) {
let hasValueClosure: HasValueClosure = { keychain, storageKey in
Result { try keychain.contains(storageKey.rawValue) }
.mapError { StorageError.unableToExtractData(underlyingError: $0) }
}
let deleteValueClosure: DeleteValueClosure = { keychain, storageKey in
Result { try keychain.remove(storageKey.rawValue) }
.mapError { StorageError.unableToWriteData(underlyingError: $0) }
}
super.init(storage: keychain,
storageKey: storageKey,
hasValueClosure: hasValueClosure,
deleteValueClosure: deleteValueClosure,
getValueClosure: getValueClosure,
setValueClosure: setValueClosure)
}
}

View File

@ -0,0 +1,53 @@
//
// Copyright (c) 2023 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 KeychainAccess
import TIFoundationUtils
public final class StringValueKeychainStorage: BaseSingleValueKeychainStorage<String> {
public init(keychain: Keychain, storageKey: StorageKey<String>) {
let getValueClosure: BaseSingleValueKeychainStorage<String>.GetValueClosure = { keychain, storageKey in
do {
guard let value = try keychain.get(storageKey.rawValue) else {
return .failure(.valueNotFound)
}
return .success(value)
} catch {
return .failure(.unableToExtractData(underlyingError: error))
}
}
let setValueClosure: BaseSingleValueKeychainStorage<String>.SetValueClosure = { keychain, value, storageKey in
do {
return .success(try keychain.set(value, key: storageKey.rawValue))
} catch {
return .failure(.unableToWriteData(underlyingError: error))
}
}
super.init(keychain: keychain,
storageKey: storageKey,
getValueClosure: getValueClosure,
setValueClosure: setValueClosure)
}
}

View File

@ -20,55 +20,110 @@
// THE SOFTWARE.
//
import Moya
import TINetworking
import TISwiftUtils
import TIFoundationUtils
@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 EndpointResponse<S: Decodable> = EndpointRecoverableRequestResult<S, ApiError, NetworkError>
public typealias ErrorType = EndpointErrorResult<ApiError, NetworkError>
public typealias RequestRetrier = AnyEndpointRequestRetrier<ErrorType>
public private(set) var defaultRequestRetriers: [RequestRetrier] = []
open func process<B: Encodable, S>(recoverableRequest: EndpointRequest<B, S>,
prependRequestRetriers: [RequestRetrier] = [],
appendRequestRetriers: [RequestRetrier] = [],
completion: @escaping ParameterClosure<EndpointResponse<S>>) -> Cancellable
{
process(recoverableRequest: recoverableRequest,
errorHandlers: prependRequestRetriers + defaultRequestRetriers + appendRequestRetriers,
completion: completion)
}
@available(iOS 13.0.0, *)
open func process<B: Encodable, S>(recoverableRequest: EndpointRequest<B, S>,
prependRequestRetriers: [RequestRetrier] = [],
appendRequestRetriers: [RequestRetrier] = []) async ->
EndpointResponse<S> {
await process(recoverableRequest: recoverableRequest,
errorHandlers: prependRequestRetriers + defaultRequestRetriers + appendRequestRetriers)
await withTaskCancellableClosure {
process(recoverableRequest: recoverableRequest,
prependRequestRetriers: prependRequestRetriers,
appendRequestRetriers: appendRequestRetriers,
completion: $0)
}
}
open func process<B: Encodable, S>(recoverableRequest: EndpointRequest<B, S>,
errorHandlers: [RequestRetrier]) async -> EndpointResponse<S> {
errorHandlers: [RequestRetrier],
completion: @escaping ParameterClosure<EndpointResponse<S>>) -> Cancellable {
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.validateAndRepair(errorResults: failures)
switch handlerResult {
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)
Cancellables.scoped { cancellableBag in
process(request: recoverableRequest) { (result: RequestResult<S, ApiError>) in
if case let .failure(errorResponse) = result {
Self.validateAndRepair(recoverableRequest: recoverableRequest,
errors: [errorResponse],
retriers: errorHandlers,
cancellableBag: cancellableBag,
completion: completion)
} else {
completion(result.mapError { .init(failures: [$0]) })
}
}
}
}
return .failure(.init(failures: failures))
@available(iOS 13.0.0, *)
open func process<B: Encodable, S>(recoverableRequest: EndpointRequest<B, S>,
errorHandlers: [RequestRetrier]) async -> EndpointResponse<S> {
await withTaskCancellableClosure {
process(recoverableRequest: recoverableRequest,
errorHandlers: errorHandlers,
completion: $0)
}
}
private static func validateAndRepair<B, S, R: Collection>(recoverableRequest: EndpointRequest<B, S>,
errors: [ErrorType],
retriers: R,
cancellableBag: BaseCancellableBag,
completion: @escaping ParameterClosure<EndpointResponse<S>>)
where R.Element == RequestRetrier {
guard let retrier = retriers.first, !cancellableBag.isCancelled else {
completion(.failure(.init(failures: errors)))
return
}
return result.mapError { .init(failures: [$0]) }
retrier.validateAndRepair(errorResults: errors) { handlerResult in
switch handlerResult {
case let .success(retryResult):
switch retryResult {
case .retry, .retryWithDelay:
validateAndRepair(recoverableRequest: recoverableRequest,
errors: errors,
retriers: retriers,
cancellableBag: cancellableBag,
completion: completion)
case .doNotRetry, .doNotRetryWithError:
validateAndRepair(recoverableRequest: recoverableRequest,
errors: errors,
retriers: retriers.dropFirst(),
cancellableBag: cancellableBag,
completion: completion)
}
case let .failure(error):
validateAndRepair(recoverableRequest: recoverableRequest,
errors: errors + [error],
retriers: retriers.dropFirst(),
cancellableBag: cancellableBag,
completion: completion)
}
}
.add(to: cancellableBag)
}
public func register<RequestRetrier: EndpointRequestRetrier>(defaultRequestRetrier: RequestRetrier)

View File

@ -36,13 +36,24 @@ public protocol ApiInteractor {
completion: @escaping ParameterClosure<R>) -> Cancellable
}
public extension ApiInteractor {
func process<B: Encodable, S, F>(request: EndpointRequest<B, S>,
completion: @escaping ParameterClosure<RequestResult<S, F>>) -> Cancellable {
process(request: request,
mapSuccess: Result.success,
mapFailure: { .failure(.apiError($0.apiError, $0.statusCode)) },
mapNetworkError: { .failure(.networkError($0)) },
completion: completion)
}
}
@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.apiError, $0.statusCode)) },
mapNetworkError: { .failure(.networkError($0)) })
await withTaskCancellableClosure {
process(request: request, completion: $0)
}
}
func process<B: Encodable, S: Decodable, AE: Decodable, R>(request: EndpointRequest<B, S>,
@ -50,13 +61,11 @@ public extension ApiInteractor {
mapFailure: @escaping Closure<FailureMappingInput<AE>, R>,
mapNetworkError: @escaping Closure<NetworkError, R>) async -> R {
await withTaskCancellableClosure { completion in
await withTaskCancellableClosure {
process(request: request,
mapSuccess: mapSuccess,
mapFailure: mapFailure,
mapNetworkError: mapNetworkError) {
completion($0)
}
mapNetworkError: mapNetworkError, completion: $0)
}
}
}

View File

@ -25,17 +25,31 @@ import TIFoundationUtils
open class DefaultSecuritySchemePreprocessor: SecuritySchemePreprocessor {
struct ValueNotProvidedError: Error {}
public typealias ResultProvider = (@escaping (Result<String, Error>) -> Void) -> Cancellable
public typealias ValueProvider = (@escaping (String?) -> Void) -> Cancellable
private let valueProvider: ValueProvider
private let resultProvider: ResultProvider
public init(valueProvider: @escaping ValueProvider) {
self.valueProvider = valueProvider
public init(resultProvider: @escaping ResultProvider) {
self.resultProvider = resultProvider
}
public init(staticValue: String?) {
self.valueProvider = {
public init(valueProvider: @escaping ValueProvider) {
self.resultProvider = { completion in
valueProvider {
guard let value = $0 else {
completion(.failure(ValueNotProvidedError()))
return
}
completion(.success(value))
}
}
}
public convenience init(staticValue: String?) {
self.init {
$0(staticValue)
return Cancellables.nonCancellable()
}
}
@ -45,44 +59,53 @@ open class DefaultSecuritySchemePreprocessor: SecuritySchemePreprocessor {
public func preprocess<B, S>(request: EndpointRequest<B, S>,
using security: SecurityScheme,
completion: @escaping (Result<EndpointRequest<B, S>, Error>) -> Void) -> Cancellable {
resultProvider {
switch $0 {
case let .success(value):
completion(.success(Self.modify(request: request,
using: security,
securityValue: value)))
case let .failure(error):
completion(.failure(error))
}
}
}
private static func modify<B, S>(request: EndpointRequest<B, S>,
using security: SecurityScheme,
securityValue: String) -> EndpointRequest<B, S> {
var modifiedRequest = request
return valueProvider {
guard let value = $0 else {
completion(.failure(ValueNotProvidedError()))
return
}
switch security {
case let .http(authenticationScheme):
let headerValue = "\(authenticationScheme.rawValue) \(securityValue)"
var headerParameters = modifiedRequest.headerParameters ?? [:]
headerParameters.updateValue(.init(value: headerValue),
forKey: "Authorization")
switch security {
case let .http(authenticationScheme):
let headerValue = "\(authenticationScheme.rawValue) \(value)"
modifiedRequest.headerParameters = headerParameters
case let .apiKey(parameterLocation, parameterName):
switch parameterLocation {
case .header:
var headerParameters = modifiedRequest.headerParameters ?? [:]
headerParameters.updateValue(.init(value: headerValue),
forKey: "Authorization")
headerParameters.updateValue(.init(value: securityValue),
forKey: parameterName)
modifiedRequest.headerParameters = headerParameters
case let .apiKey(parameterLocation, parameterName):
switch parameterLocation {
case .header:
var headerParameters = modifiedRequest.headerParameters ?? [:]
headerParameters.updateValue(.init(value: value),
forKey: parameterName)
case .query:
modifiedRequest.queryParameters.updateValue(.init(value: securityValue),
forKey: parameterName)
modifiedRequest.headerParameters = headerParameters
case .query:
modifiedRequest.queryParameters.updateValue(.init(value: value),
forKey: parameterName)
case .cookie:
modifiedRequest.cookieParameters.updateValue(.init(value: value),
forKey: parameterName)
}
case .cookie:
modifiedRequest.cookieParameters.updateValue(.init(value: securityValue),
forKey: parameterName)
}
completion(.success(modifiedRequest))
}
return modifiedRequest
}
}

View File

@ -24,6 +24,7 @@ import TISwiftUtils
import UIKit
/// A context from where the alert can be presented.
@MainActor
public protocol AlertPresentationContext {
func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: VoidClosure?)
}