feature/flat_map_async_operation_result_type_codable_storage #8
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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() })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
```
|
||||
|
|
|
|||
Loading…
Reference in New Issue