feature/flat_map_async_operation_result_type_codable_storage #8

Merged
ivan.smolin merged 5 commits from feature/flat_map_async_operation_result_type_codable_storage into master 2023-06-15 13:30:23 +03:00
60 changed files with 794 additions and 126 deletions

View File

@ -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<SingleValueStorage>` for async access to SingleValue storages wtih swift concurrency support

View File

@ -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

View File

@ -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' }

View File

@ -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' }

View File

@ -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

View File

@ -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' }

View File

@ -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' }

View File

@ -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<Output, Failure: Error>: AsyncOperation<Output, Failure> {
private var dependencyObservation: NSKeyValueObservation?
private var flatMapObservation: NSKeyValueObservation?
private let dependency: Operation
private var flatMapDependency: AsyncOperation<Output, Failure>?
init<DependencyOutput, DependencyFailure>(dependency: AsyncOperation<DependencyOutput, DependencyFailure>,
flatMapOutput: ((DependencyOutput) -> AsyncOperation<Output, Failure>)?,
flatMapFailure: ((DependencyFailure) -> AsyncOperation<Output, Failure>)?)
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<NewOutput>(_ transform: @escaping (Output) -> AsyncOperation<NewOutput, Failure>)
-> AsyncOperation<NewOutput, Failure> {
FlatMapAsyncOperation(dependency: self,
flatMapOutput: transform,
flatMapFailure: { .just(failure: $0) })
}
func flatMapError<NewFailure>(_ transform: @escaping (Failure) -> AsyncOperation<Output, NewFailure>)
-> AsyncOperation<Output, NewFailure> {
FlatMapAsyncOperation(dependency: self,
flatMapOutput: { .just(success: $0) },
flatMapFailure: transform)
}
}

View File

@ -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<Output, Failure: Error>: AsyncOperation<Output, Failure> {
init(result: Result<Output, Failure>) {
super.init()
self.result = result
}
}
public extension AsyncOperation {
static func just(result: Result<Output, Failure>) -> AsyncOperation<Output, Failure> {
JustAsyncOperation(result: result)
}
static func just(success: Output) -> AsyncOperation<Output, Failure> {
just(result: .success(success))
}
static func just(failure: Failure) -> AsyncOperation<Output, Failure> {
just(result: .failure(failure))
}
}

View File

@ -23,9 +23,9 @@
import Foundation
private final class MapAsyncOperation<Output, Failure: Error>: DependendAsyncOperation<Output, Failure> {
public init<DependencyOutput, DependencyFailure: Error>(dependency: AsyncOperation<DependencyOutput, DependencyFailure>,
mapOutput: @escaping (DependencyOutput) -> Result<Output, Failure>,
mapFailure: @escaping (DependencyFailure) -> Failure) {
init<DependencyOutput, DependencyFailure: Error>(dependency: AsyncOperation<DependencyOutput, DependencyFailure>,
mapOutput: @escaping (DependencyOutput) -> Result<Output, Failure>,
mapFailure: @escaping (DependencyFailure) -> Failure) {
super.init(dependency: dependency) {
$0.mapError(mapFailure).flatMap(mapOutput)

View File

@ -23,14 +23,14 @@
import Foundation
private final class ClosureObserverOperation<Output, Failure: Error>: DependendAsyncOperation<Output, Failure> {
public typealias OnResultClosure = (Result<Output, Failure>) -> Void
typealias OnResultClosure = (Result<Output, Failure>) -> Void
private let onResult: OnResultClosure?
private let callbackQueue: DispatchQueue
public init(dependency: AsyncOperation<Output, Failure>,
onResult: OnResultClosure? = nil,
callbackQueue: DispatchQueue = .main) {
init(dependency: AsyncOperation<Output, Failure>,
onResult: OnResultClosure? = nil,
callbackQueue: DispatchQueue = .main) {
self.onResult = onResult
self.callbackQueue = callbackQueue
@ -48,6 +48,29 @@ private final class ClosureObserverOperation<Output, Failure: Error>: DependendA
}
}
private final class UIClosureObserverOperation<Output, Failure: Error>: DependendAsyncOperation<Output, Failure> {
typealias OnResultClosure = @MainActor (Result<Output, Failure>) -> Void
private let onResult: OnResultClosure?
init(dependency: AsyncOperation<Output, Failure>,
onResult: OnResultClosure? = nil) {
self.onResult = onResult
super.init(dependency: dependency) { $0 }
}
override func handle(result: Result<Output, Failure>) {
self.result = result
DispatchQueue.main.async {
self.onResult?(result)
self.state = .isFinished
}
}
}
public extension AsyncOperation {
func observe(onResult: ((Result<Output, Failure>) -> Void)? = nil,
callbackQueue: DispatchQueue = .main) -> AsyncOperation<Output, Failure> {
@ -57,6 +80,26 @@ public extension AsyncOperation {
callbackQueue: callbackQueue)
}
func observe(onResult: (@MainActor (Result<Output, Failure>) -> Void)? = nil) -> AsyncOperation<Output, Failure> {
UIClosureObserverOperation(dependency: self, onResult: onResult)
}
func observe(onSuccess: (@MainActor (Output) -> Void)? = nil,
onFailure: (@MainActor (Failure) -> Void)? = nil) -> AsyncOperation<Output, Failure> {
let onResult: UIClosureObserverOperation<Output, Failure>.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<Output, Failure> {

View File

@ -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() })
}
}

View File

@ -31,7 +31,7 @@ public protocol CodableKeyValueStorage {
/// or throw exception if the key was not found.
/// - Throws: CodableStorageError
func codableObject<Value: Decodable>(forKey key: StorageKey<Value>,
decoder: CodableKeyValueDecoder) throws -> Value
decoder: CodableKeyValueDecoder) -> Result<Value, StorageError>
/// 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<Value: Encodable>(encodableObject: Value,
forKey key: StorageKey<Value>,
encoder: CodableKeyValueEncoder) throws
encoder: CodableKeyValueEncoder) -> Result<Void, StorageError>
/// 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<Value: Codable>(forKey key: StorageKey<Value>) throws
func removeCodableValue<Value: Codable>(forKey key: StorageKey<Value>) -> Result<Void, StorageError>
func hasCodableValue<Value: Decodable>(forKey key: StorageKey<Value>) -> Result<Bool, StorageError>
}
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<Value: Codable>(codableObject: Value?,
forKey key: StorageKey<Value>,
encoder: CodableKeyValueEncoder = JSONKeyValueEncoder()) throws {
encoder: CodableKeyValueEncoder = JSONKeyValueEncoder()) -> Result<Void, StorageError> {
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()
}
}
}

View File

@ -23,5 +23,5 @@
import Foundation
public protocol CodableKeyValueDecoder {
func decodeDecodable<Value: Decodable>(from data: Data, for key: StorageKey<Value>) throws -> Value
func decodeDecodable<Value: Decodable>(from data: Data, for key: StorageKey<Value>) -> Result<Value, StorageError>
}

View File

@ -29,11 +29,12 @@ open class JSONKeyValueDecoder: CodableKeyValueDecoder {
self.jsonDecoder = jsonDecoder
}
open func decodeDecodable<Value: Decodable>(from data: Data, for key: StorageKey<Value>) throws -> Value {
do {
return try jsonDecoder.decode(Value.self, from: data)
} catch {
throw StorageError.unableToDecode(underlyingError: error)
open func decodeDecodable<Value: Decodable>(from data: Data, for key: StorageKey<Value>) -> Result<Value, StorageError> {
Result {
try jsonDecoder.decode(Value.self, from: data)
}
.mapError {
.unableToDecode(underlyingError: $0)
}
}
}

View File

@ -26,13 +26,13 @@ import Foundation
open class UnarchiverKeyValueDecoder: CodableKeyValueDecoder {
public init() {}
open func decodeDecodable<Value: Decodable>(from data: Data, for key: StorageKey<Value>) throws -> Value {
open func decodeDecodable<Value: Decodable>(from data: Data, for key: StorageKey<Value>) -> Result<Value, StorageError> {
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)
}
}

View File

@ -26,17 +26,17 @@ import Foundation
open class ArchiverKeyValueEncoder: CodableKeyValueEncoder {
public init() {}
open func encodeEncodable<Value: Encodable>(value: Value, for key: StorageKey<Value>) throws -> Data {
open func encodeEncodable<Value: Encodable>(value: Value, for key: StorageKey<Value>) -> Result<Data, StorageError> {
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)
}
}

View File

@ -23,5 +23,5 @@
import Foundation
public protocol CodableKeyValueEncoder {
func encodeEncodable<Value: Encodable>(value: Value, for key: StorageKey<Value>) throws -> Data
func encodeEncodable<Value: Encodable>(value: Value, for key: StorageKey<Value>) -> Result<Data, StorageError>
}

View File

@ -29,11 +29,12 @@ open class JSONKeyValueEncoder: CodableKeyValueEncoder {
self.jsonEncoder = jsonEncoder
}
open func encodeEncodable<Value: Encodable>(value: Value, for key: StorageKey<Value>) throws -> Data {
do {
return try jsonEncoder.encode(value)
} catch {
throw StorageError.unableToEncode(underlyingError: error)
open func encodeEncodable<Value: Encodable>(value: Value, for key: StorageKey<Value>) -> Result<Data, StorageError> {
Result {
try jsonEncoder.encode(value)
}
.mapError {
.unableToEncode(underlyingError: $0)
}
}
}

View File

@ -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
}
}
}

View File

@ -24,29 +24,38 @@ import Foundation
extension UserDefaults: CodableKeyValueStorage {
public func codableObject<Value: Decodable>(forKey key: StorageKey<Value>,
decoder: CodableKeyValueDecoder) throws -> Value {
decoder: CodableKeyValueDecoder) -> Result<Value, StorageError> {
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<Value: Encodable>(encodableObject: Value,
forKey key: StorageKey<Value>,
encoder: CodableKeyValueEncoder) throws {
encoder: CodableKeyValueEncoder) -> Result<Void, StorageError> {
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<Value: Codable>(forKey key: StorageKey<Value>) throws {
public func removeCodableValue<Value: Codable>(forKey key: StorageKey<Value>) -> Result<Void, StorageError> {
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<Value: Decodable>(forKey key: StorageKey<Value>) -> Result<Bool, StorageError> {
guard data(forKey: key.rawValue) != nil else {
return .success(false)
}
return .success(true)
}
}

View File

@ -0,0 +1,56 @@
//
// 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 DefaultSingleValueCodableStorage<CodableStorage: CodableKeyValueStorage, ValueType: Codable>:
BaseSingleValueStorage<ValueType, CodableStorage> {
public init(storage: CodableStorage,
storageKey: StorageKey<ValueType>,
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: storage,
storageKey: storageKey,
hasValueClosure: hasValueClosure,
deleteValueClosure: deleteValueClosure,
getValueClosure: getValueClosure,
storeValueClosure: storeValueClosure)
}
}

View File

@ -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<Storage: SingleValueStorage>: 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<Void, Storage.ErrorType> {
wrappedStorage.store(value: value)
}
open func getValue() -> Result<Storage.ValueType, Storage.ErrorType> {
wrappedStorage.getValue()
.flatMap {
if isExpiredCheckClosure($0) {
return .failure(.valueNotFound)
} else {
return .success($0)
}
}
}
open func deleteValue() -> Result<Void, Storage.ErrorType> {
wrappedStorage.deleteValue()
}
}
public extension SingleValueStorage where ErrorType == StorageError {
typealias ExpirationStorage = SingleValueExpirationStorage<Self>
func isExpireCheck(_ isExpiredCheck: @escaping ExpirationStorage.ExpirationCheckClosure) -> ExpirationStorage {
ExpirationStorage(wrappedStorage: self, isExpiredCheckClosure: isExpiredCheck)
}
}

View File

@ -32,11 +32,12 @@ let intResultOperation = ClosureAsyncOperation<Int, Never> { completion in
/*:
## Базовые операторы
На данный момент реализовано всего два оператора:
На данный момент реализовано четыре оператора:
- `map(mapOutput:mapFailure:)` - конвертирует ResultType в новый NewResultType и ErrorType в новый NewErrorType
- `observe(onSuccess:onFailure)` - просто вызывает переданные callback'и при получении результата или ошибки
- `flatMap<NewOutput>(_:)` - подписывается на результат выполнения нового AsyncOperation полученного из closure
- `flatMapError<NewFailure>(_:)` - подписывается на результат выполнения нового AsyncOperation полученного из closure
*/
//: ### Пример запуска асинхронных операци с применением операторов в последовательной очереди и вывод результатов
@ -75,3 +76,53 @@ ClosureAsyncOperation<String, Never> { completion in
"Async operation two has finished with Success"
```
*/
//: ### Пример использования оператора flatMap у AsyncOperaton
import TISwiftUtils
extension StorageKey {
static var loyaltyCardNumber: StorageKey<String> {
.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<String, Failure> {
.just(success: "Supreme card")
// .just(failure: .cardFetchError)
}
func getSavedCardTitle(completion: @escaping UIParameterClosure<Result<String, Failure>>) -> 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)

View File

@ -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

View File

@ -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' }
@ -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

View File

@ -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' }

View File

@ -28,41 +28,56 @@ public typealias KeychainCodableBackingStore<T: Codable> = CodableKeyValueBackin
extension Keychain: CodableKeyValueStorage {
public func codableObject<Value: Decodable>(forKey key: StorageKey<Value>,
decoder: CodableKeyValueDecoder) throws -> Value {
decoder: CodableKeyValueDecoder) -> Result<Value, StorageError> {
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<Value: Encodable>(encodableObject: Value,
forKey key: StorageKey<Value>,
encoder: CodableKeyValueEncoder) throws {
encoder: CodableKeyValueEncoder) -> Result<Void, StorageError> {
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<Value: Codable>(forKey key: StorageKey<Value>) -> Result<Void, StorageError> {
Result {
try remove(key.rawValue)
}
.mapError {
.unableToWriteData(underlyingError: $0)
}
}
public func removeCodableValue<Value: Codable>(forKey key: StorageKey<Value>) throws {
do {
try remove(key.rawValue)
} catch {
throw StorageError.unableToWriteData(underlyingError: error)
public func hasCodableValue<Value: Decodable>(forKey key: StorageKey<Value>) -> Result<Bool, StorageError> {
Result {
try contains(key.rawValue)
}
.mapError {
.unableToExtractData(underlyingError: $0)
}
}
}

View File

@ -71,3 +71,46 @@ if appInstallAwareTokenStorage.hasStoredValue() {
// app was reinstalled or token is empty
// ...
}
/*:
### `SingleValueExpirationStorage<SingleValueStorage>`
Класс позволяющий добавить дополнительную функциональность очистки значения по конкретному ключу
если истёк срок действия объекта (например 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<Token> {
.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
}

View File

@ -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' }

View File

@ -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' }

View File

@ -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' }

View File

@ -43,20 +43,6 @@ open class DefaultRecoverableJsonNetworkService<ApiError: Decodable & Error>: De
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 withTaskCancellableClosure {
process(recoverableRequest: recoverableRequest,
prependRequestRetriers: prependRequestRetriers,
appendRequestRetriers: appendRequestRetriers,
completion: $0)
}
}
open func process<B: Encodable, S>(recoverableRequest: EndpointRequest<B, S>,
errorHandlers: [RequestRetrier],
completion: @escaping ParameterClosure<EndpointResponse<S>>) -> Cancellable {
@ -72,6 +58,32 @@ open class DefaultRecoverableJsonNetworkService<ApiError: Decodable & Error>: De
}
}
// MARK: - AsyncOperaton support
open func process<B: Encodable, S: Decodable>(recoverableRequest: EndpointRequest<B, S>,
prependRequestRetriers: [RequestRetrier] = [],
appendRequestRetriers: [RequestRetrier] = []) -> AsyncOperation<S, ErrorCollection<ErrorType>> {
ClosureAsyncOperation { completion in
self.process(recoverableRequest: recoverableRequest,
prependRequestRetriers: prependRequestRetriers,
appendRequestRetriers: appendRequestRetriers) {
completion($0)
}
}
}
open func process<B: Encodable, S: Decodable>(recoverableRequest: EndpointRequest<B, S>,
errorHandlers: [RequestRetrier]) -> AsyncOperation<S, ErrorCollection<ErrorType>> {
ClosureAsyncOperation { completion in
self.process(recoverableRequest: recoverableRequest,
errorHandlers: errorHandlers) {
completion($0)
}
}
}
// MARK: - Swift concurrency support
@available(iOS 13.0.0, *)
open func process<B: Encodable, S>(recoverableRequest: EndpointRequest<B, S>,
errorHandlers: [RequestRetrier]) async -> EndpointResponse<S> {
@ -83,6 +95,22 @@ open class DefaultRecoverableJsonNetworkService<ApiError: Decodable & Error>: De
}
}
@available(iOS 13.0.0, *)
open func process<B: Encodable, S>(recoverableRequest: EndpointRequest<B, S>,
prependRequestRetriers: [RequestRetrier] = [],
appendRequestRetriers: [RequestRetrier] = []) async ->
EndpointResponse<S> {
await withTaskCancellableClosure {
process(recoverableRequest: recoverableRequest,
prependRequestRetriers: prependRequestRetriers,
appendRequestRetriers: appendRequestRetriers,
completion: $0)
}
}
// MARK: - Response handling
open func handle<B: Encodable, S>(recoverableResponse: RequestResult<S, ApiError>,
request: EndpointRequest<B, S>,
errorHandlers: [RequestRetrier],
@ -114,6 +142,8 @@ open class DefaultRecoverableJsonNetworkService<ApiError: Decodable & Error>: De
}
}
// MARK: - RequestRetriers configuration
public func register<RequestRetrier: EndpointRequestRetrier>(defaultRequestRetrier: RequestRetrier)
where RequestRetrier.ErrorResult == ErrorType {
@ -126,6 +156,8 @@ open class DefaultRecoverableJsonNetworkService<ApiError: Decodable & Error>: De
self.defaultRequestRetriers = defaultRequestRetriers.map { $0.asAnyEndpointRequestRetrier() }
}
// MARK: - Internal
private static func validateAndRepair<B, S, R: Collection>(request: EndpointRequest<B, S>,
errors: [ErrorType],
retriers: R,

View File

@ -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' }

View File

@ -48,6 +48,17 @@ public extension ApiInteractor {
}
}
public extension ApiInteractor {
func process<B: Encodable, S: Decodable, F: Decodable>(request: EndpointRequest<B, S>)
-> AsyncOperation<S, EndpointErrorResult<F, NetworkError>> {
ClosureAsyncOperation { completion in
self.process(request: request) {
completion($0)
}
}
}
}
@available(iOS 13.0.0, *)
public extension ApiInteractor {
func process<B: Encodable, S, F>(request: EndpointRequest<B, S>) async -> RequestResult<S, F> {

View File

@ -24,3 +24,23 @@ public enum EndpointErrorResult<ApiError, NetworkError>: 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)
}
}
}

View File

@ -38,4 +38,12 @@ public struct ErrorCollection<E>: Error {
public func lastOr(_ default: E) -> E {
failures.last ?? `default`
}
public func contains<ApiError, NetworkError>(_ whereClause: (ApiError, Int) -> Bool) -> Bool where E == EndpointErrorResult<ApiError, NetworkError> {
failures.contains { $0.matches(whereClause) }
}
public func contains<ApiError, NetworkError>(_ whereClause: (NetworkError) -> Bool) -> Bool where E == EndpointErrorResult<ApiError, NetworkError> {
failures.contains { $0.matches(whereClause) }
}
}

View File

@ -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' }

View File

@ -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' }

View File

@ -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' }

View File

@ -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' }

View File

@ -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' }

View File

@ -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' }

View File

@ -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' }

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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' }

View File

@ -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' }

View File

@ -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' }

View File

@ -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' }

View File

@ -30,11 +30,12 @@ let intResultOperation = ClosureAsyncOperation<Int, Never> { completion in
## Базовые операторы
На данный момент реализовано всего два оператора:
На данный момент реализовано четыре оператора:
- `map(mapOutput:mapFailure:)` - конвертирует ResultType в новый NewResultType и ErrorType в новый NewErrorType
- `observe(onSuccess:onFailure)` - просто вызывает переданные callback'и при получении результата или ошибки
- `flatMap<NewOutput>(_:)` - подписывается на результат выполнения нового AsyncOperation полученного из closure
- `flatMapError<NewFailure>(_:)` - подписывается на результат выполнения нового AsyncOperation полученного из closure
### Пример запуска асинхронных операци с применением операторов в последовательной очереди и вывод результатов
@ -72,3 +73,55 @@ ClosureAsyncOperation<String, Never> { 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<String> {
.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<String, Failure> {
.just(success: "Supreme card")
// .just(failure: .cardFetchError)
}
func getSavedCardTitle(completion: @escaping UIParameterClosure<Result<String, Failure>>) -> 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)
```

View File

@ -70,3 +70,46 @@ if appInstallAwareTokenStorage.hasStoredValue() {
// ...
}
```
### `SingleValueExpirationStorage<SingleValueStorage>`
Класс позволяющий добавить дополнительную функциональность очистки значения по конкретному ключу
если истёк срок действия объекта (например 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<Token> {
.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
}
```

View File

@ -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)
```

View File

@ -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)
```