feat: migrating storages #10
|
|
@ -1,5 +1,9 @@
|
|||
# Changelog
|
||||
|
||||
### 1.49.0
|
||||
|
||||
- **Added**: `BaseMigratingSingleValueKeychainStorage` and `BaseMigratingSingleValueDefaultsStorage` implementations for migrating keys from one storage to another.
|
||||
|
||||
### 1.48.0
|
||||
|
||||
- **Added**: `BaseStackView` with configurable items appearance
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ let package = Package(
|
|||
plugins: [.plugin(name: "TISwiftLintPlugin")]),
|
||||
|
||||
.target(name: "TIFoundationUtils",
|
||||
dependencies: ["TISwiftUtils"],
|
||||
dependencies: ["TISwiftUtils", "TILogging"],
|
||||
path: "TIFoundationUtils",
|
||||
exclude: ["TIFoundationUtils.app"],
|
||||
plugins: [.plugin(name: "TISwiftLintPlugin")]),
|
||||
|
|
@ -145,7 +145,11 @@ let package = Package(
|
|||
.testTarget(
|
||||
name: "TITextProcessingTests",
|
||||
dependencies: ["TITextProcessing"],
|
||||
path: "Tests/TITextProcessingTests")
|
||||
path: "Tests/TITextProcessingTests"),
|
||||
.testTarget(
|
||||
name: "TIFoundationUtilsTests",
|
||||
dependencies: ["TIFoundationUtils", "TISwiftUtils", "TILogging"],
|
||||
path: "Tests/TIFoundationUtilsTests")
|
||||
]
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIAppleMapUtils'
|
||||
s.version = '1.48.0'
|
||||
s.version = '1.49.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.48.0'
|
||||
s.version = '1.49.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.48.0'
|
||||
s.version = '1.49.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' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIDeveloperUtils'
|
||||
s.version = '1.48.0'
|
||||
s.version = '1.49.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.48.0'
|
||||
s.version = '1.49.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 TISwiftUtils
|
||||
|
||||
public extension MigratingBackingStore where MigratingStorages: CodableKeyValueStorage,
|
||||
StoreContent: Codable {
|
||||
|
||||
init<Value: Codable>(key: StorageKey<Value>,
|
||||
storageContainer: MigratingStorages,
|
||||
decoder: CodableKeyValueDecoder = JSONKeyValueDecoder(),
|
||||
encoder: CodableKeyValueEncoder = JSONKeyValueEncoder())
|
||||
where StoreContent == Value? {
|
||||
|
||||
let getClosure: GetClosure = { container in
|
||||
Self.getValue(from: container, forKey: key, decoder: decoder, encoder: encoder)
|
||||
|
ivan.smolin marked this conversation as resolved
Outdated
|
||||
}
|
||||
|
||||
let setClosure: SetClosure = { container, value in
|
||||
|
ivan.smolin marked this conversation as resolved
Outdated
ivan.smolin
commented
видимо коммент должен был прояснить зачем мы сначала получаем значение а потом его обратно записываем. видимо коммент должен был прояснить зачем мы сначала получаем значение а потом его обратно записываем.
разве мигация не происходит внутри контейнера?
ivan.smolin
commented
окей, а можем это вынести в отдельный (статический?) метод, чтобы не дублировать логину ниже окей, а можем это вынести в отдельный (статический?) метод, чтобы не дублировать логину ниже
|
||||
try? container.setOrRemove(codableObject: value, forKey: key, encoder: encoder).get()
|
||||
}
|
||||
|
||||
|
ivan.smolin marked this conversation as resolved
Outdated
ivan.smolin
commented
может через цепочку? что-то такое:
может через цепочку?
что-то такое:
```swift
container.sourceStorage.codableObject(forKey: key, decoder: decoder)
.flatMap {
container.sourceStorage.removeCodableValue(forKey: key)
}
.flatMap {
container.targetStorage.set(encodableObject: value, forKey: key, encoder: encoder)
}
```
|
||||
self.init(storageContainer: storageContainer, getClosure: getClosure, setClosure: setClosure)
|
||||
}
|
||||
|
||||
init<Value: Codable>(wrappedValue: Value,
|
||||
key: StorageKey<Value>,
|
||||
storageContainer: MigratingStorages,
|
||||
decoder: CodableKeyValueDecoder = JSONKeyValueDecoder(),
|
||||
encoder: CodableKeyValueEncoder = JSONKeyValueEncoder())
|
||||
where StoreContent == Value? {
|
||||
|
||||
let getClosure: GetClosure = { container in
|
||||
Self.getValue(from: container, forKey: key, wrappedValue: wrappedValue, decoder: decoder, encoder: encoder)
|
||||
}
|
||||
|
||||
let setClosure: SetClosure = { container, value in
|
||||
try? container.setOrRemove(codableObject: value, forKey: key, encoder: encoder).get()
|
||||
}
|
||||
|
||||
self.init(storageContainer: storageContainer, getClosure: getClosure, setClosure: setClosure)
|
||||
|
ivan.smolin marked this conversation as resolved
Outdated
ivan.smolin
commented
аналогично аналогично
|
||||
}
|
||||
|
||||
static func getValue<Value: Codable>(from container: MigratingStorages,
|
||||
|
ivan.smolin marked this conversation as resolved
Outdated
ivan.smolin
commented
аналогично аналогично
|
||||
forKey key: StorageKey<Value>,
|
||||
wrappedValue: Value? = nil,
|
||||
decoder: CodableKeyValueDecoder,
|
||||
encoder: CodableKeyValueEncoder) -> StoreContent where StoreContent == Value? {
|
||||
|
||||
// Storage container can't handle migration when source storage has value and target doesn't.
|
||||
// This happens because of Decodable restrictions for Value in method `codableObject(forKey:decoder)`
|
||||
// and absence of encoder.
|
||||
try? container.codableObject(forKey: key, decoder: decoder)
|
||||
.flatMap {
|
||||
let _ = container.set(encodableObject: $0, forKey: key, encoder: encoder)
|
||||
|
||||
return .success($0)
|
||||
}
|
||||
|
ivan.smolin marked this conversation as resolved
Outdated
ivan.smolin
commented
копипаста. можем что-то с этим сделать? копипаста. можем что-то с этим сделать?
|
||||
.flatMapError {
|
||||
guard let wrappedValue else {
|
||||
return .failure($0)
|
||||
}
|
||||
|
||||
return container.set(encodableObject: wrappedValue, forKey: key, encoder: encoder)
|
||||
.flatMap { _ in .success(wrappedValue) }
|
||||
}
|
||||
.get()
|
||||
}
|
||||
}
|
||||
|
|
@ -44,11 +44,7 @@ extension UserDefaults: CodableKeyValueStorage {
|
|||
}
|
||||
|
||||
public func removeCodableValue<Value: Codable>(forKey key: StorageKey<Value>) -> Result<Void, StorageError> {
|
||||
guard data(forKey: key.rawValue) != nil else {
|
||||
return .failure(.valueNotFound)
|
||||
}
|
||||
|
||||
return .success(removeObject(forKey: key.rawValue))
|
||||
removeValue(forKey: key)
|
||||
}
|
||||
|
||||
public func hasCodableValue<Value: Decodable>(forKey key: StorageKey<Value>) -> Result<Bool, StorageError> {
|
||||
|
|
@ -58,4 +54,12 @@ extension UserDefaults: CodableKeyValueStorage {
|
|||
|
||||
return .success(true)
|
||||
}
|
||||
|
||||
public func removeValue<Value>(forKey key: StorageKey<Value>) -> Result<Void, StorageError> {
|
||||
guard data(forKey: key.rawValue) != nil else {
|
||||
return .failure(.valueNotFound)
|
||||
}
|
||||
|
||||
return .success(removeObject(forKey: key.rawValue))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ public struct AnySingleValueStorage<ValueType, ErrorType: Error>: SingleValueSto
|
|||
}
|
||||
|
||||
public extension SingleValueStorage {
|
||||
func eraseToAnySingleValueStorate() -> AnySingleValueStorage<ValueType, ErrorType> {
|
||||
func eraseToAnySingleValueStorage() -> AnySingleValueStorage<ValueType, ErrorType> {
|
||||
AnySingleValueStorage(storage: self)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,125 @@
|
|||
//
|
||||
// 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 TISwiftUtils
|
||||
import TILogging
|
||||
|
||||
open class BaseCodableMigratingStorageContainer<SourceStorage,
|
||||
TargetStorage>: MigratingStorageContainer<SourceStorage, TargetStorage>,
|
||||
CodableKeyValueStorage
|
||||
where SourceStorage: CodableKeyValueStorage, TargetStorage: CodableKeyValueStorage {
|
||||
|
||||
public let errorLogger: ErrorLogger
|
||||
|
ivan.smolin marked this conversation as resolved
ivan.smolin
commented
unused unused
|
||||
|
||||
public init(sourceStorage: SourceStorage,
|
||||
targetStorage: TargetStorage,
|
||||
errorLogger: ErrorLogger = TIFoundationLogger(category: "BaseCodableMigratingStorageContainer")) {
|
||||
|
||||
self.errorLogger = errorLogger
|
||||
|
||||
super.init(sourceStorage: sourceStorage, targetStorage: targetStorage)
|
||||
}
|
||||
|
||||
open func codableObject<Value: Decodable>(forKey key: StorageKey<Value>,
|
||||
decoder: CodableKeyValueDecoder) -> Result<Value, StorageError> {
|
||||
|
||||
targetStorage.codableObject(forKey: key, decoder: decoder)
|
||||
.flatMap {
|
||||
if case let .failure(error) = removeSourceValue(forKey: key) {
|
||||
logErrorIfNeeded(error, file: #file, line: #line)
|
||||
|
ivan.smolin marked this conversation as resolved
Outdated
ivan.smolin
commented
даже если мы не пробрасываем результат выше, то надо залогировать (см. пример в TINetworking) даже если мы не пробрасываем результат выше, то надо залогировать (см. пример в TINetworking)
|
||||
}
|
||||
|
||||
return .success($0)
|
||||
|
ivan.smolin marked this conversation as resolved
Outdated
ivan.smolin
commented
if case let if case let
|
||||
}
|
||||
.flatMapError {
|
||||
logErrorIfNeeded($0, file: #file, line: #line)
|
||||
|
||||
return sourceStorage.codableObject(forKey: key, decoder: decoder)
|
||||
|
ivan.smolin marked this conversation as resolved
Outdated
ivan.smolin
commented
в случае codable, вызов hasCodableValue будет равносилен получению codableObject (чтение + декодирование в объект), поэтому предлагаю сразу вызвать removeCodableValue и сделать flatMapError с проверкой на valueNotFound в случае codable, вызов hasCodableValue будет равносилен получению codableObject (чтение + декодирование в объект), поэтому предлагаю сразу вызвать removeCodableValue и сделать flatMapError с проверкой на valueNotFound
|
||||
}
|
||||
}
|
||||
|
||||
open func set<Value: Encodable>(encodableObject: Value,
|
||||
forKey key: StorageKey<Value>,
|
||||
|
ivan.smolin marked this conversation as resolved
ivan.smolin
commented
вот тут можно ещё залогировать, если ошибка не .valueNotFound вот тут можно ещё залогировать, если ошибка не .valueNotFound
|
||||
encoder: CodableKeyValueEncoder) -> Result<Void, StorageError> {
|
||||
|
||||
targetStorage.set(encodableObject: encodableObject, forKey: key, encoder: encoder)
|
||||
.flatMap {
|
||||
if case let .failure(error) = removeSourceValue(forKey: key) {
|
||||
logErrorIfNeeded(error, file: #file, line: #line)
|
||||
}
|
||||
|
||||
|
ivan.smolin marked this conversation as resolved
Outdated
ivan.smolin
commented
if case let if case let
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
|
||||
|
ivan.smolin marked this conversation as resolved
Outdated
ivan.smolin
commented
тут не надо вернуть тут не надо вернуть `Result<Void, StorageError>`?
|
||||
open func removeCodableValue<Value: Codable>(forKey key: StorageKey<Value>) -> Result<Void, StorageError> {
|
||||
|
ivan.smolin marked this conversation as resolved
Outdated
ivan.smolin
commented
тут вроде аналогичная логика как и выше с двумя результатами тут вроде аналогичная логика как и выше с двумя результатами
ivan.smolin
commented
up up
|
||||
switch (targetStorage.removeCodableValue(forKey: key), sourceStorage.removeCodableValue(forKey: key)) {
|
||||
case (.success, .success):
|
||||
return .success(())
|
||||
|
ivan.smolin marked this conversation as resolved
Outdated
ivan.smolin
commented
залогировать если не .valueNotFound? залогировать если не .valueNotFound?
|
||||
|
||||
case let (.success, .failure(error)):
|
||||
logErrorIfNeeded(error, file: #file, line: #line)
|
||||
|
||||
return .success(())
|
||||
|
||||
case let (.failure(error), .success):
|
||||
|
ivan.smolin marked this conversation as resolved
Outdated
ivan.smolin
commented
я не понимаю. как мне представляется, текущий код работает так:
а нужно:
я не понимаю.
как мне представляется, текущий код работает так:
- удаляем из target
- удаляем из source
- ошибку source игнорим
- ошибку из target логируем
- удаляем из source
- ошибку из source прокидываем
а нужно:
- удалить из target
- удалить из source
- ошибку из target прокинуть
- ошибку из source залогировать (если не .valueNotFound)
|
||||
return .failure(error)
|
||||
|
||||
|
ivan.smolin marked this conversation as resolved
Outdated
ivan.smolin
commented
логируем если не .valueNotFound логируем если не .valueNotFound
|
||||
case let (.failure(targetError), .failure(sourceError)):
|
||||
logErrorIfNeeded(sourceError, file: #file, line: #line)
|
||||
|
||||
return .failure(targetError)
|
||||
}
|
||||
|
ivan.smolin marked this conversation as resolved
Outdated
ivan.smolin
commented
тут тоже надо отдать приоритет ошибке из target (если она не .valueNotFound) тут тоже надо отдать приоритет ошибке из target (если она не .valueNotFound)
|
||||
}
|
||||
|
||||
open func hasCodableValue<Value: Decodable>(forKey key: StorageKey<Value>) -> Result<Bool, StorageError> {
|
||||
switch (targetStorage.hasCodableValue(forKey: key), sourceStorage.hasCodableValue(forKey: key)) {
|
||||
case let (.success(value), _):
|
||||
return .success(value)
|
||||
|
||||
case let (.failure, .success(value)):
|
||||
return .success(value)
|
||||
|
||||
case let (.failure(targetError), .failure(sourceError)):
|
||||
logErrorIfNeeded(sourceError, file: #file, line: #line)
|
||||
|
||||
return .failure(targetError)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Open methods
|
||||
|
||||
open func removeSourceValue<Value>(forKey key: StorageKey<Value>) -> Result<Void, StorageError> {
|
||||
// override in subclasses
|
||||
.failure(.valueNotFound)
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
|
||||
private func logErrorIfNeeded(_ error: StorageError, file: StaticString, line: Int) {
|
||||
guard !error.isValueNotFound else {
|
||||
return
|
||||
}
|
||||
|
||||
errorLogger.log(error: error, file: file, line: line)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// Copyright (c) 2023 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public final class DefaultsMigratingStorageContainer: BaseCodableMigratingStorageContainer<UserDefaults, UserDefaults> {
|
||||
|
||||
public override func removeSourceValue<Value>(forKey key: StorageKey<Value>) -> Result<Void, StorageError> {
|
||||
sourceStorage.removeValue(forKey: key)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
//
|
||||
// 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 TILogging
|
||||
|
||||
public final class MigratingSingleValueStorage<SourceStorage: SingleValueStorage,
|
||||
TargetStorage: SingleValueStorage>: SingleValueStorage
|
||||
where SourceStorage.ErrorType == TargetStorage.ErrorType,
|
||||
SourceStorage.ErrorType == StorageError,
|
||||
SourceStorage.ValueType == TargetStorage.ValueType {
|
||||
|
||||
public typealias ValueType = SourceStorage.ValueType
|
||||
|
||||
public let sourceStorage: SourceStorage
|
||||
public let targetStorage: TargetStorage
|
||||
public let errorLogger: ErrorLogger
|
||||
|
||||
public init(sourceStorage: SourceStorage,
|
||||
targetStorage: TargetStorage,
|
||||
errorLogger: ErrorLogger = TIFoundationLogger(category: "MigratingSingleValueStorage")) {
|
||||
|
ivan.smolin marked this conversation as resolved
Outdated
ivan.smolin
commented
надо переименовать и в доках тоже надо переименовать и в доках тоже
|
||||
|
||||
self.sourceStorage = sourceStorage
|
||||
self.targetStorage = targetStorage
|
||||
self.errorLogger = errorLogger
|
||||
}
|
||||
|
||||
// MARK: - SingleValueStorage
|
||||
|
||||
public func hasStoredValue() -> Bool {
|
||||
targetStorage.hasStoredValue() || sourceStorage.hasStoredValue()
|
||||
}
|
||||
|
||||
public func store(value: ValueType) -> Result<Void, StorageError> {
|
||||
targetStorage.store(value: value)
|
||||
|
ivan.smolin marked this conversation as resolved
Outdated
ivan.smolin
commented
сначала надо записать в target и если получилось уже удалять из source сначала надо записать в target и если получилось уже удалять из source
|
||||
.flatMap {
|
||||
if case let .failure(error) = sourceStorage.deleteValue() {
|
||||
logErrorIfNeeded(error, file: #file, line: #line)
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
|
||||
public func getValue() -> Result<ValueType, StorageError> {
|
||||
targetStorage.getValue()
|
||||
.flatMap {
|
||||
if case let .failure(error) = sourceStorage.deleteValue() {
|
||||
logErrorIfNeeded(error, file: #file, line: #line)
|
||||
}
|
||||
|
||||
return .success($0)
|
||||
}
|
||||
.flatMapError { _ in
|
||||
|
ivan.smolin marked this conversation as resolved
ivan.smolin
commented
не? ```swift
store(value: value)
.map { _ in value }
```
не?
|
||||
sourceStorage.getValue()
|
||||
.flatMap { value in
|
||||
store(value: value)
|
||||
.map { _ in value }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func deleteValue() -> Result<Void, StorageError> {
|
||||
switch (targetStorage.deleteValue(), sourceStorage.deleteValue()) {
|
||||
case (.success, .success):
|
||||
return .success(())
|
||||
|
||||
case let (.success, .failure(error)):
|
||||
logErrorIfNeeded(error, file: #file, line: #line)
|
||||
|
||||
return .success(())
|
||||
|
||||
case let (.failure(error), .success):
|
||||
return .failure(error)
|
||||
|
||||
case let (.failure(targetError), .failure(sourceError)):
|
||||
logErrorIfNeeded(sourceError, file: #file, line: #line)
|
||||
|
||||
return .failure(targetError)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
|
||||
private func logErrorIfNeeded(_ error: StorageError, file: StaticString, line: Int) {
|
||||
guard !error.isValueNotFound else {
|
||||
return
|
||||
|
ivan.smolin marked this conversation as resolved
Outdated
ivan.smolin
commented
в этом случае я бы отдал приоритет error из targetSource в этом случае я бы отдал приоритет error из targetSource
|
||||
}
|
||||
|
||||
errorLogger.log(error: error, file: file, line: line)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Factory methods
|
||||
|
||||
public extension SingleValueStorage where ErrorType == StorageError {
|
||||
|
||||
func migrating<S: SingleValueStorage>(to targetStorage: S) -> MigratingSingleValueStorage<Self, S>
|
||||
where ErrorType == S.ErrorType,
|
||||
|
ivan.smolin marked this conversation as resolved
Outdated
ivan.smolin
commented
можно ещё добавить .migrating(from:) можно ещё добавить .migrating(from:)
|
||||
ValueType == S.ValueType {
|
||||
|
||||
MigratingSingleValueStorage(sourceStorage: self, targetStorage: targetStorage)
|
||||
}
|
||||
|
||||
func migrating<S: SingleValueStorage>(from sourceStorage: S) -> MigratingSingleValueStorage<S, Self>
|
||||
where ErrorType == S.ErrorType,
|
||||
ValueType == S.ValueType {
|
||||
|
||||
MigratingSingleValueStorage(sourceStorage: sourceStorage, targetStorage: self)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// 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
|
||||
import TISwiftUtils
|
||||
|
||||
public typealias DefaultsMigratingCodableBackingStore<T: Codable> = MigratingBackingStore<DefaultsMigratingStorageContainer, T>
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// 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 TILogging
|
||||
import os
|
||||
|
||||
public final class TIFoundationLogger: DefaultOSLogErrorLogger {
|
||||
public init(category: String) {
|
||||
super.init(log: OSLog(subsystem: "TIFoundationUtils", category: category))
|
||||
}
|
||||
}
|
||||
|
|
@ -5,5 +5,6 @@ target 'TIFoundationUtils' do
|
|||
use_frameworks!
|
||||
|
||||
pod 'TISwiftUtils', :path => '../../../../TISwiftUtils/TISwiftUtils.podspec'
|
||||
pod 'TILogging', :path => '../../../../TILogging/TILogging.podspec'
|
||||
pod 'TIFoundationUtils', :path => '../../../../TIFoundationUtils/TIFoundationUtils.podspec'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIFoundationUtils'
|
||||
s.version = '1.48.0'
|
||||
s.version = '1.49.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' }
|
||||
|
|
@ -19,6 +19,7 @@ Pod::Spec.new do |s|
|
|||
s.exclude_files = s.name + '/*.app', '**/NefPlaygroundSupport.swift'
|
||||
end
|
||||
|
||||
s.dependency 'TILogging', s.version.to_s
|
||||
s.dependency 'TISwiftUtils', s.version.to_s
|
||||
s.framework = 'Foundation'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIGoogleMapUtils'
|
||||
s.version = '1.48.0'
|
||||
s.version = '1.49.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' }
|
||||
|
|
|
|||
|
|
@ -64,12 +64,7 @@ extension Keychain: CodableKeyValueStorage {
|
|||
}
|
||||
|
||||
public func removeCodableValue<Value: Codable>(forKey key: StorageKey<Value>) -> Result<Void, StorageError> {
|
||||
Result {
|
||||
try remove(key.rawValue)
|
||||
}
|
||||
.mapError {
|
||||
.unableToWriteData(underlyingError: $0)
|
||||
}
|
||||
removeValue(forKey: key)
|
||||
}
|
||||
|
||||
public func hasCodableValue<Value: Decodable>(forKey key: StorageKey<Value>) -> Result<Bool, StorageError> {
|
||||
|
|
@ -80,4 +75,13 @@ extension Keychain: CodableKeyValueStorage {
|
|||
.unableToExtractData(underlyingError: $0)
|
||||
}
|
||||
}
|
||||
|
||||
public func removeValue<Value>(forKey key: StorageKey<Value>) -> Result<Void, StorageError> {
|
||||
Result {
|
||||
try remove(key.rawValue)
|
||||
}
|
||||
.mapError {
|
||||
.unableToWriteData(underlyingError: $0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// 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
|
||||
import TISwiftUtils
|
||||
|
||||
public typealias KeychainMigratingCodableBackingStore<T: Codable> = MigratingBackingStore<KeychainMigratingStorageContainer, T>
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// Copyright (c) 2023 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TIFoundationUtils
|
||||
import KeychainAccess
|
||||
|
||||
public final class KeychainMigratingStorageContainer: BaseCodableMigratingStorageContainer<Keychain, Keychain> {
|
||||
|
||||
public override func removeSourceValue<Value>(forKey key: StorageKey<Value>) -> Result<Void, StorageError> {
|
||||
|
ivan.smolin marked this conversation as resolved
Outdated
ivan.smolin
commented
а зачем на этот метод отдельно? вроде бы мы storage'ы закрываем протоколами и там есть функция для этого а зачем на этот метод отдельно? вроде бы мы storage'ы закрываем протоколами и там есть функция для этого
nikita.semenov
commented
Проблема в том, что из методов, в которых мы удаляем значение из хранилища, Value ограничен протоколом Decodable. А стандартный метод удаления требует, чтобы Value был Decodable. Нашел вот такой способ обойти ограничение без изменения сигнатуры метода удаления Проблема в том, что из методов, в которых мы удаляем значение из хранилища, Value ограничен протоколом Decodable. А стандартный метод удаления требует, чтобы Value был Decodable. Нашел вот такой способ обойти ограничение без изменения сигнатуры метода удаления
|
||||
sourceStorage.removeValue(forKey: key)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/*:
|
||||
## KeychainMigratingCodableBackingStore
|
||||
*/
|
||||
import TIFoundationUtils
|
||||
import TIKeychainUtils
|
||||
import KeychainAccess
|
||||
|
||||
extension StorageKey {
|
||||
static var knownPins: StorageKey<[String: Set<String>]> {
|
||||
.init(rawValue: "knownPins")
|
||||
}
|
||||
}
|
||||
|
||||
extension Keychain {
|
||||
static var keychain: Keychain {
|
||||
.init()
|
||||
}
|
||||
|
||||
static var groupKeychain: Keychain {
|
||||
.init(service: "app.group.identifier")
|
||||
}
|
||||
}
|
||||
|
||||
extension KeychainMigratingStorageContainer {
|
||||
static var defaultContainer: KeychainMigratingStorageContainer {
|
||||
.init(sourceStorage: .keychain, targetStorage: .groupKeychain)
|
||||
}
|
||||
}
|
||||
|
||||
struct PinningManager {
|
||||
|
||||
// Migration from keychain to groupKeychain
|
||||
// @KeychainCodableBackingStore(key: .knownPins, codableKeyValueStorage: .keychain)
|
||||
@KeychainMigratingCodableBackingStore(key: .knownPins, storageContainer: .defaultContainer)
|
||||
var knownPins = [String: Set<String>]()
|
||||
}
|
||||
|
|
@ -96,9 +96,9 @@ extension StorageKey {
|
|||
}
|
||||
|
||||
let accessTokenStorage = DefaultSingleValueCodableStorage(storage: keychain,
|
||||
storageKey: .accessToken,
|
||||
decoder: JSONKeyValueDecoder(),
|
||||
encoder: JSONKeyValueEncoder())
|
||||
storageKey: .accessToken,
|
||||
decoder: JSONKeyValueDecoder(),
|
||||
encoder: JSONKeyValueEncoder())
|
||||
|
||||
let expirationCheckStorage = accessTokenStorage.isExpireCheck { $0.expiration.timeIntervalSinceNow > 0 }
|
||||
|
||||
|
|
@ -107,10 +107,26 @@ case let .success(token):
|
|||
// use token
|
||||
break
|
||||
case let .failure(storageError):
|
||||
if .valueNotFound == storageError {
|
||||
if case .valueNotFound = storageError {
|
||||
// token is missing or expired, request new token
|
||||
} else {
|
||||
// handle storage error
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
/*:
|
||||
### MigratingStorage
|
||||
|
||||
При необходимости мигрировать с одного keychain на другой можно воспользоваться классом `MigratingSingleValueStorage`. При создании "мигрирующего" хранилища необходимо будет указать:
|
||||
|
||||
- source storage: хранилище с которого мигрируем
|
||||
- target storage: хранилище на которое мигрируем
|
||||
*/
|
||||
|
||||
let groupKeychain = Keychain(service: "app.group.identifier")
|
||||
let targetApiTokenKeychainStorage = StringValueKeychainStorage(keychain: groupKeychain, storageKey: .apiToken)
|
||||
|
||||
let migratingApiTokenKeychainStorage = apiTokenKeychainStorage.migrating(to: targetApiTokenKeychainStorage)
|
||||
|
||||
let token = migratingApiTokenKeychainStorage.getValue()
|
||||
|
|
|
|||
|
|
@ -1,2 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<playground version='6.0' target-platform='ios' display-mode='raw' buildActiveScheme='true'/>
|
||||
<playground version='6.0' target-platform='ios' display-mode='raw' buildActiveScheme='true'>
|
||||
<pages>
|
||||
<page name='SingleValueStorage'/>
|
||||
<page name='KeychainCodableBackingStore'/>
|
||||
</pages>
|
||||
</playground>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIKeychainUtils'
|
||||
s.version = '1.48.0'
|
||||
s.version = '1.49.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.48.0'
|
||||
s.version = '1.49.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.48.0'
|
||||
s.version = '1.49.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' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIMoyaNetworking'
|
||||
s.version = '1.48.0'
|
||||
s.version = '1.49.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' }
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ open class DefaultFingerprintsProvider: FingerprintsProvider {
|
|||
errorLogger: ErrorLogger = TINetworkingLogger(category: "Fingerprints"))
|
||||
where Storage.ValueType == FingerprintsMapping, Storage.ErrorType == StorageError {
|
||||
|
||||
self.secureStorage = secureStorage.eraseToAnySingleValueStorate()
|
||||
self.secureStorage = secureStorage.eraseToAnySingleValueStorage()
|
||||
self.bundledFingerprints = bundledFingerprints
|
||||
self.errorLogger = errorLogger
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TINetworking'
|
||||
s.version = '1.48.0'
|
||||
s.version = '1.49.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.48.0'
|
||||
s.version = '1.49.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.48.0'
|
||||
s.version = '1.49.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.48.0'
|
||||
s.version = '1.49.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' }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
@propertyWrapper public struct MigratingBackingStore<MigratingStorages, StoreContent> {
|
||||
|
||||
public typealias InitClosure = (StoreContent) -> MigratingStorages
|
||||
public typealias GetClosure = (MigratingStorages) -> StoreContent
|
||||
public typealias SetClosure = (MigratingStorages, StoreContent) -> Void
|
||||
|
||||
private let getClosure: GetClosure
|
||||
private let setClosure: SetClosure
|
||||
|
||||
private let storageContainer: MigratingStorages
|
||||
|
||||
public init(wrappedValue: StoreContent,
|
||||
initStorageClosure: InitClosure,
|
||||
getClosure: @escaping GetClosure,
|
||||
setClosure: @escaping SetClosure) {
|
||||
|
||||
self.storageContainer = initStorageClosure(wrappedValue)
|
||||
self.getClosure = getClosure
|
||||
self.setClosure = setClosure
|
||||
}
|
||||
|
||||
public init(storageContainer: MigratingStorages,
|
||||
getClosure: @escaping GetClosure,
|
||||
setClosure: @escaping SetClosure) {
|
||||
|
||||
self.storageContainer = storageContainer
|
||||
self.getClosure = getClosure
|
||||
self.setClosure = setClosure
|
||||
}
|
||||
|
||||
public var wrappedValue: StoreContent {
|
||||
get {
|
||||
getClosure(storageContainer)
|
||||
}
|
||||
set {
|
||||
setClosure(storageContainer, newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
//
|
||||
// Copyright (c) 2022 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
open class MigratingStorageContainer<Source, Target> {
|
||||
|
||||
public let sourceStorage: Source
|
||||
public let targetStorage: Target
|
||||
|
||||
public init(sourceStorage: Source, targetStorage: Target) {
|
||||
self.sourceStorage = sourceStorage
|
||||
self.targetStorage = targetStorage
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TISwiftUtils'
|
||||
s.version = '1.48.0'
|
||||
s.version = '1.49.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.48.0'
|
||||
s.version = '1.49.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.48.0'
|
||||
s.version = '1.49.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' }
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TIUIKitCore
|
||||
import UIKit
|
||||
|
||||
open class BaseViewSkeletonsConfiguration {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
//
|
||||
|
||||
import TISwiftUtils
|
||||
import TIUIKitCore
|
||||
import UIKit
|
||||
|
||||
open class TextSkeletonsConfiguration: BaseViewSkeletonsConfiguration {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = 'TIUIElements'
|
||||
s.version = '1.48.0'
|
||||
s.version = '1.49.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.48.0'
|
||||
s.version = '1.49.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.48.0'
|
||||
s.version = '1.49.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.48.0'
|
||||
s.version = '1.49.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' }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
//
|
||||
// Copyright (c) 2023 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TIFoundationUtils
|
||||
|
||||
extension StorageKey {
|
||||
static var refreshToken: StorageKey<String> {
|
||||
.init(rawValue: "refreshToken")
|
||||
}
|
||||
|
||||
static var profile: StorageKey<Profile> {
|
||||
.init(rawValue: "profile")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
//
|
||||
// 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 XCTest
|
||||
import TIFoundationUtils
|
||||
|
ivan.smolin marked this conversation as resolved
Outdated
ivan.smolin
commented
тут правда нужен именно testable import? расскажи плз до чего нужен доступ тут правда нужен именно testable import? расскажи плз до чего нужен доступ
|
||||
|
||||
final class MigratingBackingStoreTests: XCTestCase {
|
||||
|
||||
private let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage
|
||||
private let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage
|
||||
private let encoder = JSONKeyValueEncoder()
|
||||
private let decoder = JSONKeyValueDecoder()
|
||||
|
||||
@MockMigratingCodableBackingStore(key: .profile, storageContainer: .defaultContainer)
|
||||
var profile: Profile?
|
||||
|
||||
override func setUp() {
|
||||
profile = nil
|
||||
|
ivan.smolin marked this conversation as resolved
Outdated
ivan.smolin
commented
profile = nil не затрёт в source и target? profile = nil не затрёт в source и target?
nikita.semenov
commented
Затрет, поправил Затрет, поправил
|
||||
}
|
||||
|
||||
// MARK: - Read Tests
|
||||
|
||||
// Precondition - source: ✅ target: ✅
|
||||
// PostCondition - source: ❌ target: ✅
|
||||
func testGetValue() throws {
|
||||
|
ivan.smolin marked this conversation as resolved
Outdated
ivan.smolin
commented
результат тоже надо через XCTAssert проверять результат тоже надо через XCTAssert проверять
|
||||
let oldProfile = Profile(name: "old", age: 0)
|
||||
let newProfile = Profile(name: "new", age: 0)
|
||||
|
ivan.smolin marked this conversation as resolved
ivan.smolin
commented
может вынести их в property TestCase'а? а то получается лишнее дублирование двух строк в каждом кейсе может вынести их в property TestCase'а? а то получается лишнее дублирование двух строк в каждом кейсе
|
||||
|
||||
let _ = sourceStorage.set(encodableObject: oldProfile, forKey: .profile, encoder: encoder)
|
||||
let _ = targetStorage.set(encodableObject: newProfile, forKey: .profile, encoder: encoder)
|
||||
|
ivan.smolin marked this conversation as resolved
Outdated
ivan.smolin
commented
в итоге, тут протестирована просто работа targetSource? если да, то этот тест надо в отдельный набор тестов надо вынести. так как тут тесты про миграцию в итоге, тут протестирована просто работа targetSource? если да, то этот тест надо в отдельный набор тестов надо вынести. так как тут тесты про миграцию
|
||||
|
||||
XCTAssertEqual(profile, newProfile)
|
||||
XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get())
|
||||
XCTAssertNoThrow(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get())
|
||||
}
|
||||
|
||||
// Precondition - source: ❌ target: ✅
|
||||
|
ivan.smolin marked this conversation as resolved
Outdated
ivan.smolin
commented
optional try тут вроде не нужен, так как функция декларирует throws optional try тут вроде не нужен, так как функция декларирует throws
|
||||
// PostCondition - source: ❌ target: ✅
|
||||
func testGetValueWithNoSource() throws {
|
||||
let newProfile = Profile(name: "new", age: 0)
|
||||
|
||||
let _ = targetStorage.set(encodableObject: newProfile, forKey: .profile, encoder: encoder)
|
||||
|
||||
XCTAssertEqual(profile, newProfile)
|
||||
XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get())
|
||||
XCTAssertNoThrow(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get())
|
||||
}
|
||||
|
ivan.smolin marked this conversation as resolved
Outdated
ivan.smolin
commented
аналогично, про optional try аналогично, про optional try
|
||||
|
||||
// Precondition - source: ✅ target: ❌
|
||||
// PostCondition - source: ❌ target: ✅
|
||||
func testGetValueWithNoTarget() throws {
|
||||
let oldProfile = Profile(name: "old", age: 0)
|
||||
|
||||
let _ = sourceStorage.set(encodableObject: oldProfile, forKey: .profile, encoder: encoder)
|
||||
|
||||
XCTAssertEqual(profile, oldProfile)
|
||||
XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get())
|
||||
XCTAssertNoThrow(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get())
|
||||
}
|
||||
|
||||
// Precondition - source: ❌ target: ❌
|
||||
// PostCondition - source: ❌ target: ❌
|
||||
func testGetValueWithNoValues() throws {
|
||||
XCTAssertEqual(profile, nil)
|
||||
XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get())
|
||||
XCTAssertThrowsError(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get())
|
||||
}
|
||||
|
||||
// MARK: - Write Tests
|
||||
|
||||
// Precondition - source: ✅ target: ✅
|
||||
// PostCondition - source: ❌ target: ✅
|
||||
func testStoreValue() throws {
|
||||
let oldProfile = Profile(name: "old", age: 0)
|
||||
let newProfile = Profile(name: "new", age: 0)
|
||||
let currentProfile = Profile(name: "name", age: 0)
|
||||
|
||||
let _ = sourceStorage.set(encodableObject: oldProfile, forKey: .profile, encoder: encoder)
|
||||
let _ = targetStorage.set(encodableObject: newProfile, forKey: .profile, encoder: encoder)
|
||||
profile = currentProfile
|
||||
|
||||
XCTAssertEqual(profile, currentProfile)
|
||||
XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get())
|
||||
XCTAssertEqual(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get(), currentProfile)
|
||||
}
|
||||
|
||||
// Precondition - source: ❌ target: ✅
|
||||
// PostCondition - source: ❌ target: ✅
|
||||
func testStoreValueWithNoSource() throws {
|
||||
let newProfile = Profile(name: "new", age: 0)
|
||||
let currentProfile = Profile(name: "name", age: 0)
|
||||
|
||||
let _ = targetStorage.set(encodableObject: newProfile, forKey: .profile, encoder: encoder)
|
||||
profile = currentProfile
|
||||
|
||||
XCTAssertEqual(profile, currentProfile)
|
||||
XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get())
|
||||
XCTAssertEqual(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get(), currentProfile)
|
||||
}
|
||||
|
||||
// Precondition - source: ✅ target: ❌
|
||||
// PostCondition - source: ❌ target: ✅
|
||||
func testStoreValueWithNoTarget() throws {
|
||||
let oldProfile = Profile(name: "old", age: 0)
|
||||
let currentProfile = Profile(name: "name", age: 0)
|
||||
|
||||
let _ = sourceStorage.set(encodableObject: oldProfile, forKey: .profile, encoder: encoder)
|
||||
profile = currentProfile
|
||||
|
||||
XCTAssertEqual(profile, currentProfile)
|
||||
XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get())
|
||||
XCTAssertEqual(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get(), currentProfile)
|
||||
}
|
||||
|
||||
// Precondition - source: ❌ target: ❌
|
||||
// PostCondition - source: ❌ target: ✅
|
||||
func testStoreValueWithNoValues() throws {
|
||||
let currentProfile = Profile(name: "name", age: 0)
|
||||
|
||||
profile = currentProfile
|
||||
|
||||
XCTAssertEqual(profile, currentProfile)
|
||||
XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get())
|
||||
XCTAssertEqual(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get(), currentProfile)
|
||||
}
|
||||
|
||||
// MARK: - Delete Tests
|
||||
|
||||
// Precondition - source: ✅ target: ✅
|
||||
// PostCondition - source: ❌ target: ❌
|
||||
func testDeleteValue() throws {
|
||||
let oldProfile = Profile(name: "old", age: 0)
|
||||
let newProfile = Profile(name: "new", age: 0)
|
||||
|
||||
let _ = sourceStorage.set(encodableObject: oldProfile, forKey: .profile, encoder: encoder)
|
||||
let _ = targetStorage.set(encodableObject: newProfile, forKey: .profile, encoder: encoder)
|
||||
profile = nil
|
||||
|
||||
XCTAssertEqual(profile, nil)
|
||||
XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get())
|
||||
XCTAssertThrowsError(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get())
|
||||
}
|
||||
|
||||
// Precondition - source: ❌ target: ✅
|
||||
// PostCondition - source: ❌ target: ❌
|
||||
func testDeleteValueWithNoSource() throws {
|
||||
let newProfile = Profile(name: "new", age: 0)
|
||||
|
||||
let _ = targetStorage.set(encodableObject: newProfile, forKey: .profile, encoder: encoder)
|
||||
profile = nil
|
||||
|
||||
XCTAssertEqual(profile, nil)
|
||||
XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get())
|
||||
XCTAssertThrowsError(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get())
|
||||
}
|
||||
|
||||
// Precondition - source: ✅ target: ❌
|
||||
// PostCondition - source: ❌ target: ❌
|
||||
func testDeleteValueWithNoTarget() throws {
|
||||
let oldProfile = Profile(name: "old", age: 0)
|
||||
|
||||
let _ = sourceStorage.set(encodableObject: oldProfile, forKey: .profile, encoder: encoder)
|
||||
profile = nil
|
||||
|
||||
XCTAssertEqual(profile, nil)
|
||||
XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get())
|
||||
XCTAssertThrowsError(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get())
|
||||
}
|
||||
|
||||
// Precondition - source: ❌ target: ❌
|
||||
// PostCondition - source: ❌ target: ❌
|
||||
func testDeleteValueWithNoValues() throws {
|
||||
profile = nil
|
||||
|
||||
XCTAssertEqual(profile, nil)
|
||||
XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get())
|
||||
XCTAssertThrowsError(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
//
|
||||
// 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 XCTest
|
||||
import TIFoundationUtils
|
||||
|
||||
final class MigratingSingleValueStorageTests: XCTestCase {
|
||||
|
||||
private let sourceRefreshTokenStorage = StringSingleValueMockStorage(storage: .defaultStorage,
|
||||
storageKey: .refreshToken)
|
||||
|
||||
private let targetRefreshTokenStorage = StringSingleValueMockStorage(storage: .defaultGroupStorage,
|
||||
storageKey: .refreshToken)
|
||||
|
||||
public lazy var refreshToken = MigratingSingleValueStorage(sourceStorage: sourceRefreshTokenStorage,
|
||||
targetStorage: targetRefreshTokenStorage,
|
||||
errorLogger: MockStorageLogger.defaultLogger)
|
||||
|
||||
override func setUp() {
|
||||
let _ = sourceRefreshTokenStorage.deleteValue()
|
||||
let _ = targetRefreshTokenStorage.deleteValue()
|
||||
let _ = MockStorageLogger.defaultLogger.getError()
|
||||
}
|
||||
|
||||
// MARK: - Read Tests
|
||||
|
||||
// Precondition - source: ✅ target: ✅
|
||||
// PostCondition - source: ❌ target: ✅
|
||||
func testGetValue() throws {
|
||||
let oldToken = "old-token"
|
||||
let newToken = "new-token"
|
||||
|
||||
let _ = sourceRefreshTokenStorage.store(value: oldToken)
|
||||
let _ = targetRefreshTokenStorage.store(value: newToken)
|
||||
|
||||
XCTAssertEqual(try refreshToken.getValue().get(), newToken)
|
||||
XCTAssertNil(MockStorageLogger.defaultLogger.getError())
|
||||
XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get())
|
||||
XCTAssertNoThrow(try targetRefreshTokenStorage.getValue().get())
|
||||
}
|
||||
|
||||
// Precondition - source: ❌ target: ✅
|
||||
// PostCondition - source: ❌ target: ✅
|
||||
func testGetValueWithNoSource() throws {
|
||||
let newToken = "new-token"
|
||||
|
||||
let _ = targetRefreshTokenStorage.store(value: newToken)
|
||||
|
||||
XCTAssertEqual(try refreshToken.getValue().get(), newToken)
|
||||
XCTAssertNil(MockStorageLogger.defaultLogger.getError())
|
||||
XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get())
|
||||
XCTAssertNoThrow(try targetRefreshTokenStorage.getValue().get())
|
||||
}
|
||||
|
||||
// Precondition - source: ✅ target: ❌
|
||||
// PostCondition - source: ❌ target: ✅
|
||||
func testGetValueWithNoTarget() throws {
|
||||
let oldToken = "old-token"
|
||||
|
||||
let _ = sourceRefreshTokenStorage.store(value: oldToken)
|
||||
|
||||
XCTAssertEqual(try refreshToken.getValue().get(), oldToken)
|
||||
XCTAssertNil(MockStorageLogger.defaultLogger.getError())
|
||||
XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get())
|
||||
XCTAssertNoThrow(try targetRefreshTokenStorage.getValue().get())
|
||||
}
|
||||
|
||||
// Precondition - source: ❌ target: ❌
|
||||
// PostCondition - source: ❌ target: ❌
|
||||
func testGetValueWithNoValues() throws {
|
||||
XCTAssertThrowsError(try refreshToken.getValue().get())
|
||||
XCTAssertNil(MockStorageLogger.defaultLogger.getError())
|
||||
XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get())
|
||||
XCTAssertThrowsError(try targetRefreshTokenStorage.getValue().get())
|
||||
}
|
||||
|
||||
// MARK: - Write Tests
|
||||
|
||||
// Precondition - source: ✅ target: ✅
|
||||
// PostCondition - source: ❌ target: ✅
|
||||
func testStoreValue() throws {
|
||||
let oldToken = "old-token"
|
||||
let newToken = "new-token"
|
||||
let currentToken = "token"
|
||||
|
||||
let _ = sourceRefreshTokenStorage.store(value: oldToken)
|
||||
let _ = targetRefreshTokenStorage.store(value: newToken)
|
||||
|
||||
XCTAssertNoThrow(try refreshToken.store(value: currentToken).get())
|
||||
XCTAssertNil(MockStorageLogger.defaultLogger.getError())
|
||||
XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get())
|
||||
XCTAssertEqual(try targetRefreshTokenStorage.getValue().get(), currentToken)
|
||||
}
|
||||
|
||||
// Precondition - source: ❌ target: ✅
|
||||
// PostCondition - source: ❌ target: ✅
|
||||
func testStoreValueWithNoSource() throws {
|
||||
let newToken = "new-token"
|
||||
let currentToken = "token"
|
||||
|
||||
let _ = targetRefreshTokenStorage.store(value: newToken)
|
||||
|
||||
XCTAssertNoThrow(try refreshToken.store(value: currentToken).get())
|
||||
XCTAssertNil(MockStorageLogger.defaultLogger.getError())
|
||||
XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get())
|
||||
XCTAssertEqual(try targetRefreshTokenStorage.getValue().get(), currentToken)
|
||||
}
|
||||
|
||||
// Precondition - source: ✅ target: ❌
|
||||
// PostCondition - source: ❌ target: ✅
|
||||
func testStoreValueWithNoTarget() throws {
|
||||
let oldToken = "old-token"
|
||||
let currentToken = "token"
|
||||
|
||||
let _ = sourceRefreshTokenStorage.store(value: oldToken)
|
||||
|
||||
XCTAssertNoThrow(try refreshToken.store(value: currentToken).get())
|
||||
XCTAssertNil(MockStorageLogger.defaultLogger.getError())
|
||||
XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get())
|
||||
XCTAssertEqual(try targetRefreshTokenStorage.getValue().get(), currentToken)
|
||||
}
|
||||
|
||||
// Precondition - source: ❌ target: ❌
|
||||
// PostCondition - source: ❌ target: ✅
|
||||
func testStoreValueWithNoValues() throws {
|
||||
let currentToken = "token"
|
||||
|
||||
XCTAssertNoThrow(try refreshToken.store(value: currentToken).get())
|
||||
XCTAssertNil(MockStorageLogger.defaultLogger.getError())
|
||||
XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get())
|
||||
XCTAssertEqual(try targetRefreshTokenStorage.getValue().get(), currentToken)
|
||||
}
|
||||
|
||||
// MARK: - Delete Tests
|
||||
|
||||
// Precondition - source: ✅ target: ✅
|
||||
// PostCondition - source: ❌ target: ❌
|
||||
func testDeleteValue() throws {
|
||||
let oldToken = "old-token"
|
||||
let newToken = "new-token"
|
||||
|
||||
let _ = sourceRefreshTokenStorage.store(value: oldToken)
|
||||
let _ = targetRefreshTokenStorage.store(value: newToken)
|
||||
|
||||
XCTAssertNoThrow(try refreshToken.deleteValue().get())
|
||||
XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get())
|
||||
XCTAssertThrowsError(try targetRefreshTokenStorage.getValue().get())
|
||||
}
|
||||
|
||||
// Precondition - source: ❌ target: ✅
|
||||
// PostCondition - source: ❌ target: ❌
|
||||
func testDeleteValueWithNoSource() throws {
|
||||
let newToken = "new-token"
|
||||
|
||||
let _ = targetRefreshTokenStorage.store(value: newToken)
|
||||
|
||||
XCTAssertNoThrow(try refreshToken.deleteValue().get())
|
||||
XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get())
|
||||
XCTAssertThrowsError(try targetRefreshTokenStorage.getValue().get())
|
||||
}
|
||||
|
||||
// Precondition - source: ✅ target: ❌
|
||||
// PostCondition - source: ❌ target: ❌
|
||||
func testDeleteValueWithNoTarget() throws {
|
||||
let oldToken = "old-token"
|
||||
|
||||
let _ = sourceRefreshTokenStorage.store(value: oldToken)
|
||||
|
||||
XCTAssertThrowsError(try refreshToken.deleteValue().get())
|
||||
XCTAssertNil(MockStorageLogger.defaultLogger.getError())
|
||||
XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get())
|
||||
XCTAssertThrowsError(try targetRefreshTokenStorage.getValue().get())
|
||||
}
|
||||
|
||||
// Precondition - source: ❌ target: ❌
|
||||
// PostCondition - source: ❌ target: ❌
|
||||
func testDeleteValueWithNoValues() throws {
|
||||
XCTAssertThrowsError(try refreshToken.deleteValue().get())
|
||||
XCTAssertNil(MockStorageLogger.defaultLogger.getError())
|
||||
XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get())
|
||||
XCTAssertThrowsError(try targetRefreshTokenStorage.getValue().get())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
//
|
||||
// Copyright (c) 2023 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TIFoundationUtils
|
||||
|
||||
class BaseSingleValueMockStorage<ValueType>: BaseSingleValueStorage<ValueType, MockStorage> {
|
||||
init(storage: MockStorage,
|
||||
storageKey: StorageKey<ValueType>,
|
||||
getValueClosure: @escaping GetValueClosure,
|
||||
storeValueClosure: @escaping StoreValueClosure) {
|
||||
|
||||
let hasValueClosure: HasValueClosure = { storage, storageKey in
|
||||
.success(storage.hasValue(forKey: storageKey))
|
||||
}
|
||||
|
||||
let deleteValueClosure: DeleteValueClosure = { storage, storageKey in
|
||||
if storage.hasValue(forKey: storageKey) {
|
||||
return .success(storage.deleteValue(forKey: storageKey))
|
||||
}
|
||||
|
||||
return .failure(.valueNotFound)
|
||||
}
|
||||
|
||||
super.init(storage: storage,
|
||||
storageKey: storageKey,
|
||||
hasValueClosure: hasValueClosure,
|
||||
deleteValueClosure: deleteValueClosure,
|
||||
getValueClosure: getValueClosure,
|
||||
storeValueClosure: storeValueClosure)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// Copyright (c) 2023 Touch Instinct
|
||||
|
ivan.smolin marked this conversation as resolved
Outdated
ivan.smolin
commented
2023 2023
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TISwiftUtils
|
||||
import TIFoundationUtils
|
||||
|
||||
typealias MockMigratingCodableBackingStore<T: Codable> = MigratingBackingStore<MockMigratingStorageContainer, T>
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
//
|
||||
// Copyright (c) 2023 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TIFoundationUtils
|
||||
|
||||
final class MockMigratingStorageContainer: BaseCodableMigratingStorageContainer<MockStorage, MockStorage> {
|
||||
|
||||
static let defaultContainer = MockMigratingStorageContainer(sourceStorage: .defaultStorage,
|
||||
targetStorage: .defaultGroupStorage)
|
||||
|
||||
override func removeSourceValue<Value>(forKey key: StorageKey<Value>) -> Result<Void, StorageError> {
|
||||
sourceStorage.removeValue(forKey: key)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
//
|
||||
// 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
|
||||
import TIFoundationUtils
|
||||
import TISwiftUtils
|
||||
|
||||
final class MockStorage {
|
||||
|
||||
static var defaultStorage: MockStorage {
|
||||
.init()
|
||||
}
|
||||
|
||||
static var defaultGroupStorage: MockStorage {
|
||||
.init()
|
||||
}
|
||||
|
||||
private var storage = [String: Any]()
|
||||
|
||||
func hasValue<Value>(forKey key: StorageKey<Value>) -> Bool {
|
||||
storage[key.rawValue] != nil
|
||||
}
|
||||
|
||||
func getValue<Value>(forKey key: StorageKey<Value>) -> Value? {
|
||||
storage[key.rawValue] as? Value
|
||||
}
|
||||
|
||||
func store<Value>(value: Value, forKey key: StorageKey<Value>) {
|
||||
storage[key.rawValue] = value
|
||||
}
|
||||
|
||||
func deleteValue<Value>(forKey key: StorageKey<Value>) {
|
||||
storage.removeValue(forKey: key.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CodableKeyValueStorage
|
||||
|
||||
extension MockStorage: CodableKeyValueStorage {
|
||||
|
||||
public func codableObject<Value: Decodable>(forKey key: StorageKey<Value>,
|
||||
decoder: CodableKeyValueDecoder) -> Result<Value, StorageError> {
|
||||
|
||||
guard let storedData = getData(forKey: key) else {
|
||||
return .failure(.valueNotFound)
|
||||
}
|
||||
|
||||
return decoder.decodeDecodable(from: storedData, for: key)
|
||||
}
|
||||
|
||||
public func set<Value: Encodable>(encodableObject: Value,
|
||||
forKey key: StorageKey<Value>,
|
||||
encoder: CodableKeyValueEncoder) -> Result<Void, StorageError> {
|
||||
|
||||
encoder.encodeEncodable(value: encodableObject, for: key)
|
||||
.map {
|
||||
setData(data: $0, forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
public func removeCodableValue<Value: Codable>(forKey key: StorageKey<Value>) -> Result<Void, StorageError> {
|
||||
removeValue(forKey: key)
|
||||
}
|
||||
|
||||
public func hasCodableValue<Value: Decodable>(forKey key: StorageKey<Value>) -> Result<Bool, StorageError> {
|
||||
guard getData(forKey: key) != nil else {
|
||||
return .success(false)
|
||||
}
|
||||
|
||||
return .success(true)
|
||||
}
|
||||
|
||||
public func removeValue<Value>(forKey key: StorageKey<Value>) -> Result<Void, StorageError> {
|
||||
guard getData(forKey: key) != nil else {
|
||||
return .failure(.valueNotFound)
|
||||
}
|
||||
|
||||
return .success(deleteValue(forKey: key))
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
|
||||
private func getData<Value>(forKey key: StorageKey<Value>) -> Data? {
|
||||
storage[key.rawValue] as? Data
|
||||
}
|
||||
|
||||
private func setData<Value>(data: Data, forKey key: StorageKey<Value>) {
|
||||
storage[key.rawValue] = data
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
//
|
||||
// Copyright (c) 2023 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TIFoundationUtils
|
||||
import TILogging
|
||||
import os
|
||||
|
||||
final class MockStorageLogger: DefaultOSLogErrorLogger {
|
||||
|
||||
static let defaultLogger = MockStorageLogger(category: "MockStorageLogger")
|
||||
|
||||
private var recentError: StorageError?
|
||||
|
||||
init(category: String) {
|
||||
super.init(log: OSLog(subsystem: "TIFoundationUtilsTests", category: category))
|
||||
}
|
||||
|
||||
func getError() -> StorageError? {
|
||||
defer { recentError = nil }
|
||||
|
||||
return recentError
|
||||
}
|
||||
|
||||
override func log(error: Error, file: StaticString, line: Int) {
|
||||
recentError = error as? StorageError
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
// 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
|
||||
|
||||
struct Profile: Codable, Equatable {
|
||||
var name: String
|
||||
var age: Int
|
||||
}
|
||||
|
|
@ -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 TIFoundationUtils
|
||||
|
||||
final class StringSingleValueMockStorage: BaseSingleValueMockStorage<String> {
|
||||
|
||||
init(storage: MockStorage, storageKey: StorageKey<String>) {
|
||||
let getValueClosure: GetValueClosure = { storage, storageKey in
|
||||
guard let value = storage.getValue(forKey: storageKey) else {
|
||||
return .failure(.valueNotFound)
|
||||
}
|
||||
|
||||
return .success(value)
|
||||
}
|
||||
|
||||
let storeValueClosure: StoreValueClosure = { storage, value, storageKey in
|
||||
.success(storage.store(value: value, forKey: storageKey))
|
||||
}
|
||||
|
||||
super.init(storage: storage,
|
||||
storageKey: storageKey,
|
||||
getValueClosure: getValueClosure,
|
||||
storeValueClosure: storeValueClosure)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
|
||||
## KeychainMigratingCodableBackingStore
|
||||
|
||||
```swift
|
||||
import TIFoundationUtils
|
||||
import TIKeychainUtils
|
||||
import KeychainAccess
|
||||
|
||||
extension StorageKey {
|
||||
static var knownPins: StorageKey<[String: Set<String>]> {
|
||||
.init(rawValue: "knownPins")
|
||||
}
|
||||
}
|
||||
|
||||
extension Keychain {
|
||||
static var keychain: Keychain {
|
||||
.init()
|
||||
}
|
||||
|
||||
static var groupKeychain: Keychain {
|
||||
.init(service: "app.group.identifier")
|
||||
}
|
||||
}
|
||||
|
||||
extension KeychainMigratingStorageContainer {
|
||||
static var defaultContainer: KeychainMigratingStorageContainer {
|
||||
.init(sourceStorage: .keychain, targetStorage: .groupKeychain)
|
||||
}
|
||||
}
|
||||
|
||||
struct PinningManager {
|
||||
|
||||
// Migration from keychain to groupKeychain
|
||||
// @KeychainCodableBackingStore(key: .knownPins, codableKeyValueStorage: .keychain)
|
||||
@KeychainMigratingCodableBackingStore(key: .knownPins, storageContainer: .defaultContainer)
|
||||
var knownPins = [String: Set<String>]()
|
||||
}
|
||||
```
|
||||
|
|
@ -94,9 +94,9 @@ extension StorageKey {
|
|||
}
|
||||
|
||||
let accessTokenStorage = DefaultSingleValueCodableStorage(storage: keychain,
|
||||
storageKey: .accessToken,
|
||||
decoder: JSONKeyValueDecoder(),
|
||||
encoder: JSONKeyValueEncoder())
|
||||
storageKey: .accessToken,
|
||||
decoder: JSONKeyValueDecoder(),
|
||||
encoder: JSONKeyValueEncoder())
|
||||
|
||||
let expirationCheckStorage = accessTokenStorage.isExpireCheck { $0.expiration.timeIntervalSinceNow > 0 }
|
||||
|
||||
|
|
@ -105,7 +105,7 @@ case let .success(token):
|
|||
// use token
|
||||
break
|
||||
case let .failure(storageError):
|
||||
if .valueNotFound == storageError {
|
||||
if case .valueNotFound = storageError {
|
||||
// token is missing or expired, request new token
|
||||
} else {
|
||||
// handle storage error
|
||||
|
|
@ -113,3 +113,19 @@ case let .failure(storageError):
|
|||
break
|
||||
}
|
||||
```
|
||||
|
||||
### MigratingStorage
|
||||
|
||||
При необходимости мигрировать с одного keychain на другой можно воспользоваться классом `MigratingSingleValueStorage`. При создании "мигрирующего" хранилища необходимо будет указать:
|
||||
|
||||
- source storage: хранилище с которого мигрируем
|
||||
- target storage: хранилище на которое мигрируем
|
||||
|
||||
```swift
|
||||
let groupKeychain = Keychain(service: "app.group.identifier")
|
||||
let targetApiTokenKeychainStorage = StringValueKeychainStorage(keychain: groupKeychain, storageKey: .apiToken)
|
||||
|
||||
let migratingApiTokenKeychainStorage = apiTokenKeychainStorage.migrating(to: targetApiTokenKeychainStorage)
|
||||
|
||||
let token = migratingApiTokenKeychainStorage.getValue()
|
||||
```
|
||||
|
|
|
|||
storate ?
ну и вообще коммент не понятен
Сделал коммент подробнее. Контейнер не может мигрировать значение из-за того, что в методе
codableObject(forKey:decoder)нет доступа к encoder'у и Value ограничен только Decodable