From 23c17c9d85d3d23591d84d48343838e1609c98fe Mon Sep 17 00:00:00 2001 From: Ivan Smolin Date: Fri, 9 Jun 2023 10:00:12 +0300 Subject: [PATCH 1/5] feat: Added flatMap operator for AsyncOperation CodableKeyValueStorage now returns Swift.Result with typed errors. SingleValueExpirationStorage for time aware entries (expirable api tokens, etc.) AsyncOperation variants of process methods in NetworkServices. --- .../Sources/AsyncOperation+FlatMap.swift | 88 +++++++++++++++++++ .../Sources/AsyncOperation+Just.swift | 45 ++++++++++ .../Sources/AsyncOperation+Map.swift | 6 +- .../Sources/AsyncOperation+Observe.swift | 61 +++++++++++-- .../CodableKeyValueStorage+BackingStore.swift | 6 +- .../Sources/CodableKeyValueStorage.swift | 22 ++--- .../Decoders/CodableKeyValueDecoder.swift | 2 +- .../Decoders/JSONKeyValueDecoder.swift | 11 +-- .../Decoders/UnarchiverKeyValueDecoder.swift | 8 +- .../Encoders/ArchiverKeyValueEncoder.swift | 6 +- .../Encoders/CodableKeyValueEncoder.swift | 2 +- .../Encoders/JSONKeyValueEncoder.swift | 11 +-- .../Sources/StorageError.swift | 10 +++ .../UserDefaults+CodableKeyValueStorage.swift | 29 +++--- .../AnySingleValueStorage.swift | 0 .../BaseSingleValueDefaultsStorage.swift | 0 .../BaseSingleValueStorage.swift | 0 .../SingleValueCodableDefaultsStorage.swift | 54 ++++++++++++ .../StringValueDefaultsStorage.swift | 0 ...AppInstallLifetimeSingleValueStorage.swift | 0 .../SingleValueExpirationStorage.swift | 76 ++++++++++++++++ .../Keychain+CodableKeyValueStorage.swift | 59 ++++++++----- ...DefaultRecoverableJsonNetworkService.swift | 60 ++++++++++--- .../Sources/ApiInteractor/ApiInteractor.swift | 11 +++ .../ApiInteractor/EndpointErrorResult.swift | 20 +++++ .../ApiInteractor/ErrorCollection.swift | 8 ++ 26 files changed, 506 insertions(+), 89 deletions(-) create mode 100644 TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+FlatMap.swift create mode 100644 TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Just.swift rename TIFoundationUtils/DataStorage/Sources/SingleValueStorage/{ => Implementations}/AnySingleValueStorage.swift (100%) rename TIFoundationUtils/DataStorage/Sources/SingleValueStorage/{ => Implementations}/BaseSingleValueDefaultsStorage.swift (100%) rename TIFoundationUtils/DataStorage/Sources/SingleValueStorage/{ => Implementations}/BaseSingleValueStorage.swift (100%) create mode 100644 TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/SingleValueCodableDefaultsStorage.swift rename TIFoundationUtils/DataStorage/Sources/SingleValueStorage/{ => Implementations}/StringValueDefaultsStorage.swift (100%) rename TIFoundationUtils/DataStorage/Sources/SingleValueStorage/{ => Wrappers}/AppInstallLifetimeSingleValueStorage.swift (100%) create mode 100644 TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Wrappers/SingleValueExpirationStorage.swift diff --git a/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+FlatMap.swift b/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+FlatMap.swift new file mode 100644 index 00000000..92398828 --- /dev/null +++ b/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+FlatMap.swift @@ -0,0 +1,88 @@ +// +// 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 + +private final class FlatMapAsyncOperation: AsyncOperation { + private var dependencyObservation: NSKeyValueObservation? + private var flatMapObservation: NSKeyValueObservation? + private let dependency: Operation + private var flatMapDependency: AsyncOperation? + + init(dependency: AsyncOperation, + flatMapOutput: ((DependencyOutput) -> AsyncOperation)?, + flatMapFailure: ((DependencyFailure) -> AsyncOperation)?) + where DependencyFailure: Error { + + self.dependency = dependency + + super.init() + + dependency.cancelOnCancellation(of: self) + + dependencyObservation = dependency.subscribe { [weak self] in + switch $0 { + case let .success(success): + self?.flatMapDependency = flatMapOutput?(success) + + case let .failure(error): + self?.flatMapDependency = flatMapFailure?(error) + } + + if let self { + self.flatMapDependency?.cancelOnCancellation(of: self) + } + + self?.flatMapObservation = self?.flatMapDependency?.subscribe { + self?.handle(result: $0) + } + + self?.flatMapDependency?.start() + } + + state = .isReady + } + + override func start() { + state = .isExecuting + + dependency.start() + } +} + +public extension AsyncOperation { + func flatMap(_ transform: @escaping (Output) -> AsyncOperation) + -> AsyncOperation { + + FlatMapAsyncOperation(dependency: self, + flatMapOutput: transform, + flatMapFailure: { .just(failure: $0) }) + } + + func flatMapError(_ transform: @escaping (Failure) -> AsyncOperation) + -> AsyncOperation { + + FlatMapAsyncOperation(dependency: self, + flatMapOutput: { .just(success: $0) }, + flatMapFailure: transform) + } +} diff --git a/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Just.swift b/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Just.swift new file mode 100644 index 00000000..ed52f197 --- /dev/null +++ b/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Just.swift @@ -0,0 +1,45 @@ +// +// 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 + +private final class JustAsyncOperation: AsyncOperation { + init(result: Result) { + super.init() + + self.result = result + } +} + +public extension AsyncOperation { + static func just(result: Result) -> AsyncOperation { + JustAsyncOperation(result: result) + } + + static func just(success: Output) -> AsyncOperation { + just(result: .success(success)) + } + + static func just(failure: Failure) -> AsyncOperation { + just(result: .failure(failure)) + } +} diff --git a/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Map.swift b/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Map.swift index 788e4547..3e6ebb30 100644 --- a/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Map.swift +++ b/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Map.swift @@ -23,9 +23,9 @@ import Foundation private final class MapAsyncOperation: DependendAsyncOperation { - public init(dependency: AsyncOperation, - mapOutput: @escaping (DependencyOutput) -> Result, - mapFailure: @escaping (DependencyFailure) -> Failure) { + init(dependency: AsyncOperation, + mapOutput: @escaping (DependencyOutput) -> Result, + mapFailure: @escaping (DependencyFailure) -> Failure) { super.init(dependency: dependency) { $0.mapError(mapFailure).flatMap(mapOutput) diff --git a/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Observe.swift b/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Observe.swift index 123fda8c..3b94c89a 100644 --- a/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Observe.swift +++ b/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Observe.swift @@ -22,21 +22,33 @@ import Foundation -private final class ClosureObserverOperation: DependendAsyncOperation { - public typealias OnResultClosure = (Result) -> Void +class BaseClosureObserverOperation: DependendAsyncOperation { + typealias OnResultClosure = ResultClosureType - private let onResult: OnResultClosure? - private let callbackQueue: DispatchQueue + let onResult: OnResultClosure? - public init(dependency: AsyncOperation, - onResult: OnResultClosure? = nil, - callbackQueue: DispatchQueue = .main) { + init(dependency: AsyncOperation, + onResult: OnResultClosure? = nil) { self.onResult = onResult - self.callbackQueue = callbackQueue super.init(dependency: dependency) { $0 } } +} + +private final class ClosureObserverOperation: + BaseClosureObserverOperation) -> Void> { + + private let callbackQueue: DispatchQueue + + init(dependency: AsyncOperation, + onResult: OnResultClosure? = nil, + callbackQueue: DispatchQueue = .main) { + + self.callbackQueue = callbackQueue + + super.init(dependency: dependency, onResult: onResult) + } override func handle(result: Result) { self.result = result @@ -48,6 +60,19 @@ private final class ClosureObserverOperation: DependendA } } +private final class UIClosureObserverOperation: + BaseClosureObserverOperation) -> Void> { + + override func handle(result: Result) { + self.result = result + + DispatchQueue.main.async { + self.onResult?(result) + self.state = .isFinished + } + } +} + public extension AsyncOperation { func observe(onResult: ((Result) -> Void)? = nil, callbackQueue: DispatchQueue = .main) -> AsyncOperation { @@ -57,6 +82,26 @@ public extension AsyncOperation { callbackQueue: callbackQueue) } + func observe(onResult: (@MainActor (Result) -> Void)? = nil) -> AsyncOperation { + UIClosureObserverOperation(dependency: self, onResult: onResult) + } + + func observe(onSuccess: (@MainActor (Output) -> Void)? = nil, + onFailure: (@MainActor (Failure) -> Void)? = nil) -> AsyncOperation { + + let onResult: UIClosureObserverOperation.OnResultClosure = { + switch $0 { + case let .success(output): + onSuccess?(output) + + case let .failure(error): + onFailure?(error) + } + } + + return observe(onResult: onResult) + } + func observe(onSuccess: ((Output) -> Void)? = nil, onFailure: ((Failure) -> Void)? = nil, callbackQueue: DispatchQueue = .main) -> AsyncOperation { diff --git a/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyValueStorage+BackingStore.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyValueStorage+BackingStore.swift index 9824afbc..c6652b50 100644 --- a/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyValueStorage+BackingStore.swift +++ b/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyValueStorage+BackingStore.swift @@ -32,8 +32,8 @@ public extension BackingStore where Store: CodableKeyValueStorage, StoreContent: where StoreContent == Value? { self.init(store: codableKeyValueStorage, - getClosure: { try? $0.codableObject(forKey: key, decoder: decoder) }, - setClosure: { try? $0.setOrRemove(codableObject: $1, forKey: key, encoder: encoder) }) + getClosure: { try? $0.codableObject(forKey: key, decoder: decoder).get() }, + setClosure: { try? $0.setOrRemove(codableObject: $1, forKey: key, encoder: encoder).get() }) } init(wrappedValue: StoreContent, @@ -44,6 +44,6 @@ public extension BackingStore where Store: CodableKeyValueStorage, StoreContent: self.init(store: codableKeyValueStorage, getClosure: { $0.codableObject(forKey: key, defaultValue: wrappedValue, decoder: decoder) }, - setClosure: { try? $0.setOrRemove(codableObject: $1, forKey: key, encoder: encoder) }) + setClosure: { try? $0.setOrRemove(codableObject: $1, forKey: key, encoder: encoder).get() }) } } diff --git a/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyValueStorage.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyValueStorage.swift index f1e4f8d4..dab8b06a 100644 --- a/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyValueStorage.swift +++ b/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyValueStorage.swift @@ -31,7 +31,7 @@ public protocol CodableKeyValueStorage { /// or throw exception if the key was not found. /// - Throws: CodableStorageError func codableObject(forKey key: StorageKey, - decoder: CodableKeyValueDecoder) throws -> Value + decoder: CodableKeyValueDecoder) -> Result /// Set or remove the value of the specified key in the storage. /// - Parameters: @@ -41,12 +41,14 @@ public protocol CodableKeyValueStorage { /// - Throws: EncodingError if error is occured during passed object encoding. func set(encodableObject: Value, forKey key: StorageKey, - encoder: CodableKeyValueEncoder) throws + encoder: CodableKeyValueEncoder) -> Result /// Removes value for specific key /// - Parameter key: The key with which to associate with the value. /// - Throws: EncodingError if error is occured during reading/writing. - func removeCodableValue(forKey key: StorageKey) throws + func removeCodableValue(forKey key: StorageKey) -> Result + + func hasCodableValue(forKey key: StorageKey) -> Result } public extension CodableKeyValueStorage { @@ -63,17 +65,17 @@ public extension CodableKeyValueStorage { defaultValue: Value, decoder: CodableKeyValueDecoder = JSONKeyValueDecoder()) -> Value { - (try? codableObject(forKey: key, decoder: decoder)) ?? defaultValue + (try? codableObject(forKey: key, decoder: decoder).get()) ?? defaultValue } func setOrRemove(codableObject: Value?, forKey key: StorageKey, - encoder: CodableKeyValueEncoder = JSONKeyValueEncoder()) throws { + encoder: CodableKeyValueEncoder = JSONKeyValueEncoder()) -> Result { - if let codableObject = codableObject { - try set(encodableObject: codableObject, forKey: key, encoder: encoder) + if let codableObject { + return set(encodableObject: codableObject, forKey: key, encoder: encoder) } else { - try? removeCodableValue(forKey: key) + return removeCodableValue(forKey: key) } } @@ -82,10 +84,10 @@ public extension CodableKeyValueStorage { encoder: CodableKeyValueEncoder = JSONKeyValueEncoder()) -> Value? { get { - try? codableObject(forKey: key, decoder: decoder) + try? codableObject(forKey: key, decoder: decoder).get() } set { - try? setOrRemove(codableObject: newValue, forKey: key, encoder: encoder) + try? setOrRemove(codableObject: newValue, forKey: key, encoder: encoder).get() } } } diff --git a/TIFoundationUtils/CodableKeyValueStorage/Sources/Decoders/CodableKeyValueDecoder.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/Decoders/CodableKeyValueDecoder.swift index 54e4cf99..1579f29b 100644 --- a/TIFoundationUtils/CodableKeyValueStorage/Sources/Decoders/CodableKeyValueDecoder.swift +++ b/TIFoundationUtils/CodableKeyValueStorage/Sources/Decoders/CodableKeyValueDecoder.swift @@ -23,5 +23,5 @@ import Foundation public protocol CodableKeyValueDecoder { - func decodeDecodable(from data: Data, for key: StorageKey) throws -> Value + func decodeDecodable(from data: Data, for key: StorageKey) -> Result } diff --git a/TIFoundationUtils/CodableKeyValueStorage/Sources/Decoders/JSONKeyValueDecoder.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/Decoders/JSONKeyValueDecoder.swift index 406f89b9..7cf759a1 100644 --- a/TIFoundationUtils/CodableKeyValueStorage/Sources/Decoders/JSONKeyValueDecoder.swift +++ b/TIFoundationUtils/CodableKeyValueStorage/Sources/Decoders/JSONKeyValueDecoder.swift @@ -29,11 +29,12 @@ open class JSONKeyValueDecoder: CodableKeyValueDecoder { self.jsonDecoder = jsonDecoder } - open func decodeDecodable(from data: Data, for key: StorageKey) throws -> Value { - do { - return try jsonDecoder.decode(Value.self, from: data) - } catch { - throw StorageError.unableToDecode(underlyingError: error) + open func decodeDecodable(from data: Data, for key: StorageKey) -> Result { + Result { + try jsonDecoder.decode(Value.self, from: data) + } + .mapError { + .unableToDecode(underlyingError: $0) } } } diff --git a/TIFoundationUtils/CodableKeyValueStorage/Sources/Decoders/UnarchiverKeyValueDecoder.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/Decoders/UnarchiverKeyValueDecoder.swift index afd4a96e..144f629f 100644 --- a/TIFoundationUtils/CodableKeyValueStorage/Sources/Decoders/UnarchiverKeyValueDecoder.swift +++ b/TIFoundationUtils/CodableKeyValueStorage/Sources/Decoders/UnarchiverKeyValueDecoder.swift @@ -26,13 +26,13 @@ import Foundation open class UnarchiverKeyValueDecoder: CodableKeyValueDecoder { public init() {} - open func decodeDecodable(from data: Data, for key: StorageKey) throws -> Value { + open func decodeDecodable(from data: Data, for key: StorageKey) -> Result { let unarchiver: NSKeyedUnarchiver do { unarchiver = try NSKeyedUnarchiver(forReadingFrom: data) } catch { - throw StorageError.unableToDecode(underlyingError: error) + return .failure(.unableToDecode(underlyingError: error)) } defer { @@ -40,9 +40,9 @@ open class UnarchiverKeyValueDecoder: CodableKeyValueDecoder { } guard let decodableObject = unarchiver.decodeDecodable(Value.self, forKey: key.rawValue) else { - throw StorageError.valueNotFound + return .failure(.valueNotFound) } - return decodableObject + return .success(decodableObject) } } diff --git a/TIFoundationUtils/CodableKeyValueStorage/Sources/Encoders/ArchiverKeyValueEncoder.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/Encoders/ArchiverKeyValueEncoder.swift index 30c916fd..f044f49d 100644 --- a/TIFoundationUtils/CodableKeyValueStorage/Sources/Encoders/ArchiverKeyValueEncoder.swift +++ b/TIFoundationUtils/CodableKeyValueStorage/Sources/Encoders/ArchiverKeyValueEncoder.swift @@ -26,17 +26,17 @@ import Foundation open class ArchiverKeyValueEncoder: CodableKeyValueEncoder { public init() {} - open func encodeEncodable(value: Value, for key: StorageKey) throws -> Data { + open func encodeEncodable(value: Value, for key: StorageKey) -> Result { let archiver = NSKeyedArchiver(requiringSecureCoding: true) do { try archiver.encodeEncodable(value, forKey: key.rawValue) } catch { - throw StorageError.unableToEncode(underlyingError: error) + return .failure(.unableToEncode(underlyingError: error)) } archiver.finishEncoding() - return archiver.encodedData + return .success(archiver.encodedData) } } diff --git a/TIFoundationUtils/CodableKeyValueStorage/Sources/Encoders/CodableKeyValueEncoder.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/Encoders/CodableKeyValueEncoder.swift index 30bf3fb0..45855a19 100644 --- a/TIFoundationUtils/CodableKeyValueStorage/Sources/Encoders/CodableKeyValueEncoder.swift +++ b/TIFoundationUtils/CodableKeyValueStorage/Sources/Encoders/CodableKeyValueEncoder.swift @@ -23,5 +23,5 @@ import Foundation public protocol CodableKeyValueEncoder { - func encodeEncodable(value: Value, for key: StorageKey) throws -> Data + func encodeEncodable(value: Value, for key: StorageKey) -> Result } diff --git a/TIFoundationUtils/CodableKeyValueStorage/Sources/Encoders/JSONKeyValueEncoder.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/Encoders/JSONKeyValueEncoder.swift index 0aacb1ae..5402ca23 100644 --- a/TIFoundationUtils/CodableKeyValueStorage/Sources/Encoders/JSONKeyValueEncoder.swift +++ b/TIFoundationUtils/CodableKeyValueStorage/Sources/Encoders/JSONKeyValueEncoder.swift @@ -29,11 +29,12 @@ open class JSONKeyValueEncoder: CodableKeyValueEncoder { self.jsonEncoder = jsonEncoder } - open func encodeEncodable(value: Value, for key: StorageKey) throws -> Data { - do { - return try jsonEncoder.encode(value) - } catch { - throw StorageError.unableToEncode(underlyingError: error) + open func encodeEncodable(value: Value, for key: StorageKey) -> Result { + Result { + try jsonEncoder.encode(value) + } + .mapError { + .unableToEncode(underlyingError: $0) } } } diff --git a/TIFoundationUtils/CodableKeyValueStorage/Sources/StorageError.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/StorageError.swift index 4e2617fd..dea21806 100644 --- a/TIFoundationUtils/CodableKeyValueStorage/Sources/StorageError.swift +++ b/TIFoundationUtils/CodableKeyValueStorage/Sources/StorageError.swift @@ -27,3 +27,13 @@ public enum StorageError: Error { case unableToEncode(underlyingError: Error) case unableToWriteData(underlyingError: Error) } + +public extension StorageError { + var isValueNotFound: Bool { + if case .valueNotFound = self { + return true + } else { + return false + } + } +} diff --git a/TIFoundationUtils/CodableKeyValueStorage/Sources/UserDefaults/UserDefaults+CodableKeyValueStorage.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/UserDefaults/UserDefaults+CodableKeyValueStorage.swift index e3a3ac64..0f8dcec6 100644 --- a/TIFoundationUtils/CodableKeyValueStorage/Sources/UserDefaults/UserDefaults+CodableKeyValueStorage.swift +++ b/TIFoundationUtils/CodableKeyValueStorage/Sources/UserDefaults/UserDefaults+CodableKeyValueStorage.swift @@ -24,29 +24,38 @@ import Foundation extension UserDefaults: CodableKeyValueStorage { public func codableObject(forKey key: StorageKey, - decoder: CodableKeyValueDecoder) throws -> Value { + decoder: CodableKeyValueDecoder) -> Result { guard let storedData = data(forKey: key.rawValue) else { - throw StorageError.valueNotFound + return .failure(.valueNotFound) } - return try decoder.decodeDecodable(from: storedData, for: key) + return decoder.decodeDecodable(from: storedData, for: key) } public func set(encodableObject: Value, forKey key: StorageKey, - encoder: CodableKeyValueEncoder) throws { + encoder: CodableKeyValueEncoder) -> Result { - let encodedData = try encoder.encodeEncodable(value: encodableObject, for: key) - - set(encodedData, forKey: key.rawValue) + encoder.encodeEncodable(value: encodableObject, for: key) + .map { + set($0, forKey: key.rawValue) + } } - public func removeCodableValue(forKey key: StorageKey) throws { + public func removeCodableValue(forKey key: StorageKey) -> Result { guard data(forKey: key.rawValue) != nil else { - throw StorageError.valueNotFound + return .failure(.valueNotFound) } - removeObject(forKey: key.rawValue) + return .success(removeObject(forKey: key.rawValue)) + } + + public func hasCodableValue(forKey key: StorageKey) -> Result { + guard data(forKey: key.rawValue) != nil else { + return .success(false) + } + + return .success(true) } } diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/AnySingleValueStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/AnySingleValueStorage.swift similarity index 100% rename from TIFoundationUtils/DataStorage/Sources/SingleValueStorage/AnySingleValueStorage.swift rename to TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/AnySingleValueStorage.swift diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/BaseSingleValueDefaultsStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/BaseSingleValueDefaultsStorage.swift similarity index 100% rename from TIFoundationUtils/DataStorage/Sources/SingleValueStorage/BaseSingleValueDefaultsStorage.swift rename to TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/BaseSingleValueDefaultsStorage.swift diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/BaseSingleValueStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/BaseSingleValueStorage.swift similarity index 100% rename from TIFoundationUtils/DataStorage/Sources/SingleValueStorage/BaseSingleValueStorage.swift rename to TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/BaseSingleValueStorage.swift diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/SingleValueCodableDefaultsStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/SingleValueCodableDefaultsStorage.swift new file mode 100644 index 00000000..2d9d2fc4 --- /dev/null +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/SingleValueCodableDefaultsStorage.swift @@ -0,0 +1,54 @@ +// +// 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 SingleValueCodableDefaultsStorage: BaseSingleValueStorage { + public init(defaults: UserDefaults, + storageKey: StorageKey, + decoder: CodableKeyValueDecoder, + encoder: CodableKeyValueEncoder) { + + let getValueClosure: GetValueClosure = { + $0.codableObject(forKey: $1, decoder: decoder) + } + + let storeValueClosure: StoreValueClosure = { + $0.set(encodableObject: $1, forKey: $2, encoder: encoder) + } + + let hasValueClosure: HasValueClosure = { + $0.hasCodableValue(forKey: $1) + } + + let deleteValueClosure: DeleteValueClosure = { + $0.removeCodableValue(forKey: $1) + } + + super.init(storage: defaults, + storageKey: storageKey, + hasValueClosure: hasValueClosure, + deleteValueClosure: deleteValueClosure, + getValueClosure: getValueClosure, + storeValueClosure: storeValueClosure) + } +} diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/StringValueDefaultsStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/StringValueDefaultsStorage.swift similarity index 100% rename from TIFoundationUtils/DataStorage/Sources/SingleValueStorage/StringValueDefaultsStorage.swift rename to TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/StringValueDefaultsStorage.swift diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/AppInstallLifetimeSingleValueStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Wrappers/AppInstallLifetimeSingleValueStorage.swift similarity index 100% rename from TIFoundationUtils/DataStorage/Sources/SingleValueStorage/AppInstallLifetimeSingleValueStorage.swift rename to TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Wrappers/AppInstallLifetimeSingleValueStorage.swift diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Wrappers/SingleValueExpirationStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Wrappers/SingleValueExpirationStorage.swift new file mode 100644 index 00000000..de314627 --- /dev/null +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Wrappers/SingleValueExpirationStorage.swift @@ -0,0 +1,76 @@ +// +// 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 SingleValueExpirationStorage: SingleValueStorage +where Storage.ErrorType == StorageError { + + public typealias ExpirationCheckClosure = (ValueType) -> Bool + + public let isExpiredCheckClosure: ExpirationCheckClosure + public let wrappedStorage: Storage + + public init(wrappedStorage: Storage, isExpiredCheckClosure: @escaping ExpirationCheckClosure) { + self.wrappedStorage = wrappedStorage + self.isExpiredCheckClosure = isExpiredCheckClosure + } + + open func hasStoredValue() -> Bool { + guard wrappedStorage.hasStoredValue() else { + return false + } + + switch getValue() { + case .success: + return true + + case .failure: + return false + } + } + + open func store(value: Storage.ValueType) -> Result { + wrappedStorage.store(value: value) + } + + open func getValue() -> Result { + wrappedStorage.getValue() + .flatMap { + if isExpiredCheckClosure($0) { + return .failure(.valueNotFound) + } else { + return .success($0) + } + } + } + + open func deleteValue() -> Result { + wrappedStorage.deleteValue() + } +} + +public extension SingleValueStorage where ErrorType == StorageError { + typealias ExpirationStorage = SingleValueExpirationStorage + + func isExpireCheck(_ isExpiredCheck: @escaping ExpirationStorage.ExpirationCheckClosure) -> ExpirationStorage { + ExpirationStorage(wrappedStorage: self, isExpiredCheckClosure: isExpiredCheck) + } +} diff --git a/TIKeychainUtils/Sources/KeychainCodableBackingStore/Keychain+CodableKeyValueStorage.swift b/TIKeychainUtils/Sources/KeychainCodableBackingStore/Keychain+CodableKeyValueStorage.swift index 9e66246c..647287b9 100644 --- a/TIKeychainUtils/Sources/KeychainCodableBackingStore/Keychain+CodableKeyValueStorage.swift +++ b/TIKeychainUtils/Sources/KeychainCodableBackingStore/Keychain+CodableKeyValueStorage.swift @@ -28,41 +28,56 @@ public typealias KeychainCodableBackingStore = CodableKeyValueBackin extension Keychain: CodableKeyValueStorage { public func codableObject(forKey key: StorageKey, - decoder: CodableKeyValueDecoder) throws -> Value { + decoder: CodableKeyValueDecoder) -> Result { - let unwrappedStoredData: Data? - - do { - unwrappedStoredData = try getData(key.rawValue) - } catch { - throw StorageError.unableToExtractData(underlyingError: error) + Result { + try getData(key.rawValue) } - - guard let storedData = unwrappedStoredData else { - throw StorageError.valueNotFound + .mapError { + StorageError.unableToExtractData(underlyingError: $0) } + .flatMap { + guard let storedData = $0 else { + return .failure(.valueNotFound) + } - return try decoder.decodeDecodable(from: storedData, for: key) + return .success(storedData) + } + .flatMap { + decoder.decodeDecodable(from: $0, for: key) + } } public func set(encodableObject: Value, forKey key: StorageKey, - encoder: CodableKeyValueEncoder) throws { + encoder: CodableKeyValueEncoder) -> Result { - let objectData = try encoder.encodeEncodable(value: encodableObject, for: key) + encoder.encodeEncodable(value: encodableObject, for: key) + .flatMap { encodedData in + Result { + try set(encodedData, key: key.rawValue) + } + .mapError { + .unableToWriteData(underlyingError: $0) + } + } + } - do { - try set(objectData, key: key.rawValue) - } catch { - throw StorageError.unableToWriteData(underlyingError: error) + public func removeCodableValue(forKey key: StorageKey) -> Result { + Result { + try remove(key.rawValue) + } + .mapError { + .unableToWriteData(underlyingError: $0) } } - public func removeCodableValue(forKey key: StorageKey) throws { - do { - try remove(key.rawValue) - } catch { - throw StorageError.unableToWriteData(underlyingError: error) + public func hasCodableValue(forKey key: StorageKey) -> Result { + Result { + try contains(key.rawValue) + } + .mapError { + .unableToExtractData(underlyingError: $0) } } } diff --git a/TIMoyaNetworking/Sources/RecoverableNetworkService/DefaultRecoverableJsonNetworkService.swift b/TIMoyaNetworking/Sources/RecoverableNetworkService/DefaultRecoverableJsonNetworkService.swift index 96338ebd..ad955e01 100644 --- a/TIMoyaNetworking/Sources/RecoverableNetworkService/DefaultRecoverableJsonNetworkService.swift +++ b/TIMoyaNetworking/Sources/RecoverableNetworkService/DefaultRecoverableJsonNetworkService.swift @@ -43,20 +43,6 @@ open class DefaultRecoverableJsonNetworkService: De completion: completion) } - @available(iOS 13.0.0, *) - open func process(recoverableRequest: EndpointRequest, - prependRequestRetriers: [RequestRetrier] = [], - appendRequestRetriers: [RequestRetrier] = []) async -> - EndpointResponse { - - await withTaskCancellableClosure { - process(recoverableRequest: recoverableRequest, - prependRequestRetriers: prependRequestRetriers, - appendRequestRetriers: appendRequestRetriers, - completion: $0) - } - } - open func process(recoverableRequest: EndpointRequest, errorHandlers: [RequestRetrier], completion: @escaping ParameterClosure>) -> Cancellable { @@ -72,6 +58,32 @@ open class DefaultRecoverableJsonNetworkService: De } } + // MARK: - AsyncOperaton support + + open func process(recoverableRequest: EndpointRequest, + prependRequestRetriers: [RequestRetrier] = [], + appendRequestRetriers: [RequestRetrier] = []) -> AsyncOperation> { + ClosureAsyncOperation { completion in + self.process(recoverableRequest: recoverableRequest, + prependRequestRetriers: prependRequestRetriers, + appendRequestRetriers: appendRequestRetriers) { + completion($0) + } + } + } + + open func process(recoverableRequest: EndpointRequest, + errorHandlers: [RequestRetrier]) -> AsyncOperation> { + ClosureAsyncOperation { completion in + self.process(recoverableRequest: recoverableRequest, + errorHandlers: errorHandlers) { + completion($0) + } + } + } + + // MARK: - Swift concurrency support + @available(iOS 13.0.0, *) open func process(recoverableRequest: EndpointRequest, errorHandlers: [RequestRetrier]) async -> EndpointResponse { @@ -83,6 +95,22 @@ open class DefaultRecoverableJsonNetworkService: De } } + @available(iOS 13.0.0, *) + open func process(recoverableRequest: EndpointRequest, + prependRequestRetriers: [RequestRetrier] = [], + appendRequestRetriers: [RequestRetrier] = []) async -> + EndpointResponse { + + await withTaskCancellableClosure { + process(recoverableRequest: recoverableRequest, + prependRequestRetriers: prependRequestRetriers, + appendRequestRetriers: appendRequestRetriers, + completion: $0) + } + } + + // MARK: - Response handling + open func handle(recoverableResponse: RequestResult, request: EndpointRequest, errorHandlers: [RequestRetrier], @@ -114,6 +142,8 @@ open class DefaultRecoverableJsonNetworkService: De } } + // MARK: - RequestRetriers configuration + public func register(defaultRequestRetrier: RequestRetrier) where RequestRetrier.ErrorResult == ErrorType { @@ -126,6 +156,8 @@ open class DefaultRecoverableJsonNetworkService: De self.defaultRequestRetriers = defaultRequestRetriers.map { $0.asAnyEndpointRequestRetrier() } } + // MARK: - Internal + private static func validateAndRepair(request: EndpointRequest, errors: [ErrorType], retriers: R, diff --git a/TINetworking/Sources/ApiInteractor/ApiInteractor.swift b/TINetworking/Sources/ApiInteractor/ApiInteractor.swift index ffe1c485..3df08d8c 100644 --- a/TINetworking/Sources/ApiInteractor/ApiInteractor.swift +++ b/TINetworking/Sources/ApiInteractor/ApiInteractor.swift @@ -48,6 +48,17 @@ public extension ApiInteractor { } } +public extension ApiInteractor { + func process(request: EndpointRequest) + -> AsyncOperation> { + ClosureAsyncOperation { completion in + self.process(request: request) { + completion($0) + } + } + } +} + @available(iOS 13.0.0, *) public extension ApiInteractor { func process(request: EndpointRequest) async -> RequestResult { diff --git a/TINetworking/Sources/ApiInteractor/EndpointErrorResult.swift b/TINetworking/Sources/ApiInteractor/EndpointErrorResult.swift index 38e746de..fc5fe232 100644 --- a/TINetworking/Sources/ApiInteractor/EndpointErrorResult.swift +++ b/TINetworking/Sources/ApiInteractor/EndpointErrorResult.swift @@ -24,3 +24,23 @@ public enum EndpointErrorResult: Error { case apiError(ApiError, Int) case networkError(NetworkError) } + +public extension EndpointErrorResult { + func matches(_ whereClause: (ApiError, Int) -> Bool) -> Bool { + switch self { + case let .apiError(apiError, statusCode): + return whereClause(apiError, statusCode) + case .networkError: + return false + } + } + + func matches(_ whereClause: (NetworkError) -> Bool) -> Bool { + switch self { + case .apiError: + return false + case let .networkError(networkError): + return whereClause(networkError) + } + } +} diff --git a/TINetworking/Sources/ApiInteractor/ErrorCollection.swift b/TINetworking/Sources/ApiInteractor/ErrorCollection.swift index 05345b87..0cc4a709 100644 --- a/TINetworking/Sources/ApiInteractor/ErrorCollection.swift +++ b/TINetworking/Sources/ApiInteractor/ErrorCollection.swift @@ -38,4 +38,12 @@ public struct ErrorCollection: Error { public func lastOr(_ default: E) -> E { failures.last ?? `default` } + + public func contains(_ whereClause: (ApiError, Int) -> Bool) -> Bool where E == EndpointErrorResult { + failures.contains { $0.matches(whereClause) } + } + + public func contains(_ whereClause: (NetworkError) -> Bool) -> Bool where E == EndpointErrorResult { + failures.contains { $0.matches(whereClause) } + } } -- 2.40.1 From bf613b99e80ecd92dff9f158f0780829171cc5d5 Mon Sep 17 00:00:00 2001 From: Ivan Smolin Date: Fri, 9 Jun 2023 10:14:07 +0300 Subject: [PATCH 2/5] build: update changelog and podspec version, fix playground compile issues --- CHANGELOG.md | 7 +++++ Gemfile.lock | 3 +- TIAppleMapUtils/TIAppleMapUtils.podspec | 2 +- TIAuth/TIAuth.podspec | 2 +- TIDeeplink/TIDeeplink.podspec | 6 ++-- TIDeveloperUtils/TIDeveloperUtils.podspec | 2 +- TIEcommerce/TIEcommerce.podspec | 2 +- TIFoundationUtils/TIFoundationUtils.podspec | 2 +- TIGoogleMapUtils/TIGoogleMapUtils.podspec | 2 +- TIKeychainUtils/TIKeychainUtils.podspec | 2 +- TILogging/TILogging.podspec | 2 +- TIMapUtils/TIMapUtils.podspec | 2 +- TIMoyaNetworking/TIMoyaNetworking.podspec | 2 +- TINetworking/TINetworking.podspec | 2 +- TINetworkingCache/TINetworkingCache.podspec | 2 +- TIPagination/TIPagination.podspec | 2 +- TISwiftUICore/TISwiftUICore.podspec | 2 +- TISwiftUtils/TISwiftUtils.podspec | 2 +- TITableKitUtils/TITableKitUtils.podspec | 2 +- TITextProcessing/TITextProcessing.podspec | 2 +- .../Contents.swift | 4 +-- .../Skeletons.xcplaygroundpage/Contents.swift | 3 +- .../Sources/NefPlaygroundSupport.swift | 30 +++++++++++++++++++ TIUIElements/TIUIElements.podspec | 2 +- TIUIKitCore/TIUIKitCore.podspec | 2 +- TIWebView/TIWebView.podspec | 2 +- TIYandexMapUtils/TIYandexMapUtils.podspec | 2 +- docs/tiuielements/placeholder.md | 4 +-- docs/tiuielements/skeletons.md | 4 +-- 29 files changed, 67 insertions(+), 36 deletions(-) create mode 100644 TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Sources/NefPlaygroundSupport.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 6947e08b..eaa39c1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +### 1.47.0 + +- **Added**: `flatMap` operator for `AsyncOperation` +- **Update**: `CodableKeyValueStorage` now returns `Swift.Result` with typed errors. +- **Added**: `SingleValueExpirationStorage` for time aware entries (expirable api tokens, etc.) +- **Added**: `AsyncOperation` variants of process methods in NetworkServices. + ### 1.46.0 - **Added**: `AsyncSingleValueStorage` and `SingleValueStorageAsyncWrapper` for async access to SingleValue storages wtih swift concurrency support diff --git a/Gemfile.lock b/Gemfile.lock index 3e0e8625..7a59006c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -89,9 +89,10 @@ GEM PLATFORMS x86_64-darwin-20 + x86_64-darwin-21 DEPENDENCIES cocoapods (~> 1.11) BUNDLED WITH - 2.3.10 + 2.3.26 diff --git a/TIAppleMapUtils/TIAppleMapUtils.podspec b/TIAppleMapUtils/TIAppleMapUtils.podspec index e9d52951..37f275b2 100644 --- a/TIAppleMapUtils/TIAppleMapUtils.podspec +++ b/TIAppleMapUtils/TIAppleMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIAppleMapUtils' - s.version = '1.46.0' + s.version = '1.47.0' s.summary = 'Set of helpers for map objects clustering and interacting using Apple MapKit.' s.homepage = 'https://git.svc.touchin.ru/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 fccbffab..3a4416da 100644 --- a/TIAuth/TIAuth.podspec +++ b/TIAuth/TIAuth.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIAuth' - s.version = '1.46.0' + s.version = '1.47.0' s.summary = 'Login, registration, confirmation and other related actions' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIDeeplink/TIDeeplink.podspec b/TIDeeplink/TIDeeplink.podspec index 14e443fe..b78e4656 100644 --- a/TIDeeplink/TIDeeplink.podspec +++ b/TIDeeplink/TIDeeplink.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIDeeplink' - s.version = '1.46.0' + s.version = '1.47.0' s.summary = 'Deeplink service API' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } @@ -11,12 +11,12 @@ Pod::Spec.new do |s| s.ios.deployment_target = '11.0' s.swift_versions = ['5.7'] - sources = '/Sources/**/*' + sources = 'Sources/**/*' if ENV["DEVELOPMENT_INSTALL"] # installing using :path => s.source_files = sources s.exclude_files = s.name + '.app' else - s.source_files = s.name + sources + s.source_files = s.name + '/' + sources s.exclude_files = s.name + '/*.app' end diff --git a/TIDeveloperUtils/TIDeveloperUtils.podspec b/TIDeveloperUtils/TIDeveloperUtils.podspec index 325b91ec..1fd97b1f 100644 --- a/TIDeveloperUtils/TIDeveloperUtils.podspec +++ b/TIDeveloperUtils/TIDeveloperUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIDeveloperUtils' - s.version = '1.46.0' + s.version = '1.47.0' s.summary = 'Universal web view API' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIEcommerce/TIEcommerce.podspec b/TIEcommerce/TIEcommerce.podspec index a4726845..c22841c2 100644 --- a/TIEcommerce/TIEcommerce.podspec +++ b/TIEcommerce/TIEcommerce.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIEcommerce' - s.version = '1.46.0' + s.version = '1.47.0' s.summary = 'Cart, products, promocodes, bonuses and other related actions' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIFoundationUtils/TIFoundationUtils.podspec b/TIFoundationUtils/TIFoundationUtils.podspec index d70cc0cb..5ad8eff1 100644 --- a/TIFoundationUtils/TIFoundationUtils.podspec +++ b/TIFoundationUtils/TIFoundationUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIFoundationUtils' - s.version = '1.46.0' + s.version = '1.47.0' s.summary = 'Set of helpers for Foundation framework classes.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIGoogleMapUtils/TIGoogleMapUtils.podspec b/TIGoogleMapUtils/TIGoogleMapUtils.podspec index 10448203..b5ffdfd9 100644 --- a/TIGoogleMapUtils/TIGoogleMapUtils.podspec +++ b/TIGoogleMapUtils/TIGoogleMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIGoogleMapUtils' - s.version = '1.46.0' + s.version = '1.47.0' s.summary = 'Set of helpers for map objects clustering and interacting using Google Maps SDK.' s.homepage = 'https://git.svc.touchin.ru/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 1e83fbe9..69183289 100644 --- a/TIKeychainUtils/TIKeychainUtils.podspec +++ b/TIKeychainUtils/TIKeychainUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIKeychainUtils' - s.version = '1.46.0' + s.version = '1.47.0' s.summary = 'Set of helpers for Keychain classes.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TILogging/TILogging.podspec b/TILogging/TILogging.podspec index 3d6a2382..87d0c220 100644 --- a/TILogging/TILogging.podspec +++ b/TILogging/TILogging.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TILogging' - s.version = '1.46.0' + s.version = '1.47.0' s.summary = 'Logging for TI libraries.' s.homepage = 'https://git.svc.touchin.ru/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 37c22f9e..7475e4b8 100644 --- a/TIMapUtils/TIMapUtils.podspec +++ b/TIMapUtils/TIMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIMapUtils' - s.version = '1.46.0' + s.version = '1.47.0' s.summary = 'Set of helpers for map objects clustering and interacting.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIMoyaNetworking/TIMoyaNetworking.podspec b/TIMoyaNetworking/TIMoyaNetworking.podspec index a640216c..f2f8afe1 100644 --- a/TIMoyaNetworking/TIMoyaNetworking.podspec +++ b/TIMoyaNetworking/TIMoyaNetworking.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIMoyaNetworking' - s.version = '1.46.0' + s.version = '1.47.0' s.summary = 'Moya + Swagger network service.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TINetworking/TINetworking.podspec b/TINetworking/TINetworking.podspec index 02071b65..ffdf6bec 100644 --- a/TINetworking/TINetworking.podspec +++ b/TINetworking/TINetworking.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TINetworking' - s.version = '1.46.0' + s.version = '1.47.0' s.summary = 'Swagger-frendly networking layer helpers.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TINetworkingCache/TINetworkingCache.podspec b/TINetworkingCache/TINetworkingCache.podspec index de220e82..6d715d3e 100644 --- a/TINetworkingCache/TINetworkingCache.podspec +++ b/TINetworkingCache/TINetworkingCache.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TINetworkingCache' - s.version = '1.46.0' + s.version = '1.47.0' s.summary = 'Caching results of EndpointRequests.' s.homepage = 'https://git.svc.touchin.ru/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 685cf69c..c566dc32 100644 --- a/TIPagination/TIPagination.podspec +++ b/TIPagination/TIPagination.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIPagination' - s.version = '1.46.0' + s.version = '1.47.0' s.summary = 'Generic pagination component.' s.homepage = 'https://git.svc.touchin.ru/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 cdba18ff..1dcada42 100644 --- a/TISwiftUICore/TISwiftUICore.podspec +++ b/TISwiftUICore/TISwiftUICore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TISwiftUICore' - s.version = '1.46.0' + s.version = '1.47.0' s.summary = 'Core UI elements: protocols, views and helpers.' s.homepage = 'https://git.svc.touchin.ru/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 de1784b6..ef389fb6 100644 --- a/TISwiftUtils/TISwiftUtils.podspec +++ b/TISwiftUtils/TISwiftUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TISwiftUtils' - s.version = '1.46.0' + s.version = '1.47.0' s.summary = 'Bunch of useful helpers for Swift development.' s.homepage = 'https://git.svc.touchin.ru/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 250c7a03..01cdfed1 100644 --- a/TITableKitUtils/TITableKitUtils.podspec +++ b/TITableKitUtils/TITableKitUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TITableKitUtils' - s.version = '1.46.0' + s.version = '1.47.0' s.summary = 'Set of helpers for TableKit classes.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TITextProcessing/TITextProcessing.podspec b/TITextProcessing/TITextProcessing.podspec index 3e60037e..752fba5c 100644 --- a/TITextProcessing/TITextProcessing.podspec +++ b/TITextProcessing/TITextProcessing.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TITextProcessing' - s.version = '1.46.0' + s.version = '1.47.0' s.summary = 'A text processing service helping to get a text mask and a placeholder from incoming regex.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/Placeholder.xcplaygroundpage/Contents.swift b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/Placeholder.xcplaygroundpage/Contents.swift index 19eddec8..1e2985d9 100644 --- a/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/Placeholder.xcplaygroundpage/Contents.swift +++ b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/Placeholder.xcplaygroundpage/Contents.swift @@ -265,11 +265,9 @@ enum ErrorType { case unknown } -import PlaygroundSupport - let placeholder = PlaceholderHolderViewController() -PlaygroundPage.current.liveView = placeholder +Nef.Playground.liveView(placeholder) placeholder.configure(with: .internetConnection) diff --git a/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/Skeletons.xcplaygroundpage/Contents.swift b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/Skeletons.xcplaygroundpage/Contents.swift index d2bb6abd..1d0c53ab 100644 --- a/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/Skeletons.xcplaygroundpage/Contents.swift +++ b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/Skeletons.xcplaygroundpage/Contents.swift @@ -316,13 +316,12 @@ extension DefaultPlaceholderImageView: Skeletonable { } //: ## Тестовый сконфигурированный контроллер -import PlaygroundSupport canShowAndHideController.view.frame = .init(origin: .zero, size: .init(width: 250, height: 100)) canShowAndHideController.hideSkeletons() confWithLeftToRightAnim.labelConfiguration = .init(numberOfLines: 2) -PlaygroundPage.current.liveView = canShowAndHideController +Nef.Playground.liveView(canShowAndHideController) canShowAndHideController.showSkeletons(viewsToSkeletons: nil, confWithLeftToRightAnim) diff --git a/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Sources/NefPlaygroundSupport.swift b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Sources/NefPlaygroundSupport.swift new file mode 100644 index 00000000..2ffea80a --- /dev/null +++ b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Sources/NefPlaygroundSupport.swift @@ -0,0 +1,30 @@ +import UIKit + +public protocol NefPlaygroundLiveViewable {} +extension UIView: NefPlaygroundLiveViewable {} +extension UIViewController: NefPlaygroundLiveViewable {} + +#if NOT_IN_PLAYGROUND +public enum Nef { + public enum Playground { + public static func liveView(_ view: NefPlaygroundLiveViewable) {} + public static func needsIndefiniteExecution(_ state: Bool) {} + } +} + +#else +import PlaygroundSupport + +public enum Nef { + public enum Playground { + public static func liveView(_ view: NefPlaygroundLiveViewable) { + PlaygroundPage.current.liveView = (view as! PlaygroundLiveViewable) + } + + public static func needsIndefiniteExecution(_ state: Bool) { + PlaygroundPage.current.needsIndefiniteExecution = state + } + } +} + +#endif diff --git a/TIUIElements/TIUIElements.podspec b/TIUIElements/TIUIElements.podspec index 8bfbd184..30990585 100644 --- a/TIUIElements/TIUIElements.podspec +++ b/TIUIElements/TIUIElements.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIUIElements' - s.version = '1.46.0' + s.version = '1.47.0' s.summary = 'Bunch of useful protocols and views.' s.homepage = 'https://git.svc.touchin.ru/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 b0a5799e..1349a3d2 100644 --- a/TIUIKitCore/TIUIKitCore.podspec +++ b/TIUIKitCore/TIUIKitCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIUIKitCore' - s.version = '1.46.0' + s.version = '1.47.0' s.summary = 'Core UI elements: protocols, views and helpers.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIWebView/TIWebView.podspec b/TIWebView/TIWebView.podspec index 35a86c34..ac10df39 100644 --- a/TIWebView/TIWebView.podspec +++ b/TIWebView/TIWebView.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIWebView' - s.version = '1.46.0' + s.version = '1.47.0' s.summary = 'Universal web view API' s.homepage = 'https://git.svc.touchin.ru/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 10b8eca8..6dfd6024 100644 --- a/TIYandexMapUtils/TIYandexMapUtils.podspec +++ b/TIYandexMapUtils/TIYandexMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIYandexMapUtils' - s.version = '1.46.0' + s.version = '1.47.0' s.summary = 'Set of helpers for map objects clustering and interacting using Yandex Maps SDK.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/docs/tiuielements/placeholder.md b/docs/tiuielements/placeholder.md index b65767a0..aa2b78bb 100644 --- a/docs/tiuielements/placeholder.md +++ b/docs/tiuielements/placeholder.md @@ -281,11 +281,9 @@ enum ErrorType { case unknown } -import PlaygroundSupport - let placeholder = PlaceholderHolderViewController() -PlaygroundPage.current.liveView = placeholder +Nef.Playground.liveView(placeholder) placeholder.configure(with: .internetConnection) ``` diff --git a/docs/tiuielements/skeletons.md b/docs/tiuielements/skeletons.md index a8dad19a..39705b71 100644 --- a/docs/tiuielements/skeletons.md +++ b/docs/tiuielements/skeletons.md @@ -343,14 +343,12 @@ extension DefaultPlaceholderImageView: Skeletonable { ## Тестовый сконфигурированный контроллер ```swift -import PlaygroundSupport - canShowAndHideController.view.frame = .init(origin: .zero, size: .init(width: 250, height: 100)) canShowAndHideController.hideSkeletons() confWithLeftToRightAnim.labelConfiguration = .init(numberOfLines: 2) -PlaygroundPage.current.liveView = canShowAndHideController +Nef.Playground.liveView(canShowAndHideController) canShowAndHideController.showSkeletons(viewsToSkeletons: nil, confWithLeftToRightAnim) ``` -- 2.40.1 From 1be28959bc3d0edc07ebe2db3fff73dcb9785e25 Mon Sep 17 00:00:00 2001 From: Ivan Smolin Date: Fri, 9 Jun 2023 11:45:55 +0300 Subject: [PATCH 3/5] docs: update playground pages for AsyncOperation and SingleValueExpirationStorage --- .../SingleValueCodableDefaultsStorage.swift | 8 ++- .../Contents.swift | 55 +++++++++++++++++- .../Sources/NefPlaygroundSupport.swift | 30 ++++++++++ .../Contents.swift | 43 ++++++++++++++ docs/tifoundationutils/asyncoperation.md | 57 ++++++++++++++++++- docs/tikeychainutils/singlevaluestorage.md | 43 ++++++++++++++ 6 files changed, 229 insertions(+), 7 deletions(-) create mode 100644 TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils.playground/Sources/NefPlaygroundSupport.swift diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/SingleValueCodableDefaultsStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/SingleValueCodableDefaultsStorage.swift index 2d9d2fc4..e0680bd0 100644 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/SingleValueCodableDefaultsStorage.swift +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/SingleValueCodableDefaultsStorage.swift @@ -22,8 +22,10 @@ import Foundation -open class SingleValueCodableDefaultsStorage: BaseSingleValueStorage { - public init(defaults: UserDefaults, +open class DefaultSingleValueCodableStorage: + BaseSingleValueStorage { + + public init(storage: CodableStorage, storageKey: StorageKey, decoder: CodableKeyValueDecoder, encoder: CodableKeyValueEncoder) { @@ -44,7 +46,7 @@ open class SingleValueCodableDefaultsStorage: BaseSingleValu $0.removeCodableValue(forKey: $1) } - super.init(storage: defaults, + super.init(storage: storage, storageKey: storageKey, hasValueClosure: hasValueClosure, deleteValueClosure: deleteValueClosure, diff --git a/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils.playground/Pages/AsyncOperation.xcplaygroundpage/Contents.swift b/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils.playground/Pages/AsyncOperation.xcplaygroundpage/Contents.swift index 37a1769a..119761d9 100644 --- a/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils.playground/Pages/AsyncOperation.xcplaygroundpage/Contents.swift +++ b/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils.playground/Pages/AsyncOperation.xcplaygroundpage/Contents.swift @@ -32,11 +32,12 @@ let intResultOperation = ClosureAsyncOperation { completion in /*: ## Базовые операторы - На данный момент реализовано всего два оператора: + На данный момент реализовано четыре оператора: - `map(mapOutput:mapFailure:)` - конвертирует ResultType в новый NewResultType и ErrorType в новый NewErrorType - `observe(onSuccess:onFailure)` - просто вызывает переданные callback'и при получении результата или ошибки - + - `flatMap(_:)` - подписывается на результат выполнения нового AsyncOperation полученного из closure + - `flatMapError(_:)` - подписывается на результат выполнения нового AsyncOperation полученного из closure */ //: ### Пример запуска асинхронных операци с применением операторов в последовательной очереди и вывод результатов @@ -75,3 +76,53 @@ ClosureAsyncOperation { completion in "Async operation two has finished with Success" ``` */ + +//: ### Пример использования оператора flatMap у AsyncOperaton + +import TISwiftUtils + +extension StorageKey { + static var loyaltyCardNumber: StorageKey { + .init(rawValue: "loyaltyCardNumber") + } +} + +struct CardService { + enum Failure: Error { + case noCardFound + case cardFetchError + } + + private let operationQueue = OperationQueue() + + private let asyncStorage = StringValueDefaultsStorage(defaults: .standard, storageKey: .loyaltyCardNumber) + .async(on: .global()) + + func requestCardTitle(cardNumber: String) -> AsyncOperation { + .just(success: "Supreme card") +// .just(failure: .cardFetchError) + } + + func getSavedCardTitle(completion: @escaping UIParameterClosure>) -> Cancellable { + ClosureAsyncOperation { completion in + asyncStorage.getValue { + completion($0.mapError { _ in + .noCardFound + }) + } + } + .flatMap { + requestCardTitle(cardNumber: $0) + } + .observe(onResult: completion) + .add(to: operationQueue) + } +} + +let cardService = CardService() + +cardService.getSavedCardTitle { result in + debugPrint(result) +} + +Nef.Playground.needsIndefiniteExecution(true) diff --git a/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils.playground/Sources/NefPlaygroundSupport.swift b/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils.playground/Sources/NefPlaygroundSupport.swift new file mode 100644 index 00000000..2ffea80a --- /dev/null +++ b/TIFoundationUtils/TIFoundationUtils.app/Contents/MacOS/TIFoundationUtils.playground/Sources/NefPlaygroundSupport.swift @@ -0,0 +1,30 @@ +import UIKit + +public protocol NefPlaygroundLiveViewable {} +extension UIView: NefPlaygroundLiveViewable {} +extension UIViewController: NefPlaygroundLiveViewable {} + +#if NOT_IN_PLAYGROUND +public enum Nef { + public enum Playground { + public static func liveView(_ view: NefPlaygroundLiveViewable) {} + public static func needsIndefiniteExecution(_ state: Bool) {} + } +} + +#else +import PlaygroundSupport + +public enum Nef { + public enum Playground { + public static func liveView(_ view: NefPlaygroundLiveViewable) { + PlaygroundPage.current.liveView = (view as! PlaygroundLiveViewable) + } + + public static func needsIndefiniteExecution(_ state: Bool) { + PlaygroundPage.current.needsIndefiniteExecution = state + } + } +} + +#endif diff --git a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift index 8808b79f..e1a757c0 100644 --- a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift +++ b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift @@ -71,3 +71,46 @@ if appInstallAwareTokenStorage.hasStoredValue() { // app was reinstalled or token is empty // ... } + +/*: + ### `SingleValueExpirationStorage` + + Класс позволяющий добавить дополнительную функциональность очистки значения по конкретному ключу + если истёк срок действия объекта (например refresh token'а) +*/ + +struct Token: Codable, Equatable { + var value: String + var expiration: Date + + init(value: String, expiration: Date) { + self.value = value + self.expiration = expiration + } +} + +extension StorageKey { + static var accessToken: StorageKey { + .init(rawValue: "accessToken") + } +} + +let accessTokenStorage = DefaultSingleValueCodableStorage(storage: keychain, + storageKey: .accessToken, + decoder: JSONKeyValueDecoder(), + encoder: JSONKeyValueEncoder()) + +let expirationCheckStorage = accessTokenStorage.isExpireCheck { $0.expiration.timeIntervalSinceNow > 0 } + +switch expirationCheckStorage.getValue() { +case let .success(token): + // use token + break +case let .failure(storageError) + if .valueNotFound = storageError { + // token is missing or expired, request new token + } else { + // handle storage error + } + break +} diff --git a/docs/tifoundationutils/asyncoperation.md b/docs/tifoundationutils/asyncoperation.md index 5a2175e1..9af67653 100644 --- a/docs/tifoundationutils/asyncoperation.md +++ b/docs/tifoundationutils/asyncoperation.md @@ -30,11 +30,12 @@ let intResultOperation = ClosureAsyncOperation { completion in ## Базовые операторы - На данный момент реализовано всего два оператора: + На данный момент реализовано четыре оператора: - `map(mapOutput:mapFailure:)` - конвертирует ResultType в новый NewResultType и ErrorType в новый NewErrorType - `observe(onSuccess:onFailure)` - просто вызывает переданные callback'и при получении результата или ошибки - + - `flatMap(_:)` - подписывается на результат выполнения нового AsyncOperation полученного из closure + - `flatMapError(_:)` - подписывается на результат выполнения нового AsyncOperation полученного из closure ### Пример запуска асинхронных операци с применением операторов в последовательной очереди и вывод результатов @@ -72,3 +73,55 @@ ClosureAsyncOperation { completion in "Async operation one has finished with 2" "Async operation two has finished with Success" ``` + +### Пример использования оператора flatMap у AsyncOperaton + +```swift +import TISwiftUtils + +extension StorageKey { + static var loyaltyCardNumber: StorageKey { + .init(rawValue: "loyaltyCardNumber") + } +} + +struct CardService { + enum Failure: Error { + case noCardFound + case cardFetchError + } + + private let operationQueue = OperationQueue() + + private let asyncStorage = StringValueDefaultsStorage(defaults: .standard, storageKey: .loyaltyCardNumber) + .async(on: .global()) + + func requestCardTitle(cardNumber: String) -> AsyncOperation { + .just(success: "Supreme card") +// .just(failure: .cardFetchError) + } + + func getSavedCardTitle(completion: @escaping UIParameterClosure>) -> Cancellable { + ClosureAsyncOperation { completion in + asyncStorage.getValue { + completion($0.mapError { _ in + .noCardFound + }) + } + } + .flatMap { + requestCardTitle(cardNumber: $0) + } + .observe(onResult: completion) + .add(to: operationQueue) + } +} + +let cardService = CardService() + +cardService.getSavedCardTitle { result in + debugPrint(result) +} + +Nef.Playground.needsIndefiniteExecution(true) +``` diff --git a/docs/tikeychainutils/singlevaluestorage.md b/docs/tikeychainutils/singlevaluestorage.md index 8ccfebe9..ac420aca 100644 --- a/docs/tikeychainutils/singlevaluestorage.md +++ b/docs/tikeychainutils/singlevaluestorage.md @@ -70,3 +70,46 @@ if appInstallAwareTokenStorage.hasStoredValue() { // ... } ``` + +### `SingleValueExpirationStorage` + + Класс позволяющий добавить дополнительную функциональность очистки значения по конкретному ключу + если истёк срок действия объекта (например refresh token'а) + +```swift +struct Token: Codable, Equatable { + var value: String + var expiration: Date + + init(value: String, expiration: Date) { + self.value = value + self.expiration = expiration + } +} + +extension StorageKey { + static var accessToken: StorageKey { + .init(rawValue: "accessToken") + } +} + +let accessTokenStorage = DefaultSingleValueCodableStorage(storage: keychain, + storageKey: .accessToken, + decoder: JSONKeyValueDecoder(), + encoder: JSONKeyValueEncoder()) + +let expirationCheckStorage = accessTokenStorage.isExpireCheck { $0.expiration.timeIntervalSinceNow > 0 } + +switch expirationCheckStorage.getValue() { +case let .success(token): + // use token + break +case let .failure(storageError) + if .valueNotFound = storageError { + // token is missing or expired, request new token + } else { + // handle storage error + } + break +} +``` -- 2.40.1 From 2ea88a94aa8e37446eccf495220a025eecbf8b10 Mon Sep 17 00:00:00 2001 From: Ivan Smolin Date: Fri, 9 Jun 2023 12:42:55 +0300 Subject: [PATCH 4/5] build: fix excluded files pattern in TIFoundationUtils --- TIFoundationUtils/TIFoundationUtils.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TIFoundationUtils/TIFoundationUtils.podspec b/TIFoundationUtils/TIFoundationUtils.podspec index 5ad8eff1..1881b9d8 100644 --- a/TIFoundationUtils/TIFoundationUtils.podspec +++ b/TIFoundationUtils/TIFoundationUtils.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| s.exclude_files = s.name + '.app' else s.source_files = s.name + '/' + sources - s.exclude_files = s.name + '/*.app' + s.exclude_files = s.name + '/*.app', '**/NefPlaygroundSupport.swift' end s.dependency 'TISwiftUtils', s.version.to_s -- 2.40.1 From 1e3b986c8352eb6b5f5e29d992738b42282098ae Mon Sep 17 00:00:00 2001 From: Ivan Smolin Date: Fri, 9 Jun 2023 16:09:17 +0300 Subject: [PATCH 5/5] fix: iOS 12 crash - failed to demangle superclass of UIClosureObserverOperation from mangled name '\M^? \^Hp\M-}\M^?' --- .../Sources/AsyncOperation+Observe.swift | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Observe.swift b/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Observe.swift index 3b94c89a..17baac58 100644 --- a/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Observe.swift +++ b/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Observe.swift @@ -22,32 +22,20 @@ import Foundation -class BaseClosureObserverOperation: DependendAsyncOperation { - typealias OnResultClosure = ResultClosureType - - let onResult: OnResultClosure? - - init(dependency: AsyncOperation, - onResult: OnResultClosure? = nil) { - - self.onResult = onResult - - super.init(dependency: dependency) { $0 } - } -} - -private final class ClosureObserverOperation: - BaseClosureObserverOperation) -> Void> { +private final class ClosureObserverOperation: DependendAsyncOperation { + typealias OnResultClosure = (Result) -> Void + private let onResult: OnResultClosure? private let callbackQueue: DispatchQueue init(dependency: AsyncOperation, onResult: OnResultClosure? = nil, callbackQueue: DispatchQueue = .main) { + self.onResult = onResult self.callbackQueue = callbackQueue - super.init(dependency: dependency, onResult: onResult) + super.init(dependency: dependency) { $0 } } override func handle(result: Result) { @@ -60,8 +48,18 @@ private final class ClosureObserverOperation: } } -private final class UIClosureObserverOperation: - BaseClosureObserverOperation) -> Void> { +private final class UIClosureObserverOperation: DependendAsyncOperation { + typealias OnResultClosure = @MainActor (Result) -> Void + + private let onResult: OnResultClosure? + + init(dependency: AsyncOperation, + onResult: OnResultClosure? = nil) { + + self.onResult = onResult + + super.init(dependency: dependency) { $0 } + } override func handle(result: Result) { self.result = result -- 2.40.1