From 9056b2fe8c44f7552d29acc0aa08bfff11b03846 Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Sun, 25 Jun 2023 20:14:06 +0300 Subject: [PATCH 01/12] feat: migrating storages --- CHANGELOG.md | 4 + TIAppleMapUtils/TIAppleMapUtils.podspec | 2 +- TIAuth/TIAuth.podspec | 2 +- TIDeeplink/TIDeeplink.podspec | 2 +- TIDeveloperUtils/TIDeveloperUtils.podspec | 2 +- TIEcommerce/TIEcommerce.podspec | 2 +- ...eyVlaueStorage+MigratingBackingStore.swift | 103 ++++++++++++++++++ ...eMigratingSingleValueDefaultsStorage.swift | 79 ++++++++++++++ .../BaseMigratingSingleValueStorage.swift | 71 ++++++++++++ .../BoolMigratingValueDefaultsStorage.swift | 82 ++++++++++++++ .../DefaultsMigratingStorageContainer.swift | 69 ++++++++++++ .../StringMigratingValueDefaultsStorage.swift | 81 ++++++++++++++ ...DefaultsMigratingCodableBackingStore.swift | 28 +++++ TIFoundationUtils/TIFoundationUtils.podspec | 2 +- TIGoogleMapUtils/TIGoogleMapUtils.podspec | 2 +- ...KeychainMigratingCodableBackingStore.swift | 29 +++++ ...eMigratingSingleValueKeychainStorage.swift | 80 ++++++++++++++ .../KeychainMigratingStorageContainer.swift | 69 ++++++++++++ .../StringMigratingValueKeychainStorage.swift | 84 ++++++++++++++ .../Contents.swift | 30 +++++ .../Contents.swift | 31 +++++- .../contents.xcplayground | 7 +- TIKeychainUtils/TIKeychainUtils.podspec | 2 +- TILogging/TILogging.podspec | 2 +- TIMapUtils/TIMapUtils.podspec | 2 +- TIMoyaNetworking/TIMoyaNetworking.podspec | 2 +- TINetworking/TINetworking.podspec | 2 +- TINetworkingCache/TINetworkingCache.podspec | 2 +- TIPagination/TIPagination.podspec | 2 +- TISwiftUICore/TISwiftUICore.podspec | 2 +- .../MigratingBackingStore.swift | 72 ++++++++++++ .../MigratingStorageContainer.swift | 32 ++++++ TISwiftUtils/TISwiftUtils.podspec | 2 +- TITableKitUtils/TITableKitUtils.podspec | 2 +- TITextProcessing/TITextProcessing.podspec | 2 +- .../BaseViewSkeletonsConfiguration.swift | 1 + .../TextSkeletonsConfiguration.swift | 1 + TIUIElements/TIUIElements.podspec | 2 +- TIUIKitCore/TIUIKitCore.podspec | 2 +- TIWebView/TIWebView.podspec | 2 +- TIYandexMapUtils/TIYandexMapUtils.podspec | 2 +- .../keychaincodablebackingstore.md | 32 ++++++ docs/tikeychainutils/singlevaluestorage.md | 31 +++++- 43 files changed, 1027 insertions(+), 33 deletions(-) create mode 100644 TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyVlaueStorage+MigratingBackingStore.swift create mode 100644 TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueDefaultsStorage.swift create mode 100644 TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueStorage.swift create mode 100644 TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BoolMigratingValueDefaultsStorage.swift create mode 100644 TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Containers/DefaultsMigratingStorageContainer.swift create mode 100644 TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/StringMigratingValueDefaultsStorage.swift create mode 100644 TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/UserDefaultsMigratingCodableBackingStore.swift create mode 100644 TIKeychainUtils/Sources/KeychainCodableBackingStore/KeychainMigratingCodableBackingStore.swift create mode 100644 TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/BaseMigratingSingleValueKeychainStorage.swift create mode 100644 TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/Container/KeychainMigratingStorageContainer.swift create mode 100644 TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/StringMigratingValueKeychainStorage.swift create mode 100644 TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/KeychainCodableBackingStore.xcplaygroundpage/Contents.swift create mode 100644 TISwiftUtils/Sources/PropertyWrappers/MigratingBackingStore.swift create mode 100644 TISwiftUtils/Sources/PropertyWrappers/MigratingStorageContainer.swift create mode 100644 docs/tikeychainutils/keychaincodablebackingstore.md diff --git a/CHANGELOG.md b/CHANGELOG.md index eaa39c1d..cc2395be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### 1.49.0 + +- **Added**: `BaseMigratingSingleValueKeychainStorage` and `BaseMigratingSingleValueDefaultsStorage` implementations for migrating keys from one storage to another. + ### 1.47.0 - **Added**: `flatMap` operator for `AsyncOperation` diff --git a/TIAppleMapUtils/TIAppleMapUtils.podspec b/TIAppleMapUtils/TIAppleMapUtils.podspec index 37f275b2..59ef13c5 100644 --- a/TIAppleMapUtils/TIAppleMapUtils.podspec +++ b/TIAppleMapUtils/TIAppleMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIAppleMapUtils' - s.version = '1.47.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' } diff --git a/TIAuth/TIAuth.podspec b/TIAuth/TIAuth.podspec index 3a4416da..3125d1ec 100644 --- a/TIAuth/TIAuth.podspec +++ b/TIAuth/TIAuth.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIAuth' - s.version = '1.47.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' } diff --git a/TIDeeplink/TIDeeplink.podspec b/TIDeeplink/TIDeeplink.podspec index b78e4656..04f4b5b0 100644 --- a/TIDeeplink/TIDeeplink.podspec +++ b/TIDeeplink/TIDeeplink.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIDeeplink' - s.version = '1.47.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' } diff --git a/TIDeveloperUtils/TIDeveloperUtils.podspec b/TIDeveloperUtils/TIDeveloperUtils.podspec index 1fd97b1f..3f26677b 100644 --- a/TIDeveloperUtils/TIDeveloperUtils.podspec +++ b/TIDeveloperUtils/TIDeveloperUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIDeveloperUtils' - s.version = '1.47.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' } diff --git a/TIEcommerce/TIEcommerce.podspec b/TIEcommerce/TIEcommerce.podspec index c22841c2..988443f0 100644 --- a/TIEcommerce/TIEcommerce.podspec +++ b/TIEcommerce/TIEcommerce.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIEcommerce' - s.version = '1.47.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' } diff --git a/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyVlaueStorage+MigratingBackingStore.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyVlaueStorage+MigratingBackingStore.swift new file mode 100644 index 00000000..d5fc5caf --- /dev/null +++ b/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyVlaueStorage+MigratingBackingStore.swift @@ -0,0 +1,103 @@ +// +// 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 SourceStorage: CodableKeyValueStorage, + TargetStorage: CodableKeyValueStorage, + StoreContent: Codable { + + init(key: StorageKey, + sourceStorage: SourceStorage, + targetStorage: TargetStorage, + decoder: CodableKeyValueDecoder = JSONKeyValueDecoder(), + encoder: CodableKeyValueEncoder = JSONKeyValueEncoder()) + where StoreContent == Value? { + + let getClosure: GetClosure = { container in + if case let .success(value) = container.targetStorage.codableObject(forKey: key, decoder: decoder) { + return value + } + + if case let .success(value) = container.sourceStorage.codableObject(forKey: key, decoder: decoder) { + let removalResult = container.sourceStorage.removeCodableValue(forKey: key) + + if case .success(_) = removalResult { + let _ = container.targetStorage.set(encodableObject: value, forKey: key, encoder: encoder) + } + + return value + } + + return nil + } + + let setClosure: SetClosure = { container, value in + try? container.targetStorage.setOrRemove(codableObject: value, forKey: key).get() + } + + self.init(sourceStore: sourceStorage, + targetStore: targetStorage, + getClosure: getClosure, + setClosure: setClosure) + } + + init(wrappedValue: Value, + key: StorageKey, + sourceStorage: SourceStorage, + targetStorage: TargetStorage, + decoder: CodableKeyValueDecoder = JSONKeyValueDecoder(), + encoder: CodableKeyValueEncoder = JSONKeyValueEncoder()) + where StoreContent == Value? { + + let getClosure: GetClosure = { container in + if case let .success(value) = container.targetStorage.codableObject(forKey: key, decoder: decoder) { + return value + } + + if case let .success(value) = container.sourceStorage.codableObject(forKey: key, decoder: decoder) { + let removalResult = container.sourceStorage.removeCodableValue(forKey: key) + + if case .success = removalResult { + let _ = container.targetStorage.set(encodableObject: value, forKey: key, encoder: encoder) + } + + return value + } + + if case .success = container.targetStorage.set(encodableObject: wrappedValue, forKey: key, encoder: encoder) { + return wrappedValue + } + + return nil + } + + let setClosure: SetClosure = { container, value in + try? container.targetStorage.setOrRemove(codableObject: value, forKey: key).get() + } + + self.init(sourceStore: sourceStorage, + targetStore: targetStorage, + getClosure: getClosure, + setClosure: setClosure) + } +} diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueDefaultsStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueDefaultsStorage.swift new file mode 100644 index 00000000..fd0e816f --- /dev/null +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueDefaultsStorage.swift @@ -0,0 +1,79 @@ +// +// 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 BaseMigratingSingleValueDefaultsStorage: BaseMigratingSingleValueStorage { + + public init(sourceStorage: UserDefaults, + targetStorage: UserDefaults, + storageKey: StorageKey, + hasValueClosure: @escaping HasValueClosure, + deleteValueClosure: @escaping DeleteValueClosure, + getValueClosure: @escaping GetValueClosure, + storeValueClosure: @escaping StoreValueClosure) { + + let migratingStorages = DefaultsMigratingStorageContainer(sourceStorage: sourceStorage, + targetStorage: targetStorage) + + super.init(migratingStorages: migratingStorages, + storageKey: storageKey, + hasValueClosure: hasValueClosure, + deleteValueClosure: deleteValueClosure, + getValueClosure: getValueClosure, + storeValueClosure: storeValueClosure) + } + + public init(sourceStorage: UserDefaults, + targetStorage: UserDefaults, + storageKey: StorageKey, + decoder: CodableKeyValueDecoder, + encoder: CodableKeyValueEncoder) where ValueType: Codable { + + let hasValueClosure: HasValueClosure = { container, storageKey in + container.hasCodableValue(forKey: storageKey) + } + + let deleteValueClosure: DeleteValueClosure = { container, storageKey in + container.removeCodableValue(forKey: storageKey) + } + + let getValueClosure: GetValueClosure = { container, storageKey in + container.codableObject(forKey: storageKey, decoder: decoder) + } + + let storeValueClosure: StoreValueClosure = { container, value, storageKey in + container.set(encodableObject: value, forKey: storageKey, encoder: encoder) + } + + let migratingStorages = DefaultsMigratingStorageContainer(sourceStorage: sourceStorage, + targetStorage: targetStorage) + + super.init(migratingStorages: migratingStorages, + storageKey: storageKey, + hasValueClosure: hasValueClosure, + deleteValueClosure: deleteValueClosure, + getValueClosure: getValueClosure, + storeValueClosure: storeValueClosure) + } +} diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueStorage.swift new file mode 100644 index 00000000..ffffb046 --- /dev/null +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueStorage.swift @@ -0,0 +1,71 @@ +// +// 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 + +open class BaseMigratingSingleValueStorage: SingleValueStorage { + + public typealias HasValueClosure = (MigratingStorages, StorageKey) -> Result + public typealias GetValueClosure = (MigratingStorages, StorageKey) -> Result + public typealias StoreValueClosure = (MigratingStorages, ValueType, StorageKey) -> Result + public typealias DeleteValueClosure = (MigratingStorages, StorageKey) -> Result + + public let migratingStorages: MigratingStorages + public let storageKey: StorageKey + public let hasValueClosure: HasValueClosure + public let deleteValueClosure: DeleteValueClosure + public let getValueClosure: GetValueClosure + public let storeValueClosure: StoreValueClosure + + public init(migratingStorages: MigratingStorages, + storageKey: StorageKey, + hasValueClosure: @escaping HasValueClosure, + deleteValueClosure: @escaping DeleteValueClosure, + getValueClosure: @escaping GetValueClosure, + storeValueClosure: @escaping StoreValueClosure) { + + self.migratingStorages = migratingStorages + self.storageKey = storageKey + self.hasValueClosure = hasValueClosure + self.deleteValueClosure = deleteValueClosure + self.getValueClosure = getValueClosure + self.storeValueClosure = storeValueClosure + } + + // MARK: - SingleValueStorage + + open func hasStoredValue() -> Bool { + (try? hasValueClosure(migratingStorages, storageKey).get()) ?? false + } + + open func store(value: ValueType) -> Result { + storeValueClosure(migratingStorages, value, storageKey) + } + + open func getValue() -> Result { + getValueClosure(migratingStorages, storageKey) + } + + open func deleteValue() -> Result { + deleteValueClosure(migratingStorages, storageKey) + } +} diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BoolMigratingValueDefaultsStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BoolMigratingValueDefaultsStorage.swift new file mode 100644 index 00000000..e4c1a1e0 --- /dev/null +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BoolMigratingValueDefaultsStorage.swift @@ -0,0 +1,82 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public final class BoolMigratingValueDefaultsStorage: BaseMigratingSingleValueDefaultsStorage { + + public init(sourceStorage: UserDefaults, + targetStorage: UserDefaults, + storageKey: StorageKey) { + + let hasValueClosure: HasValueClosure = { container, storageKey in + container.hasCodableValue(forKey: storageKey) + } + + let deleteValueClosure: DeleteValueClosure = { container, storageKey in + container.removeCodableValue(forKey: storageKey) + } + + let getValueClosure: GetValueClosure = { container, storageKey in + let codableResult = container.codableObject(forKey: storageKey, decoder: UnarchiverKeyValueDecoder()) + + if case .success = codableResult { + return codableResult + } + + return container.getBoolValue(forKey: storageKey) + } + + let storeValueClosure: StoreValueClosure = { container, value, storageKey in + container.set(encodableObject: value, forKey: storageKey, encoder: ArchiverKeyValueEncoder()) + } + + super.init(sourceStorage: sourceStorage, + targetStorage: targetStorage, + storageKey: storageKey, + hasValueClosure: hasValueClosure, + deleteValueClosure: deleteValueClosure, + getValueClosure: getValueClosure, + storeValueClosure: storeValueClosure) + } +} + +extension DefaultsMigratingStorageContainer { + + func getBoolValue(forKey key: StorageKey) -> Result { + if targetStorage.object(forKey: key.rawValue) != nil { + return .success(targetStorage.bool(forKey: key.rawValue)) + } + + if sourceStorage.object(forKey: key.rawValue) != nil { + let value = sourceStorage.bool(forKey: key.rawValue) + + if case .success = set(encodableObject: value, forKey: key, encoder: ArchiverKeyValueEncoder()) { + sourceStorage.removeObject(forKey: key.rawValue) + } + + return .success(value) + } + + return .failure(.valueNotFound) + } +} diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Containers/DefaultsMigratingStorageContainer.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Containers/DefaultsMigratingStorageContainer.swift new file mode 100644 index 00000000..8247fae9 --- /dev/null +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Containers/DefaultsMigratingStorageContainer.swift @@ -0,0 +1,69 @@ +// +// 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 + +open class DefaultsMigratingStorageContainer: MigratingStorageContainer, + CodableKeyValueStorage { + + open func codableObject(forKey key: StorageKey, + decoder: CodableKeyValueDecoder) -> Result { + + if case let .success(value) = targetStorage.codableObject(forKey: key, decoder: decoder) { + return .success(value) + } + + if case let .success(value) = sourceStorage.codableObject(forKey: key, decoder: decoder) { + return .success(value) + } + + return .failure(.valueNotFound) + } + + open func set(encodableObject: Value, + forKey key: StorageKey, + encoder: CodableKeyValueEncoder) -> Result { + + targetStorage.set(encodableObject: encodableObject, forKey: key, encoder: encoder) + } + + open func removeCodableValue(forKey key: StorageKey) -> Result { + if sourceStorage.object(forKey: key.rawValue) != nil { + sourceStorage.removeObject(forKey: key.rawValue) + } + + guard targetStorage.object(forKey: key.rawValue) != nil else { + return .failure(.valueNotFound) + } + + return .success(targetStorage.removeObject(forKey: key.rawValue)) + } + + open func hasCodableValue(forKey key: StorageKey) -> Result { + if targetStorage.object(forKey: key.rawValue) != nil { + return .success(true) + } + + return .success(sourceStorage.object(forKey: key.rawValue) != nil) + } +} diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/StringMigratingValueDefaultsStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/StringMigratingValueDefaultsStorage.swift new file mode 100644 index 00000000..c3dfca84 --- /dev/null +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/StringMigratingValueDefaultsStorage.swift @@ -0,0 +1,81 @@ +// +// 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 StringMigratingValueDefaultsStorage: BaseMigratingSingleValueDefaultsStorage { + + public init(sourceStorage: UserDefaults, + targetStorage: UserDefaults, + storageKey: StorageKey) { + + let hasValueClosure: HasValueClosure = { container, storageKey in + container.hasCodableValue(forKey: storageKey) + } + + let deleteValueClosure: DeleteValueClosure = { container, storageKey in + container.removeCodableValue(forKey: storageKey) + } + + let getValueClosure: GetValueClosure = { container, storageKey in + let codableResult = container.codableObject(forKey: storageKey, decoder: UnarchiverKeyValueDecoder()) + + if case .success = codableResult { + return codableResult + } + + return container.getStringValue(forKey: storageKey) + } + + let storeValueClosure: StoreValueClosure = { container, value, storageKey in + container.set(encodableObject: value, forKey: storageKey, encoder: ArchiverKeyValueEncoder()) + } + + super.init(sourceStorage: sourceStorage, + targetStorage: targetStorage, + storageKey: storageKey, + hasValueClosure: hasValueClosure, + deleteValueClosure: deleteValueClosure, + getValueClosure: getValueClosure, + storeValueClosure: storeValueClosure) + } +} + + +extension DefaultsMigratingStorageContainer { + + func getStringValue(forKey key: StorageKey) -> Result { + if let value = targetStorage.string(forKey: key.rawValue) { + return .success(value) + } + + if let value = sourceStorage.string(forKey: key.rawValue) { + if case .success = set(encodableObject: value, forKey: key, encoder: ArchiverKeyValueEncoder()) { + sourceStorage.removeObject(forKey: key.rawValue) + } + + return .success(value) + } + + return .failure(.valueNotFound) + } +} diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/UserDefaultsMigratingCodableBackingStore.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/UserDefaultsMigratingCodableBackingStore.swift new file mode 100644 index 00000000..ada49aec --- /dev/null +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/UserDefaultsMigratingCodableBackingStore.swift @@ -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 +import TISwiftUtils + +public typealias UserDefaultsMigratingCodableBackingStore = MigratingBackingStore diff --git a/TIFoundationUtils/TIFoundationUtils.podspec b/TIFoundationUtils/TIFoundationUtils.podspec index 1881b9d8..a8f783b2 100644 --- a/TIFoundationUtils/TIFoundationUtils.podspec +++ b/TIFoundationUtils/TIFoundationUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIFoundationUtils' - s.version = '1.47.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' } diff --git a/TIGoogleMapUtils/TIGoogleMapUtils.podspec b/TIGoogleMapUtils/TIGoogleMapUtils.podspec index b5ffdfd9..d371a268 100644 --- a/TIGoogleMapUtils/TIGoogleMapUtils.podspec +++ b/TIGoogleMapUtils/TIGoogleMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIGoogleMapUtils' - s.version = '1.47.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' } diff --git a/TIKeychainUtils/Sources/KeychainCodableBackingStore/KeychainMigratingCodableBackingStore.swift b/TIKeychainUtils/Sources/KeychainCodableBackingStore/KeychainMigratingCodableBackingStore.swift new file mode 100644 index 00000000..58cb7772 --- /dev/null +++ b/TIKeychainUtils/Sources/KeychainCodableBackingStore/KeychainMigratingCodableBackingStore.swift @@ -0,0 +1,29 @@ +// +// 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 KeychainAccess +import TISwiftUtils + +public typealias KeychainMigratingCodableBackingStore = MigratingBackingStore diff --git a/TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/BaseMigratingSingleValueKeychainStorage.swift b/TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/BaseMigratingSingleValueKeychainStorage.swift new file mode 100644 index 00000000..536396a0 --- /dev/null +++ b/TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/BaseMigratingSingleValueKeychainStorage.swift @@ -0,0 +1,80 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import TIFoundationUtils +import KeychainAccess + +open class BaseMigratingSingleValueKeychainStorage: BaseMigratingSingleValueStorage { + + public init(sourceStorage: Keychain, + targetStorage: Keychain, + storageKey: StorageKey, + hasValueClosure: @escaping HasValueClosure, + deleteValueClosure: @escaping DeleteValueClosure, + getValueClosure: @escaping GetValueClosure, + storeValueClosure: @escaping StoreValueClosure) { + + let migratingStorages = KeychainMigratingStorageContainer(sourceStorage: sourceStorage, + targetStorage: targetStorage) + + super.init(migratingStorages: migratingStorages, + storageKey: storageKey, + hasValueClosure: hasValueClosure, + deleteValueClosure: deleteValueClosure, + getValueClosure: getValueClosure, + storeValueClosure: storeValueClosure) + } + + public init(sourceStorage: Keychain, + targetStorage: Keychain, + storageKey: StorageKey, + decoder: CodableKeyValueDecoder, + encoder: CodableKeyValueEncoder) where ValueType: Codable { + + let hasValueClosure: HasValueClosure = { container, storageKey in + container.hasCodableValue(forKey: storageKey) + } + + let deleteValueClosure: DeleteValueClosure = { container, storageKey in + container.removeCodableValue(forKey: storageKey) + } + + let getValueClosure: GetValueClosure = { container, storageKey in + container.codableObject(forKey: storageKey, decoder: decoder) + } + + let storeValueClosure: StoreValueClosure = { container, value, storageKey in + container.set(encodableObject: value, forKey: storageKey, encoder: encoder) + } + + let migratingStorages = KeychainMigratingStorageContainer(sourceStorage: sourceStorage, + targetStorage: targetStorage) + + super.init(migratingStorages: migratingStorages, + storageKey: storageKey, + hasValueClosure: hasValueClosure, + deleteValueClosure: deleteValueClosure, + getValueClosure: getValueClosure, + storeValueClosure: storeValueClosure) + } +} diff --git a/TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/Container/KeychainMigratingStorageContainer.swift b/TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/Container/KeychainMigratingStorageContainer.swift new file mode 100644 index 00000000..56263d3c --- /dev/null +++ b/TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/Container/KeychainMigratingStorageContainer.swift @@ -0,0 +1,69 @@ +// +// 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 TISwiftUtils +import KeychainAccess + +open class KeychainMigratingStorageContainer: MigratingStorageContainer, CodableKeyValueStorage { + + open func codableObject(forKey key: StorageKey, + decoder: CodableKeyValueDecoder) -> Result { + + if case let .success(value) = targetStorage.codableObject(forKey: key, decoder: decoder) { + return .success(value) + } + + if case let .success(value) = sourceStorage.codableObject(forKey: key, decoder: decoder) { + return .success(value) + } + + return .failure(.valueNotFound) + } + + open func set(encodableObject: Value, + forKey key: StorageKey, + encoder: CodableKeyValueEncoder) -> Result { + + targetStorage.set(encodableObject: encodableObject, forKey: key, encoder: encoder) + } + + open func removeCodableValue(forKey key: StorageKey) -> Result { + if case .success = sourceStorage.hasCodableValue(forKey: key) { + let _ = sourceStorage.removeCodableValue(forKey: key) + } + + guard case .success = targetStorage.hasCodableValue(forKey: key) else { + return .failure(.valueNotFound) + } + + return targetStorage.removeCodableValue(forKey: key) + } + + open func hasCodableValue(forKey key: StorageKey) -> Result { + if case .success = targetStorage.hasCodableValue(forKey: key) { + return .success(true) + } + + return sourceStorage.hasCodableValue(forKey: key) + } +} diff --git a/TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/StringMigratingValueKeychainStorage.swift b/TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/StringMigratingValueKeychainStorage.swift new file mode 100644 index 00000000..56ae999e --- /dev/null +++ b/TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/StringMigratingValueKeychainStorage.swift @@ -0,0 +1,84 @@ +// +// 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 StringMigratingValueKeychainStorage: BaseMigratingSingleValueKeychainStorage { + + public init(sourceStorage: Keychain, + targetStorage: Keychain, + storageKey: StorageKey) { + + let hasValueClosure: HasValueClosure = { container, storageKey in + container.hasCodableValue(forKey: storageKey) + } + + let deleteValueClosure: DeleteValueClosure = { container, storageKey in + container.removeCodableValue(forKey: storageKey) + } + + let getValueClosure: GetValueClosure = { container, storageKey in + let codableResult = container.codableObject(forKey: storageKey, decoder: UnarchiverKeyValueDecoder()) + + if case .success = codableResult { + return codableResult + } + + return container.getStringValue(forKey: storageKey) + } + + let storeValueClosure: StoreValueClosure = { container, value, storageKey in + container.set(encodableObject: value, forKey: storageKey, encoder: ArchiverKeyValueEncoder()) + } + + super.init(sourceStorage: sourceStorage, + targetStorage: targetStorage, + storageKey: storageKey, + hasValueClosure: hasValueClosure, + deleteValueClosure: deleteValueClosure, + getValueClosure: getValueClosure, + storeValueClosure: storeValueClosure) + } +} + +extension KeychainMigratingStorageContainer { + + func getStringValue(forKey key: StorageKey) -> Result { + Result { + if let value = try targetStorage.get(key.rawValue) { + return value + } + + if let value = try sourceStorage.get(key.rawValue) { + if case .success = set(encodableObject: value, forKey: key, encoder: ArchiverKeyValueEncoder()) { + try? sourceStorage.remove(key.rawValue) + } + + return value + } + + throw StorageError.valueNotFound + } + .mapError { .unableToExtractData(underlyingError: $0) } + } +} diff --git a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/KeychainCodableBackingStore.xcplaygroundpage/Contents.swift b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/KeychainCodableBackingStore.xcplaygroundpage/Contents.swift new file mode 100644 index 00000000..1272ac5f --- /dev/null +++ b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/KeychainCodableBackingStore.xcplaygroundpage/Contents.swift @@ -0,0 +1,30 @@ +/*: + ## KeychainMigratingCodableBackingStore + */ +import TIFoundationUtils +import TIKeychainUtils +import KeychainAccess + +extension StorageKey { + static var knownPins: StorageKey<[String: Set]> { + .init(rawValue: "knownPins") + } +} + +extension Keychain { + static var keychain: Keychain { + .init() + } + + static var groupKeychain: Keychain { + .init(service: "app.group.identifier") + } +} + +struct PinningManager { + + // Migration from keychain to groupKeychain + // @KeychainCodableBackingStore(key: .knownPins, codableKeyValueStorage: .keychain) + @KeychainMigratingCodableBackingStore(key: .knownPins, sourceStorage: .keychain, targetStorage: .groupKeychain) + var knownPins = [String: Set]() +} diff --git a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift index e1a757c0..0bdb3bb7 100644 --- a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift +++ b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift @@ -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 } @@ -106,11 +106,32 @@ switch expirationCheckStorage.getValue() { case let .success(token): // use token break -case let .failure(storageError) - if .valueNotFound = storageError { +case let .failure(storageError): + if case .valueNotFound = storageError { // token is missing or expired, request new token } else { // handle storage error } break } + +/*: + ### MigratingStorage + + При необходимости мигрировать с одного keychain на другой можно воспользоваться классом `BaseMigratingSingleValueKeychainStorage` и его наследниками. При создании "мигрирующего" хранилища необходимо будет указать: + + - source storage: keychain с которого мигрируем + - target storage: keychain на который мигрируем + */ + +extension StorageKey { + static var registerTokenKey: StorageKey { + .init(rawValue: "registerTokenKey") + } +} + +let groupKeychain = Keychain(service: "app.group.identifier") + +let registerTokenStorage = StringMigratingValueKeychainStorage(sourceStorage: keychain, + targetStorage: groupKeychain, + storageKey: StorageKey.registerTokenKey) diff --git a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/contents.xcplayground b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/contents.xcplayground index 00daa653..3fc64681 100644 --- a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/contents.xcplayground +++ b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/contents.xcplayground @@ -1,2 +1,7 @@ - \ No newline at end of file + + + + + + \ No newline at end of file diff --git a/TIKeychainUtils/TIKeychainUtils.podspec b/TIKeychainUtils/TIKeychainUtils.podspec index 69183289..e1b95183 100644 --- a/TIKeychainUtils/TIKeychainUtils.podspec +++ b/TIKeychainUtils/TIKeychainUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIKeychainUtils' - s.version = '1.47.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' } diff --git a/TILogging/TILogging.podspec b/TILogging/TILogging.podspec index 87d0c220..05211f1c 100644 --- a/TILogging/TILogging.podspec +++ b/TILogging/TILogging.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TILogging' - s.version = '1.47.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' } diff --git a/TIMapUtils/TIMapUtils.podspec b/TIMapUtils/TIMapUtils.podspec index 7475e4b8..cf2051ba 100644 --- a/TIMapUtils/TIMapUtils.podspec +++ b/TIMapUtils/TIMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIMapUtils' - s.version = '1.47.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' } diff --git a/TIMoyaNetworking/TIMoyaNetworking.podspec b/TIMoyaNetworking/TIMoyaNetworking.podspec index f2f8afe1..acc102be 100644 --- a/TIMoyaNetworking/TIMoyaNetworking.podspec +++ b/TIMoyaNetworking/TIMoyaNetworking.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIMoyaNetworking' - s.version = '1.47.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' } diff --git a/TINetworking/TINetworking.podspec b/TINetworking/TINetworking.podspec index ffdf6bec..23417288 100644 --- a/TINetworking/TINetworking.podspec +++ b/TINetworking/TINetworking.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TINetworking' - s.version = '1.47.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' } diff --git a/TINetworkingCache/TINetworkingCache.podspec b/TINetworkingCache/TINetworkingCache.podspec index 6d715d3e..32416764 100644 --- a/TINetworkingCache/TINetworkingCache.podspec +++ b/TINetworkingCache/TINetworkingCache.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TINetworkingCache' - s.version = '1.47.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' } diff --git a/TIPagination/TIPagination.podspec b/TIPagination/TIPagination.podspec index c566dc32..ec5395d1 100644 --- a/TIPagination/TIPagination.podspec +++ b/TIPagination/TIPagination.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIPagination' - s.version = '1.47.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' } diff --git a/TISwiftUICore/TISwiftUICore.podspec b/TISwiftUICore/TISwiftUICore.podspec index 1dcada42..32d0f5f9 100644 --- a/TISwiftUICore/TISwiftUICore.podspec +++ b/TISwiftUICore/TISwiftUICore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TISwiftUICore' - s.version = '1.47.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' } diff --git a/TISwiftUtils/Sources/PropertyWrappers/MigratingBackingStore.swift b/TISwiftUtils/Sources/PropertyWrappers/MigratingBackingStore.swift new file mode 100644 index 00000000..de6a84d9 --- /dev/null +++ b/TISwiftUtils/Sources/PropertyWrappers/MigratingBackingStore.swift @@ -0,0 +1,72 @@ +// +// 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 { + + public typealias StorageContainer = MigratingStorageContainer + public typealias InitSourceClosure = (StoreContent) -> SourceStorage + public typealias InitTargetClosure = (StoreContent) -> TargetStorage + public typealias GetClosure = (StorageContainer) -> StoreContent + public typealias SetClosure = (StorageContainer, StoreContent) -> Void + + private let getClosure: GetClosure + private let setClosure: SetClosure + + private let storageContainer: StorageContainer + + public init(wrappedValue: StoreContent, + sourceInitStorageClosure: InitSourceClosure, + targetInitStorageClosure: InitTargetClosure, + getClosure: @escaping GetClosure, + setClosure: @escaping SetClosure) { + + let sourceStore = sourceInitStorageClosure(wrappedValue) + let targetStore = targetInitStorageClosure(wrappedValue) + + self.storageContainer = StorageContainer(sourceStorage: sourceStore, targetStorage: targetStore) + self.getClosure = getClosure + self.setClosure = setClosure + } + + public init(sourceStore: SourceStorage, + targetStore: TargetStorage, + getClosure: @escaping GetClosure, + setClosure: @escaping SetClosure) { + + self.storageContainer = StorageContainer(sourceStorage: sourceStore, targetStorage: targetStore) + self.getClosure = getClosure + self.setClosure = setClosure + } + + public var wrappedValue: StoreContent { + get { + getClosure(storageContainer) + } + set { + setClosure(storageContainer, newValue) + } + } + + public var projectedValue: TargetStorage { + storageContainer.targetStorage + } +} diff --git a/TISwiftUtils/Sources/PropertyWrappers/MigratingStorageContainer.swift b/TISwiftUtils/Sources/PropertyWrappers/MigratingStorageContainer.swift new file mode 100644 index 00000000..321d3d58 --- /dev/null +++ b/TISwiftUtils/Sources/PropertyWrappers/MigratingStorageContainer.swift @@ -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 { + + public let sourceStorage: Source + public let targetStorage: Target + + public init(sourceStorage: Source, targetStorage: Target) { + self.sourceStorage = sourceStorage + self.targetStorage = targetStorage + } +} diff --git a/TISwiftUtils/TISwiftUtils.podspec b/TISwiftUtils/TISwiftUtils.podspec index ef389fb6..8be4e979 100644 --- a/TISwiftUtils/TISwiftUtils.podspec +++ b/TISwiftUtils/TISwiftUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TISwiftUtils' - s.version = '1.47.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' } diff --git a/TITableKitUtils/TITableKitUtils.podspec b/TITableKitUtils/TITableKitUtils.podspec index 01cdfed1..db3631a9 100644 --- a/TITableKitUtils/TITableKitUtils.podspec +++ b/TITableKitUtils/TITableKitUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TITableKitUtils' - s.version = '1.47.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' } diff --git a/TITextProcessing/TITextProcessing.podspec b/TITextProcessing/TITextProcessing.podspec index 752fba5c..f3e06b0d 100644 --- a/TITextProcessing/TITextProcessing.podspec +++ b/TITextProcessing/TITextProcessing.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TITextProcessing' - s.version = '1.47.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' } diff --git a/TIUIElements/Sources/Views/Skeletons/Configuration/BaseViewSkeletonsConfiguration.swift b/TIUIElements/Sources/Views/Skeletons/Configuration/BaseViewSkeletonsConfiguration.swift index 5ad77c0e..82c65412 100644 --- a/TIUIElements/Sources/Views/Skeletons/Configuration/BaseViewSkeletonsConfiguration.swift +++ b/TIUIElements/Sources/Views/Skeletons/Configuration/BaseViewSkeletonsConfiguration.swift @@ -20,6 +20,7 @@ // THE SOFTWARE. // +import TIUIKitCore import UIKit open class BaseViewSkeletonsConfiguration { diff --git a/TIUIElements/Sources/Views/Skeletons/Configuration/TextSkeletonsConfiguration.swift b/TIUIElements/Sources/Views/Skeletons/Configuration/TextSkeletonsConfiguration.swift index ef68b06a..4819e742 100644 --- a/TIUIElements/Sources/Views/Skeletons/Configuration/TextSkeletonsConfiguration.swift +++ b/TIUIElements/Sources/Views/Skeletons/Configuration/TextSkeletonsConfiguration.swift @@ -21,6 +21,7 @@ // import TISwiftUtils +import TIUIKitCore import UIKit open class TextSkeletonsConfiguration: BaseViewSkeletonsConfiguration { diff --git a/TIUIElements/TIUIElements.podspec b/TIUIElements/TIUIElements.podspec index 30990585..f401396b 100644 --- a/TIUIElements/TIUIElements.podspec +++ b/TIUIElements/TIUIElements.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIUIElements' - s.version = '1.47.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' } diff --git a/TIUIKitCore/TIUIKitCore.podspec b/TIUIKitCore/TIUIKitCore.podspec index 1349a3d2..64ce3b1e 100644 --- a/TIUIKitCore/TIUIKitCore.podspec +++ b/TIUIKitCore/TIUIKitCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIUIKitCore' - s.version = '1.47.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' } diff --git a/TIWebView/TIWebView.podspec b/TIWebView/TIWebView.podspec index ac10df39..6b7eaefb 100644 --- a/TIWebView/TIWebView.podspec +++ b/TIWebView/TIWebView.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIWebView' - s.version = '1.47.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' } diff --git a/TIYandexMapUtils/TIYandexMapUtils.podspec b/TIYandexMapUtils/TIYandexMapUtils.podspec index 6dfd6024..f77de80f 100644 --- a/TIYandexMapUtils/TIYandexMapUtils.podspec +++ b/TIYandexMapUtils/TIYandexMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIYandexMapUtils' - s.version = '1.47.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' } diff --git a/docs/tikeychainutils/keychaincodablebackingstore.md b/docs/tikeychainutils/keychaincodablebackingstore.md new file mode 100644 index 00000000..2c9e545f --- /dev/null +++ b/docs/tikeychainutils/keychaincodablebackingstore.md @@ -0,0 +1,32 @@ + +## KeychainMigratingCodableBackingStore + +```swift +import TIFoundationUtils +import TIKeychainUtils +import KeychainAccess + +extension StorageKey { + static var knownPins: StorageKey<[String: Set]> { + .init(rawValue: "knownPins") + } +} + +extension Keychain { + static var keychain: Keychain { + .init() + } + + static var groupKeychain: Keychain { + .init(service: "app.group.identifier") + } +} + +struct PinningManager { + + // Migration from keychain to groupKeychain + // @KeychainCodableBackingStore(key: .knownPins, codableKeyValueStorage: .keychain) + @KeychainMigratingCodableBackingStore(key: .knownPins, sourceStorage: .keychain, targetStorage: .groupKeychain) + var knownPins = [String: Set]() +} +``` diff --git a/docs/tikeychainutils/singlevaluestorage.md b/docs/tikeychainutils/singlevaluestorage.md index ac420aca..8bdfd6db 100644 --- a/docs/tikeychainutils/singlevaluestorage.md +++ b/docs/tikeychainutils/singlevaluestorage.md @@ -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 } @@ -104,8 +104,8 @@ switch expirationCheckStorage.getValue() { case let .success(token): // use token break -case let .failure(storageError) - if .valueNotFound = storageError { +case let .failure(storageError): + if case .valueNotFound = storageError { // token is missing or expired, request new token } else { // handle storage error @@ -113,3 +113,24 @@ case let .failure(storageError) break } ``` + +### MigratingStorage + + При необходимости мигрировать с одного keychain на другой можно воспользоваться классом `BaseMigratingSingleValueKeychainStorage` и его наследниками. При создании "мигрирующего" хранилища необходимо будет указать: + + - source storage: keychain с которого мигрируем + - target storage: keychain на который мигрируем + +```swift +extension StorageKey { + static var registerTokenKey: StorageKey { + .init(rawValue: "registerTokenKey") + } +} + +let groupKeychain = Keychain(service: "app.group.identifier") + +let registerTokenStorage = StringMigratingValueKeychainStorage(sourceStorage: keychain, + targetStorage: groupKeychain, + storageKey: StorageKey.registerTokenKey) +``` -- 2.40.1 From a06f4952b9217ac883f0a8a530e57a6f3344c250 Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Wed, 28 Jun 2023 09:26:15 +0300 Subject: [PATCH 02/12] fix: reimagine migration storage api --- .../BaseSingleValueStorage.swift | 35 +++++++- ...eMigratingSingleValueDefaultsStorage.swift | 79 ------------------ .../BaseMigratingSingleValueStorage.swift | 67 ++++++++------- .../BoolMigratingValueDefaultsStorage.swift | 82 ------------------- .../DefaultsMigratingStorageContainer.swift | 69 ---------------- .../StringMigratingValueDefaultsStorage.swift | 81 ------------------ .../Contents.swift | 23 +++--- docs/tikeychainutils/singlevaluestorage.md | 26 +++--- 8 files changed, 94 insertions(+), 368 deletions(-) delete mode 100644 TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueDefaultsStorage.swift delete mode 100644 TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BoolMigratingValueDefaultsStorage.swift delete mode 100644 TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Containers/DefaultsMigratingStorageContainer.swift delete mode 100644 TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/StringMigratingValueDefaultsStorage.swift diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/BaseSingleValueStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/BaseSingleValueStorage.swift index 73a2119d..4603f93e 100644 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/BaseSingleValueStorage.swift +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/BaseSingleValueStorage.swift @@ -33,6 +33,8 @@ open class BaseSingleValueStorage: SingleValueStorage { public let getValueClosure: GetValueClosure public let storeValueClosure: StoreValueClosure + public var migrationStorage: StorageType? + public init(storage: StorageType, storageKey: StorageKey, hasValueClosure: @escaping HasValueClosure, @@ -51,18 +53,43 @@ open class BaseSingleValueStorage: SingleValueStorage { // MARK: - SingleValueStorage open func hasStoredValue() -> Bool { - (try? hasValueClosure(storage, storageKey).get()) ?? false + if let migrationStorage, (try? hasValueClosure(migrationStorage, storageKey).get()) == true { + let _ = deleteValueClosure(storage, storageKey) + return true + } + + return (try? hasValueClosure(storage, storageKey).get()) ?? false } open func store(value: ValueType) -> Result { - storeValueClosure(storage, value, storageKey) + let targetStorate = migrationStorage ?? storage + + return storeValueClosure(targetStorate, value, storageKey) } open func getValue() -> Result { - getValueClosure(storage, storageKey) + guard let migrationStorage else { + return getValueClosure(storage, storageKey) + } + + if let value = try? getValueClosure(migrationStorage, storageKey).get() { + return .success(value) + } + + let result = getValueClosure(storage, storageKey) + + if case let .success(value) = result, case .success = store(value: value) { + let _ = deleteValueClosure(storage, storageKey) + } + + return result } open func deleteValue() -> Result { - deleteValueClosure(storage, storageKey) + if let migrationStorage { + return deleteValueClosure(migrationStorage, storageKey) + } + + return deleteValueClosure(storage, storageKey) } } diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueDefaultsStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueDefaultsStorage.swift deleted file mode 100644 index fd0e816f..00000000 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueDefaultsStorage.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// 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 BaseMigratingSingleValueDefaultsStorage: BaseMigratingSingleValueStorage { - - public init(sourceStorage: UserDefaults, - targetStorage: UserDefaults, - storageKey: StorageKey, - hasValueClosure: @escaping HasValueClosure, - deleteValueClosure: @escaping DeleteValueClosure, - getValueClosure: @escaping GetValueClosure, - storeValueClosure: @escaping StoreValueClosure) { - - let migratingStorages = DefaultsMigratingStorageContainer(sourceStorage: sourceStorage, - targetStorage: targetStorage) - - super.init(migratingStorages: migratingStorages, - storageKey: storageKey, - hasValueClosure: hasValueClosure, - deleteValueClosure: deleteValueClosure, - getValueClosure: getValueClosure, - storeValueClosure: storeValueClosure) - } - - public init(sourceStorage: UserDefaults, - targetStorage: UserDefaults, - storageKey: StorageKey, - decoder: CodableKeyValueDecoder, - encoder: CodableKeyValueEncoder) where ValueType: Codable { - - let hasValueClosure: HasValueClosure = { container, storageKey in - container.hasCodableValue(forKey: storageKey) - } - - let deleteValueClosure: DeleteValueClosure = { container, storageKey in - container.removeCodableValue(forKey: storageKey) - } - - let getValueClosure: GetValueClosure = { container, storageKey in - container.codableObject(forKey: storageKey, decoder: decoder) - } - - let storeValueClosure: StoreValueClosure = { container, value, storageKey in - container.set(encodableObject: value, forKey: storageKey, encoder: encoder) - } - - let migratingStorages = DefaultsMigratingStorageContainer(sourceStorage: sourceStorage, - targetStorage: targetStorage) - - super.init(migratingStorages: migratingStorages, - storageKey: storageKey, - hasValueClosure: hasValueClosure, - deleteValueClosure: deleteValueClosure, - getValueClosure: getValueClosure, - storeValueClosure: storeValueClosure) - } -} diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueStorage.swift index ffffb046..78405e25 100644 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueStorage.swift +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueStorage.swift @@ -20,52 +20,61 @@ // THE SOFTWARE. // -import TISwiftUtils +open class BaseMigratingSingleValueStorage: SingleValueStorage +where SourceStorage.ErrorType == TargetStorage.ErrorType, + SourceStorage.ErrorType == StorageError, + SourceStorage.ValueType == TargetStorage.ValueType { -open class BaseMigratingSingleValueStorage: SingleValueStorage { + public typealias ValueType = SourceStorage.ValueType - public typealias HasValueClosure = (MigratingStorages, StorageKey) -> Result - public typealias GetValueClosure = (MigratingStorages, StorageKey) -> Result - public typealias StoreValueClosure = (MigratingStorages, ValueType, StorageKey) -> Result - public typealias DeleteValueClosure = (MigratingStorages, StorageKey) -> Result + public let sourceStorage: SourceStorage + public let targetStorage: TargetStorage - public let migratingStorages: MigratingStorages - public let storageKey: StorageKey - public let hasValueClosure: HasValueClosure - public let deleteValueClosure: DeleteValueClosure - public let getValueClosure: GetValueClosure - public let storeValueClosure: StoreValueClosure + public init(sourceStorage: SourceStorage, + targetStorage: TargetStorage) { - public init(migratingStorages: MigratingStorages, - storageKey: StorageKey, - hasValueClosure: @escaping HasValueClosure, - deleteValueClosure: @escaping DeleteValueClosure, - getValueClosure: @escaping GetValueClosure, - storeValueClosure: @escaping StoreValueClosure) { - - self.migratingStorages = migratingStorages - self.storageKey = storageKey - self.hasValueClosure = hasValueClosure - self.deleteValueClosure = deleteValueClosure - self.getValueClosure = getValueClosure - self.storeValueClosure = storeValueClosure + self.sourceStorage = sourceStorage + self.targetStorage = targetStorage } // MARK: - SingleValueStorage open func hasStoredValue() -> Bool { - (try? hasValueClosure(migratingStorages, storageKey).get()) ?? false + if targetStorage.hasStoredValue() { + let _ = sourceStorage.deleteValue() + + return true + } + + return sourceStorage.hasStoredValue() } open func store(value: ValueType) -> Result { - storeValueClosure(migratingStorages, value, storageKey) + targetStorage.store(value: value) } open func getValue() -> Result { - getValueClosure(migratingStorages, storageKey) + if case let .success(value) = targetStorage.getValue() { + return .success(value) + } + + let result = sourceStorage.getValue() + + if case let .success(value) = result, case .success = store(value: value) { + let _ = sourceStorage.deleteValue() + } + + return result } open func deleteValue() -> Result { - deleteValueClosure(migratingStorages, storageKey) + if targetStorage.hasStoredValue() { + let _ = sourceStorage.deleteValue() + + return targetStorage.deleteValue() + } + + return sourceStorage.deleteValue() } } diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BoolMigratingValueDefaultsStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BoolMigratingValueDefaultsStorage.swift deleted file mode 100644 index e4c1a1e0..00000000 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BoolMigratingValueDefaultsStorage.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// 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 BoolMigratingValueDefaultsStorage: BaseMigratingSingleValueDefaultsStorage { - - public init(sourceStorage: UserDefaults, - targetStorage: UserDefaults, - storageKey: StorageKey) { - - let hasValueClosure: HasValueClosure = { container, storageKey in - container.hasCodableValue(forKey: storageKey) - } - - let deleteValueClosure: DeleteValueClosure = { container, storageKey in - container.removeCodableValue(forKey: storageKey) - } - - let getValueClosure: GetValueClosure = { container, storageKey in - let codableResult = container.codableObject(forKey: storageKey, decoder: UnarchiverKeyValueDecoder()) - - if case .success = codableResult { - return codableResult - } - - return container.getBoolValue(forKey: storageKey) - } - - let storeValueClosure: StoreValueClosure = { container, value, storageKey in - container.set(encodableObject: value, forKey: storageKey, encoder: ArchiverKeyValueEncoder()) - } - - super.init(sourceStorage: sourceStorage, - targetStorage: targetStorage, - storageKey: storageKey, - hasValueClosure: hasValueClosure, - deleteValueClosure: deleteValueClosure, - getValueClosure: getValueClosure, - storeValueClosure: storeValueClosure) - } -} - -extension DefaultsMigratingStorageContainer { - - func getBoolValue(forKey key: StorageKey) -> Result { - if targetStorage.object(forKey: key.rawValue) != nil { - return .success(targetStorage.bool(forKey: key.rawValue)) - } - - if sourceStorage.object(forKey: key.rawValue) != nil { - let value = sourceStorage.bool(forKey: key.rawValue) - - if case .success = set(encodableObject: value, forKey: key, encoder: ArchiverKeyValueEncoder()) { - sourceStorage.removeObject(forKey: key.rawValue) - } - - return .success(value) - } - - return .failure(.valueNotFound) - } -} diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Containers/DefaultsMigratingStorageContainer.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Containers/DefaultsMigratingStorageContainer.swift deleted file mode 100644 index 8247fae9..00000000 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Containers/DefaultsMigratingStorageContainer.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// 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 - -open class DefaultsMigratingStorageContainer: MigratingStorageContainer, - CodableKeyValueStorage { - - open func codableObject(forKey key: StorageKey, - decoder: CodableKeyValueDecoder) -> Result { - - if case let .success(value) = targetStorage.codableObject(forKey: key, decoder: decoder) { - return .success(value) - } - - if case let .success(value) = sourceStorage.codableObject(forKey: key, decoder: decoder) { - return .success(value) - } - - return .failure(.valueNotFound) - } - - open func set(encodableObject: Value, - forKey key: StorageKey, - encoder: CodableKeyValueEncoder) -> Result { - - targetStorage.set(encodableObject: encodableObject, forKey: key, encoder: encoder) - } - - open func removeCodableValue(forKey key: StorageKey) -> Result { - if sourceStorage.object(forKey: key.rawValue) != nil { - sourceStorage.removeObject(forKey: key.rawValue) - } - - guard targetStorage.object(forKey: key.rawValue) != nil else { - return .failure(.valueNotFound) - } - - return .success(targetStorage.removeObject(forKey: key.rawValue)) - } - - open func hasCodableValue(forKey key: StorageKey) -> Result { - if targetStorage.object(forKey: key.rawValue) != nil { - return .success(true) - } - - return .success(sourceStorage.object(forKey: key.rawValue) != nil) - } -} diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/StringMigratingValueDefaultsStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/StringMigratingValueDefaultsStorage.swift deleted file mode 100644 index c3dfca84..00000000 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/StringMigratingValueDefaultsStorage.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// 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 StringMigratingValueDefaultsStorage: BaseMigratingSingleValueDefaultsStorage { - - public init(sourceStorage: UserDefaults, - targetStorage: UserDefaults, - storageKey: StorageKey) { - - let hasValueClosure: HasValueClosure = { container, storageKey in - container.hasCodableValue(forKey: storageKey) - } - - let deleteValueClosure: DeleteValueClosure = { container, storageKey in - container.removeCodableValue(forKey: storageKey) - } - - let getValueClosure: GetValueClosure = { container, storageKey in - let codableResult = container.codableObject(forKey: storageKey, decoder: UnarchiverKeyValueDecoder()) - - if case .success = codableResult { - return codableResult - } - - return container.getStringValue(forKey: storageKey) - } - - let storeValueClosure: StoreValueClosure = { container, value, storageKey in - container.set(encodableObject: value, forKey: storageKey, encoder: ArchiverKeyValueEncoder()) - } - - super.init(sourceStorage: sourceStorage, - targetStorage: targetStorage, - storageKey: storageKey, - hasValueClosure: hasValueClosure, - deleteValueClosure: deleteValueClosure, - getValueClosure: getValueClosure, - storeValueClosure: storeValueClosure) - } -} - - -extension DefaultsMigratingStorageContainer { - - func getStringValue(forKey key: StorageKey) -> Result { - if let value = targetStorage.string(forKey: key.rawValue) { - return .success(value) - } - - if let value = sourceStorage.string(forKey: key.rawValue) { - if case .success = set(encodableObject: value, forKey: key, encoder: ArchiverKeyValueEncoder()) { - sourceStorage.removeObject(forKey: key.rawValue) - } - - return .success(value) - } - - return .failure(.valueNotFound) - } -} diff --git a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift index 0bdb3bb7..845945ed 100644 --- a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift +++ b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift @@ -118,20 +118,19 @@ case let .failure(storageError): /*: ### MigratingStorage - При необходимости мигрировать с одного keychain на другой можно воспользоваться классом `BaseMigratingSingleValueKeychainStorage` и его наследниками. При создании "мигрирующего" хранилища необходимо будет указать: + При необходимости мигрировать с одного keychain на другой можно воспользоваться классом `BaseMigratingSingleValueStorage`. При создании "мигрирующего" хранилища необходимо будет указать: - - source storage: keychain с которого мигрируем - - target storage: keychain на который мигрируем + - source storage: хранилище с которого мигрируем + - target storage: хранилище на которое мигрируем */ -extension StorageKey { - static var registerTokenKey: StorageKey { - .init(rawValue: "registerTokenKey") - } -} - let groupKeychain = Keychain(service: "app.group.identifier") +let targetApiTokenKeychainStorage = StringValueKeychainStorage(keychain: groupKeychain, storageKey: .apiToken) -let registerTokenStorage = StringMigratingValueKeychainStorage(sourceStorage: keychain, - targetStorage: groupKeychain, - storageKey: StorageKey.registerTokenKey) +let migratingApiTokenKeychainStorage = BaseMigratingSingleValueStorage(sourceStorage: apiTokenKeychainStorage, + targetStorage: targetApiTokenKeychainStorage) + +let token = migratingApiTokenKeychainStorage.getValue() + +//: Если нет необходимости в создании target storage, можно воспользоваться переменной `migrationStorage`, которая есть у каждого хранилища, наследованного от `BaseSingleValueStorage` +apiTokenKeychainStorage.migrationStorage = groupKeychain diff --git a/docs/tikeychainutils/singlevaluestorage.md b/docs/tikeychainutils/singlevaluestorage.md index 8bdfd6db..9a8dcc87 100644 --- a/docs/tikeychainutils/singlevaluestorage.md +++ b/docs/tikeychainutils/singlevaluestorage.md @@ -116,21 +116,23 @@ case let .failure(storageError): ### MigratingStorage - При необходимости мигрировать с одного keychain на другой можно воспользоваться классом `BaseMigratingSingleValueKeychainStorage` и его наследниками. При создании "мигрирующего" хранилища необходимо будет указать: + При необходимости мигрировать с одного keychain на другой можно воспользоваться классом `BaseMigratingSingleValueStorage`. При создании "мигрирующего" хранилища необходимо будет указать: - - source storage: keychain с которого мигрируем - - target storage: keychain на который мигрируем + - source storage: хранилище с которого мигрируем + - target storage: хранилище на которое мигрируем ```swift -extension StorageKey { - static var registerTokenKey: StorageKey { - .init(rawValue: "registerTokenKey") - } -} - let groupKeychain = Keychain(service: "app.group.identifier") +let targetApiTokenKeychainStorage = StringValueKeychainStorage(keychain: groupKeychain, storageKey: .apiToken) -let registerTokenStorage = StringMigratingValueKeychainStorage(sourceStorage: keychain, - targetStorage: groupKeychain, - storageKey: StorageKey.registerTokenKey) +let migratingApiTokenKeychainStorage = BaseMigratingSingleValueStorage(sourceStorage: apiTokenKeychainStorage, + targetStorage: targetApiTokenKeychainStorage) + +let token = migratingApiTokenKeychainStorage.getValue() +``` + +Если нет необходимости в создании target storage, можно воспользоваться переменной `migrationStorage`, которая есть у каждого хранилища, наследованного от `BaseSingleValueStorage` + +```swift +apiTokenKeychainStorage.migrationStorage = groupKeychain ``` -- 2.40.1 From 46ecd6970f636c74fc5c46d29f7c248ca8e1416e Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Sun, 2 Jul 2023 17:48:50 +0300 Subject: [PATCH 03/12] fix: migration api refactoring + test coverage --- Package.swift | 6 +- ...eyVlaueStorage+MigratingBackingStore.swift | 59 +------ .../UserDefaults+CodableKeyValueStorage.swift | 14 +- .../BaseMigratingSingleValueStorage.swift | 49 ++--- ...BaseCodableMigratingStorageContainer.swift | 76 ++++++++ .../DefaultsMigratingStorageContainer.swift | 30 ++++ ...DefaultsMigratingCodableBackingStore.swift | 4 +- .../Keychain+CodableKeyValueStorage.swift | 16 +- ...KeychainMigratingCodableBackingStore.swift | 5 +- ...eMigratingSingleValueKeychainStorage.swift | 80 --------- .../KeychainMigratingStorageContainer.swift | 44 +---- .../StringMigratingValueKeychainStorage.swift | 84 --------- .../Contents.swift | 8 +- .../MigratingBackingStore.swift | 29 +-- .../Helpers/StorageKey+Constants.swift | 33 ++++ .../MigratingBackingStoreTests.swift | 93 ++++++++++ .../MigratingSingleValueStorageTests.swift | 167 ++++++++++++++++++ .../Mocks/BaseSingleValueMockStorage.swift | 46 +++++ .../MockMigratingCodableBackingStore.swift | 26 +++ .../Mocks/MockMigratingStorageContainer.swift | 33 ++++ .../Mocks/MockStorage.swift | 109 ++++++++++++ .../Mocks/Profile.swift | 28 +++ .../Mocks/StringSingleValueMockStorage.swift | 45 +++++ .../keychaincodablebackingstore.md | 8 +- 24 files changed, 773 insertions(+), 319 deletions(-) create mode 100644 TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift create mode 100644 TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/DefaultsMigratingStorageContainer.swift delete mode 100644 TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/BaseMigratingSingleValueKeychainStorage.swift delete mode 100644 TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/StringMigratingValueKeychainStorage.swift create mode 100644 Tests/TIFoundationUtilsTests/Helpers/StorageKey+Constants.swift create mode 100644 Tests/TIFoundationUtilsTests/MigratingBackingStoreTests.swift create mode 100644 Tests/TIFoundationUtilsTests/MigratingSingleValueStorageTests.swift create mode 100644 Tests/TIFoundationUtilsTests/Mocks/BaseSingleValueMockStorage.swift create mode 100644 Tests/TIFoundationUtilsTests/Mocks/MockMigratingCodableBackingStore.swift create mode 100644 Tests/TIFoundationUtilsTests/Mocks/MockMigratingStorageContainer.swift create mode 100644 Tests/TIFoundationUtilsTests/Mocks/MockStorage.swift create mode 100644 Tests/TIFoundationUtilsTests/Mocks/Profile.swift create mode 100644 Tests/TIFoundationUtilsTests/Mocks/StringSingleValueMockStorage.swift diff --git a/Package.swift b/Package.swift index 15078619..4f3fe068 100644 --- a/Package.swift +++ b/Package.swift @@ -145,7 +145,11 @@ let package = Package( .testTarget( name: "TITextProcessingTests", dependencies: ["TITextProcessing"], - path: "Tests/TITextProcessingTests") + path: "Tests/TITextProcessingTests"), + .testTarget( + name: "TIFoundationUtilsTests", + dependencies: ["TIFoundationUtils", "TISwiftUtils"], + path: "Tests/TIFoundationUtilsTests") ] ) diff --git a/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyVlaueStorage+MigratingBackingStore.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyVlaueStorage+MigratingBackingStore.swift index d5fc5caf..7620f8cf 100644 --- a/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyVlaueStorage+MigratingBackingStore.swift +++ b/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyVlaueStorage+MigratingBackingStore.swift @@ -22,82 +22,41 @@ import TISwiftUtils -public extension MigratingBackingStore where SourceStorage: CodableKeyValueStorage, - TargetStorage: CodableKeyValueStorage, +public extension MigratingBackingStore where MigratingStorages: CodableKeyValueStorage, StoreContent: Codable { init(key: StorageKey, - sourceStorage: SourceStorage, - targetStorage: TargetStorage, + storageContainer: MigratingStorages, decoder: CodableKeyValueDecoder = JSONKeyValueDecoder(), encoder: CodableKeyValueEncoder = JSONKeyValueEncoder()) where StoreContent == Value? { let getClosure: GetClosure = { container in - if case let .success(value) = container.targetStorage.codableObject(forKey: key, decoder: decoder) { - return value - } - - if case let .success(value) = container.sourceStorage.codableObject(forKey: key, decoder: decoder) { - let removalResult = container.sourceStorage.removeCodableValue(forKey: key) - - if case .success(_) = removalResult { - let _ = container.targetStorage.set(encodableObject: value, forKey: key, encoder: encoder) - } - - return value - } - - return nil + try? container.codableObject(forKey: key, decoder: decoder).get() } let setClosure: SetClosure = { container, value in - try? container.targetStorage.setOrRemove(codableObject: value, forKey: key).get() + try? container.setOrRemove(codableObject: value, forKey: key, encoder: encoder).get() } - self.init(sourceStore: sourceStorage, - targetStore: targetStorage, - getClosure: getClosure, - setClosure: setClosure) + self.init(storageContainer: storageContainer, getClosure: getClosure, setClosure: setClosure) } init(wrappedValue: Value, key: StorageKey, - sourceStorage: SourceStorage, - targetStorage: TargetStorage, + storageContainer: MigratingStorages, decoder: CodableKeyValueDecoder = JSONKeyValueDecoder(), encoder: CodableKeyValueEncoder = JSONKeyValueEncoder()) where StoreContent == Value? { let getClosure: GetClosure = { container in - if case let .success(value) = container.targetStorage.codableObject(forKey: key, decoder: decoder) { - return value - } - - if case let .success(value) = container.sourceStorage.codableObject(forKey: key, decoder: decoder) { - let removalResult = container.sourceStorage.removeCodableValue(forKey: key) - - if case .success = removalResult { - let _ = container.targetStorage.set(encodableObject: value, forKey: key, encoder: encoder) - } - - return value - } - - if case .success = container.targetStorage.set(encodableObject: wrappedValue, forKey: key, encoder: encoder) { - return wrappedValue - } - - return nil + container.codableObject(forKey: key, defaultValue: wrappedValue, decoder: decoder) } let setClosure: SetClosure = { container, value in - try? container.targetStorage.setOrRemove(codableObject: value, forKey: key).get() + try? container.setOrRemove(codableObject: value, forKey: key, encoder: encoder).get() } - self.init(sourceStore: sourceStorage, - targetStore: targetStorage, - getClosure: getClosure, - setClosure: setClosure) + self.init(storageContainer: storageContainer, getClosure: getClosure, setClosure: setClosure) } } diff --git a/TIFoundationUtils/CodableKeyValueStorage/Sources/UserDefaults/UserDefaults+CodableKeyValueStorage.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/UserDefaults/UserDefaults+CodableKeyValueStorage.swift index 0f8dcec6..b3c762f8 100644 --- a/TIFoundationUtils/CodableKeyValueStorage/Sources/UserDefaults/UserDefaults+CodableKeyValueStorage.swift +++ b/TIFoundationUtils/CodableKeyValueStorage/Sources/UserDefaults/UserDefaults+CodableKeyValueStorage.swift @@ -44,11 +44,7 @@ extension UserDefaults: CodableKeyValueStorage { } public func removeCodableValue(forKey key: StorageKey) -> Result { - guard data(forKey: key.rawValue) != nil else { - return .failure(.valueNotFound) - } - - return .success(removeObject(forKey: key.rawValue)) + removeValue(forKey: key) } public func hasCodableValue(forKey key: StorageKey) -> Result { @@ -58,4 +54,12 @@ extension UserDefaults: CodableKeyValueStorage { return .success(true) } + + public func removeValue(forKey key: StorageKey) -> Result { + guard data(forKey: key.rawValue) != nil else { + return .failure(.valueNotFound) + } + + return .success(removeObject(forKey: key.rawValue)) + } } diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueStorage.swift index 78405e25..a1a45a32 100644 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueStorage.swift +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueStorage.swift @@ -41,40 +41,43 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType, // MARK: - SingleValueStorage open func hasStoredValue() -> Bool { - if targetStorage.hasStoredValue() { - let _ = sourceStorage.deleteValue() - - return true - } - - return sourceStorage.hasStoredValue() + targetStorage.hasStoredValue() || sourceStorage.hasStoredValue() } open func store(value: ValueType) -> Result { - targetStorage.store(value: value) + let _ = sourceStorage.deleteValue() + + return targetStorage.store(value: value) } open func getValue() -> Result { - if case let .success(value) = targetStorage.getValue() { - return .success(value) - } + targetStorage.getValue() + .flatMap { + let _ = sourceStorage.deleteValue() - let result = sourceStorage.getValue() + return .success($0) + } + .flatMapError { _ in + sourceStorage.getValue() + .flatMap { value in + Result { + try store(value: value).get() - if case let .success(value) = result, case .success = store(value: value) { - let _ = sourceStorage.deleteValue() - } - - return result + return value + } + .mapError { .unableToExtractData(underlyingError: $0) } + } + } } open func deleteValue() -> Result { - if targetStorage.hasStoredValue() { - let _ = sourceStorage.deleteValue() + targetStorage.deleteValue() + .flatMap { + if sourceStorage.hasStoredValue() { + return sourceStorage.deleteValue() + } - return targetStorage.deleteValue() - } - - return sourceStorage.deleteValue() + return .success($0) + } } } diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift new file mode 100644 index 00000000..766f5d47 --- /dev/null +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift @@ -0,0 +1,76 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import TISwiftUtils + +open class BaseCodableMigratingStorageContainer: MigratingStorageContainer, + CodableKeyValueStorage +where SourceStorage: CodableKeyValueStorage, TargetStorage: CodableKeyValueStorage { + + open func codableObject(forKey key: StorageKey, + decoder: CodableKeyValueDecoder) -> Result { + + targetStorage.codableObject(forKey: key, decoder: decoder) + .flatMap { + removeSourceValue(forKey: key) + + return .success($0) + } + .flatMapError { _ in + sourceStorage.codableObject(forKey: key, decoder: decoder) + } + } + + open func set(encodableObject: Value, + forKey key: StorageKey, + encoder: CodableKeyValueEncoder) -> Result { + + removeSourceValue(forKey: key) + + return targetStorage.set(encodableObject: encodableObject, forKey: key, encoder: encoder) + } + + open func removeCodableValue(forKey key: StorageKey) -> Result { + targetStorage.removeCodableValue(forKey: key) + .flatMap { + if let value = try? sourceStorage.hasCodableValue(forKey: key).get(), value { + return sourceStorage.removeCodableValue(forKey: key) + } + + return .success($0) + } + } + + open func hasCodableValue(forKey key: StorageKey) -> Result { + targetStorage.hasCodableValue(forKey: key) + .flatMapError { _ in + sourceStorage.hasCodableValue(forKey: key) + } + } + + // MARK: - Open methods + + open func removeSourceValue(forKey key: StorageKey) { + // override in subclasses + } +} diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/DefaultsMigratingStorageContainer.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/DefaultsMigratingStorageContainer.swift new file mode 100644 index 00000000..97da6482 --- /dev/null +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/DefaultsMigratingStorageContainer.swift @@ -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 { + + public override func removeSourceValue(forKey key: StorageKey) { + let _ = sourceStorage.removeValue(forKey: key) + } +} diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/UserDefaultsMigratingCodableBackingStore.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/UserDefaultsMigratingCodableBackingStore.swift index ada49aec..add885c7 100644 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/UserDefaultsMigratingCodableBackingStore.swift +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/UserDefaultsMigratingCodableBackingStore.swift @@ -23,6 +23,4 @@ import Foundation import TISwiftUtils -public typealias UserDefaultsMigratingCodableBackingStore = MigratingBackingStore +public typealias DefaultsMigratingCodableBackingStore = MigratingBackingStore diff --git a/TIKeychainUtils/Sources/KeychainCodableBackingStore/Keychain+CodableKeyValueStorage.swift b/TIKeychainUtils/Sources/KeychainCodableBackingStore/Keychain+CodableKeyValueStorage.swift index 647287b9..f63033d9 100644 --- a/TIKeychainUtils/Sources/KeychainCodableBackingStore/Keychain+CodableKeyValueStorage.swift +++ b/TIKeychainUtils/Sources/KeychainCodableBackingStore/Keychain+CodableKeyValueStorage.swift @@ -64,12 +64,7 @@ extension Keychain: CodableKeyValueStorage { } public func removeCodableValue(forKey key: StorageKey) -> Result { - Result { - try remove(key.rawValue) - } - .mapError { - .unableToWriteData(underlyingError: $0) - } + removeValue(forKey: key) } public func hasCodableValue(forKey key: StorageKey) -> Result { @@ -80,4 +75,13 @@ extension Keychain: CodableKeyValueStorage { .unableToExtractData(underlyingError: $0) } } + + public func removeValue(forKey key: StorageKey) -> Result { + Result { + try remove(key.rawValue) + } + .mapError { + .unableToWriteData(underlyingError: $0) + } + } } diff --git a/TIKeychainUtils/Sources/KeychainCodableBackingStore/KeychainMigratingCodableBackingStore.swift b/TIKeychainUtils/Sources/KeychainCodableBackingStore/KeychainMigratingCodableBackingStore.swift index 58cb7772..e7bad246 100644 --- a/TIKeychainUtils/Sources/KeychainCodableBackingStore/KeychainMigratingCodableBackingStore.swift +++ b/TIKeychainUtils/Sources/KeychainCodableBackingStore/KeychainMigratingCodableBackingStore.swift @@ -21,9 +21,6 @@ // import Foundation -import KeychainAccess import TISwiftUtils -public typealias KeychainMigratingCodableBackingStore = MigratingBackingStore +public typealias KeychainMigratingCodableBackingStore = MigratingBackingStore diff --git a/TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/BaseMigratingSingleValueKeychainStorage.swift b/TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/BaseMigratingSingleValueKeychainStorage.swift deleted file mode 100644 index 536396a0..00000000 --- a/TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/BaseMigratingSingleValueKeychainStorage.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// Copyright (c) 2023 Touch Instinct -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the Software), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -import TIFoundationUtils -import KeychainAccess - -open class BaseMigratingSingleValueKeychainStorage: BaseMigratingSingleValueStorage { - - public init(sourceStorage: Keychain, - targetStorage: Keychain, - storageKey: StorageKey, - hasValueClosure: @escaping HasValueClosure, - deleteValueClosure: @escaping DeleteValueClosure, - getValueClosure: @escaping GetValueClosure, - storeValueClosure: @escaping StoreValueClosure) { - - let migratingStorages = KeychainMigratingStorageContainer(sourceStorage: sourceStorage, - targetStorage: targetStorage) - - super.init(migratingStorages: migratingStorages, - storageKey: storageKey, - hasValueClosure: hasValueClosure, - deleteValueClosure: deleteValueClosure, - getValueClosure: getValueClosure, - storeValueClosure: storeValueClosure) - } - - public init(sourceStorage: Keychain, - targetStorage: Keychain, - storageKey: StorageKey, - decoder: CodableKeyValueDecoder, - encoder: CodableKeyValueEncoder) where ValueType: Codable { - - let hasValueClosure: HasValueClosure = { container, storageKey in - container.hasCodableValue(forKey: storageKey) - } - - let deleteValueClosure: DeleteValueClosure = { container, storageKey in - container.removeCodableValue(forKey: storageKey) - } - - let getValueClosure: GetValueClosure = { container, storageKey in - container.codableObject(forKey: storageKey, decoder: decoder) - } - - let storeValueClosure: StoreValueClosure = { container, value, storageKey in - container.set(encodableObject: value, forKey: storageKey, encoder: encoder) - } - - let migratingStorages = KeychainMigratingStorageContainer(sourceStorage: sourceStorage, - targetStorage: targetStorage) - - super.init(migratingStorages: migratingStorages, - storageKey: storageKey, - hasValueClosure: hasValueClosure, - deleteValueClosure: deleteValueClosure, - getValueClosure: getValueClosure, - storeValueClosure: storeValueClosure) - } -} diff --git a/TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/Container/KeychainMigratingStorageContainer.swift b/TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/Container/KeychainMigratingStorageContainer.swift index 56263d3c..0a8ff79e 100644 --- a/TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/Container/KeychainMigratingStorageContainer.swift +++ b/TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/Container/KeychainMigratingStorageContainer.swift @@ -21,49 +21,11 @@ // import TIFoundationUtils -import TISwiftUtils import KeychainAccess -open class KeychainMigratingStorageContainer: MigratingStorageContainer, CodableKeyValueStorage { +public final class KeychainMigratingStorageContainer: BaseCodableMigratingStorageContainer { - open func codableObject(forKey key: StorageKey, - decoder: CodableKeyValueDecoder) -> Result { - - if case let .success(value) = targetStorage.codableObject(forKey: key, decoder: decoder) { - return .success(value) - } - - if case let .success(value) = sourceStorage.codableObject(forKey: key, decoder: decoder) { - return .success(value) - } - - return .failure(.valueNotFound) - } - - open func set(encodableObject: Value, - forKey key: StorageKey, - encoder: CodableKeyValueEncoder) -> Result { - - targetStorage.set(encodableObject: encodableObject, forKey: key, encoder: encoder) - } - - open func removeCodableValue(forKey key: StorageKey) -> Result { - if case .success = sourceStorage.hasCodableValue(forKey: key) { - let _ = sourceStorage.removeCodableValue(forKey: key) - } - - guard case .success = targetStorage.hasCodableValue(forKey: key) else { - return .failure(.valueNotFound) - } - - return targetStorage.removeCodableValue(forKey: key) - } - - open func hasCodableValue(forKey key: StorageKey) -> Result { - if case .success = targetStorage.hasCodableValue(forKey: key) { - return .success(true) - } - - return sourceStorage.hasCodableValue(forKey: key) + public override func removeSourceValue(forKey key: StorageKey) { + let _ = sourceStorage.removeValue(forKey: key) } } diff --git a/TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/StringMigratingValueKeychainStorage.swift b/TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/StringMigratingValueKeychainStorage.swift deleted file mode 100644 index 56ae999e..00000000 --- a/TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/StringMigratingValueKeychainStorage.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// 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 StringMigratingValueKeychainStorage: BaseMigratingSingleValueKeychainStorage { - - public init(sourceStorage: Keychain, - targetStorage: Keychain, - storageKey: StorageKey) { - - let hasValueClosure: HasValueClosure = { container, storageKey in - container.hasCodableValue(forKey: storageKey) - } - - let deleteValueClosure: DeleteValueClosure = { container, storageKey in - container.removeCodableValue(forKey: storageKey) - } - - let getValueClosure: GetValueClosure = { container, storageKey in - let codableResult = container.codableObject(forKey: storageKey, decoder: UnarchiverKeyValueDecoder()) - - if case .success = codableResult { - return codableResult - } - - return container.getStringValue(forKey: storageKey) - } - - let storeValueClosure: StoreValueClosure = { container, value, storageKey in - container.set(encodableObject: value, forKey: storageKey, encoder: ArchiverKeyValueEncoder()) - } - - super.init(sourceStorage: sourceStorage, - targetStorage: targetStorage, - storageKey: storageKey, - hasValueClosure: hasValueClosure, - deleteValueClosure: deleteValueClosure, - getValueClosure: getValueClosure, - storeValueClosure: storeValueClosure) - } -} - -extension KeychainMigratingStorageContainer { - - func getStringValue(forKey key: StorageKey) -> Result { - Result { - if let value = try targetStorage.get(key.rawValue) { - return value - } - - if let value = try sourceStorage.get(key.rawValue) { - if case .success = set(encodableObject: value, forKey: key, encoder: ArchiverKeyValueEncoder()) { - try? sourceStorage.remove(key.rawValue) - } - - return value - } - - throw StorageError.valueNotFound - } - .mapError { .unableToExtractData(underlyingError: $0) } - } -} diff --git a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/KeychainCodableBackingStore.xcplaygroundpage/Contents.swift b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/KeychainCodableBackingStore.xcplaygroundpage/Contents.swift index 1272ac5f..dc848467 100644 --- a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/KeychainCodableBackingStore.xcplaygroundpage/Contents.swift +++ b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/KeychainCodableBackingStore.xcplaygroundpage/Contents.swift @@ -21,10 +21,16 @@ extension Keychain { } } +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, sourceStorage: .keychain, targetStorage: .groupKeychain) + @KeychainMigratingCodableBackingStore(key: .knownPins, storageContainer: .defaultContainer) var knownPins = [String: Set]() } diff --git a/TISwiftUtils/Sources/PropertyWrappers/MigratingBackingStore.swift b/TISwiftUtils/Sources/PropertyWrappers/MigratingBackingStore.swift index de6a84d9..690590f7 100644 --- a/TISwiftUtils/Sources/PropertyWrappers/MigratingBackingStore.swift +++ b/TISwiftUtils/Sources/PropertyWrappers/MigratingBackingStore.swift @@ -20,39 +20,32 @@ // THE SOFTWARE. // -@propertyWrapper public struct MigratingBackingStore { +@propertyWrapper public struct MigratingBackingStore { - public typealias StorageContainer = MigratingStorageContainer - public typealias InitSourceClosure = (StoreContent) -> SourceStorage - public typealias InitTargetClosure = (StoreContent) -> TargetStorage - public typealias GetClosure = (StorageContainer) -> StoreContent - public typealias SetClosure = (StorageContainer, StoreContent) -> Void + 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: StorageContainer + private let storageContainer: MigratingStorages public init(wrappedValue: StoreContent, - sourceInitStorageClosure: InitSourceClosure, - targetInitStorageClosure: InitTargetClosure, + initStorageClosure: InitClosure, getClosure: @escaping GetClosure, setClosure: @escaping SetClosure) { - let sourceStore = sourceInitStorageClosure(wrappedValue) - let targetStore = targetInitStorageClosure(wrappedValue) - - self.storageContainer = StorageContainer(sourceStorage: sourceStore, targetStorage: targetStore) + self.storageContainer = initStorageClosure(wrappedValue) self.getClosure = getClosure self.setClosure = setClosure } - public init(sourceStore: SourceStorage, - targetStore: TargetStorage, + public init(storageContainer: MigratingStorages, getClosure: @escaping GetClosure, setClosure: @escaping SetClosure) { - self.storageContainer = StorageContainer(sourceStorage: sourceStore, targetStorage: targetStore) + self.storageContainer = storageContainer self.getClosure = getClosure self.setClosure = setClosure } @@ -65,8 +58,4 @@ setClosure(storageContainer, newValue) } } - - public var projectedValue: TargetStorage { - storageContainer.targetStorage - } } diff --git a/Tests/TIFoundationUtilsTests/Helpers/StorageKey+Constants.swift b/Tests/TIFoundationUtilsTests/Helpers/StorageKey+Constants.swift new file mode 100644 index 00000000..ea700423 --- /dev/null +++ b/Tests/TIFoundationUtilsTests/Helpers/StorageKey+Constants.swift @@ -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 { + .init(rawValue: "refreshToken") + } + + static var profile: StorageKey { + .init(rawValue: "profile") + } +} diff --git a/Tests/TIFoundationUtilsTests/MigratingBackingStoreTests.swift b/Tests/TIFoundationUtilsTests/MigratingBackingStoreTests.swift new file mode 100644 index 00000000..7621ab11 --- /dev/null +++ b/Tests/TIFoundationUtilsTests/MigratingBackingStoreTests.swift @@ -0,0 +1,93 @@ +// +// 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 +@testable import TIFoundationUtils + +final class MigratingBackingStoreTests: XCTestCase { + + private let encoder = JSONKeyValueEncoder() + private let decoder = JSONKeyValueDecoder() + + @MockMigratingCodableBackingStore(key: .profile, storageContainer: .defaultContainer) + var profile: Profile? + + override func setUp() { + profile = nil + } + + // MARK: - Tests + + func testMigrationOnGetValue() throws { + let oldProfile = Profile(name: "some", age: 0) + let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage + + let _ = sourceStorage.set(encodableObject: oldProfile, forKey: .profile, encoder: encoder) + + XCTAssertEqual(profile, oldProfile) + } + + func testGetValueFromTarget() throws { + let newProfile = Profile(name: "any", age: 1) + let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage + + let _ = targetStorage.set(encodableObject: newProfile, forKey: .profile, encoder: encoder) + + XCTAssertEqual(profile, newProfile) + XCTAssertEqual(try? targetStorage.codableObject(forKey: .profile, decoder: decoder).get(), newProfile) + } + + func testGetValue() throws { + let newProfile = Profile(name: "any", age: 1) + let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage + + profile = newProfile + + XCTAssertEqual(profile, newProfile) + XCTAssertEqual(try? targetStorage.codableObject(forKey: .profile, decoder: decoder).get(), newProfile) + } + + func testMigrationOnStoreValue() throws { + let oldProfile = Profile(name: "some", age: 0) + let newProfile = Profile(name: "any", age: 1) + let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage + let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage + + let _ = sourceStorage.set(encodableObject: oldProfile, forKey: .profile, encoder: encoder) + profile = newProfile + + XCTAssertEqual(profile, newProfile) + XCTAssertEqual(try? targetStorage.codableObject(forKey: .profile, decoder: decoder).get(), newProfile) + } + + func testStoreValueFromTarget() throws { + let oldProfile = Profile(name: "some", age: 0) + let newProfile = Profile(name: "any", age: 1) + let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage + + let _ = targetStorage.set(encodableObject: oldProfile, forKey: .profile, encoder: encoder) + profile = newProfile + + XCTAssertEqual(profile, newProfile) + XCTAssertEqual(try? targetStorage.codableObject(forKey: .profile, decoder: decoder).get(), newProfile) + } +} diff --git a/Tests/TIFoundationUtilsTests/MigratingSingleValueStorageTests.swift b/Tests/TIFoundationUtilsTests/MigratingSingleValueStorageTests.swift new file mode 100644 index 00000000..5b00dcba --- /dev/null +++ b/Tests/TIFoundationUtilsTests/MigratingSingleValueStorageTests.swift @@ -0,0 +1,167 @@ +// +// 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 +@testable 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 = BaseMigratingSingleValueStorage(sourceStorage: sourceRefreshTokenStorage, + targetStorage: targetRefreshTokenStorage) + + override func setUp() { + let _ = sourceRefreshTokenStorage.deleteValue() + let _ = targetRefreshTokenStorage.deleteValue() + } + + // MARK: - Tests + + func testMigrationOnGetValue() throws { + let oldToken = "old-token" + + let _ = sourceRefreshTokenStorage.store(value: oldToken) + + XCTAssertEqual(try? refreshToken.getValue().get(), oldToken) + XCTAssertEqual(try? targetRefreshTokenStorage.getValue().get(), oldToken) + XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) + } + + func testGetValueFromTarget() throws { + let token = "new-token" + + let _ = targetRefreshTokenStorage.store(value: token) + + XCTAssertEqual(try? refreshToken.getValue().get(), token) + XCTAssertEqual(try? targetRefreshTokenStorage.getValue().get(), token) + XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) + } + + func testGetValue() throws { + let token = "token" + + let _ = refreshToken.store(value: token) + + XCTAssertEqual(try? refreshToken.getValue().get(), token) + XCTAssertEqual(try? targetRefreshTokenStorage.getValue().get(), token) + XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) + } + + func testMigrationOnHasValue() throws { + let oldToken = "old-token" + + let _ = sourceRefreshTokenStorage.store(value: oldToken) + + XCTAssertTrue(refreshToken.hasStoredValue()) + XCTAssertFalse(targetRefreshTokenStorage.hasStoredValue()) + XCTAssertTrue(sourceRefreshTokenStorage.hasStoredValue()) + } + + func testHasValueFromTarget() throws { + let token = "new-token" + + let _ = targetRefreshTokenStorage.store(value: token) + + XCTAssertTrue(refreshToken.hasStoredValue()) + XCTAssertTrue(targetRefreshTokenStorage.hasStoredValue()) + XCTAssertFalse(sourceRefreshTokenStorage.hasStoredValue()) + } + + func testHasValue() throws { + let token = "token" + + let _ = refreshToken.store(value: token) + + XCTAssertTrue(refreshToken.hasStoredValue()) + XCTAssertTrue(targetRefreshTokenStorage.hasStoredValue()) + XCTAssertFalse(sourceRefreshTokenStorage.hasStoredValue()) + } + + func testMigrationOnDeleteValue() throws { + let oldToken = "old-token" + + let _ = sourceRefreshTokenStorage.store(value: oldToken) + + XCTAssertNoThrow(try refreshToken.deleteValue().get()) + XCTAssertThrowsError(try targetRefreshTokenStorage.getValue().get()) + XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) + } + + func testDeleteValueFromTarget() throws { + let token = "new-token" + + let _ = targetRefreshTokenStorage.store(value: token) + + XCTAssertNoThrow(try refreshToken.deleteValue().get()) + XCTAssertThrowsError(try targetRefreshTokenStorage.getValue().get()) + XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) + } + + func testDeleteValue() throws { + let token = "token" + + let _ = refreshToken.store(value: token) + + XCTAssertNoThrow(try refreshToken.deleteValue().get()) + XCTAssertThrowsError(try targetRefreshTokenStorage.getValue().get()) + XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) + } + + func testMigrationOnStoreValue() throws { + let oldToken = "old-token" + let newToken = "new-token" + + let _ = sourceRefreshTokenStorage.store(value: oldToken) + let _ = refreshToken.store(value: newToken) + + XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) + XCTAssertEqual(try? refreshToken.getValue().get(), newToken) + XCTAssertEqual(try? targetRefreshTokenStorage.getValue().get(), newToken) + } + + func testStoreValueFromTarget() throws { + let oldToken = "old-token" + let newToken = "new-token" + + let _ = targetRefreshTokenStorage.store(value: oldToken) + let _ = refreshToken.store(value: newToken) + + XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) + XCTAssertEqual(try? refreshToken.getValue().get(), newToken) + XCTAssertEqual(try? targetRefreshTokenStorage.getValue().get(), newToken) + } + + func testStoreValue() throws { + let token = "token" + + let _ = refreshToken.store(value: token) + + XCTAssertEqual(try? refreshToken.getValue().get(), token) + XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) + XCTAssertEqual(try? targetRefreshTokenStorage.getValue().get(), token) + } +} diff --git a/Tests/TIFoundationUtilsTests/Mocks/BaseSingleValueMockStorage.swift b/Tests/TIFoundationUtilsTests/Mocks/BaseSingleValueMockStorage.swift new file mode 100644 index 00000000..9bd84c5a --- /dev/null +++ b/Tests/TIFoundationUtilsTests/Mocks/BaseSingleValueMockStorage.swift @@ -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 + +class BaseSingleValueMockStorage: BaseSingleValueStorage { + init(storage: MockStorage, + storageKey: StorageKey, + getValueClosure: @escaping GetValueClosure, + storeValueClosure: @escaping StoreValueClosure) { + + let hasValueClosure: HasValueClosure = { storage, storageKey in + .success(storage.hasValue(forKey: storageKey)) + } + + let deleteValueClosure: DeleteValueClosure = { storage, storageKey in + .success(storage.deleteValue(forKey: storageKey)) + } + + super.init(storage: storage, + storageKey: storageKey, + hasValueClosure: hasValueClosure, + deleteValueClosure: deleteValueClosure, + getValueClosure: getValueClosure, + storeValueClosure: storeValueClosure) + } +} diff --git a/Tests/TIFoundationUtilsTests/Mocks/MockMigratingCodableBackingStore.swift b/Tests/TIFoundationUtilsTests/Mocks/MockMigratingCodableBackingStore.swift new file mode 100644 index 00000000..d2bf74aa --- /dev/null +++ b/Tests/TIFoundationUtilsTests/Mocks/MockMigratingCodableBackingStore.swift @@ -0,0 +1,26 @@ +// +// Copyright (c) 2020 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 TIFoundationUtils + +typealias MockMigratingCodableBackingStore = MigratingBackingStore diff --git a/Tests/TIFoundationUtilsTests/Mocks/MockMigratingStorageContainer.swift b/Tests/TIFoundationUtilsTests/Mocks/MockMigratingStorageContainer.swift new file mode 100644 index 00000000..ab7b3dd0 --- /dev/null +++ b/Tests/TIFoundationUtilsTests/Mocks/MockMigratingStorageContainer.swift @@ -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 { + + static let defaultContainer = MockMigratingStorageContainer(sourceStorage: .defaultStorage, + targetStorage: .defaultGroupStorage) + + override func removeSourceValue(forKey key: StorageKey) { + let _ = sourceStorage.removeValue(forKey: key) + } +} diff --git a/Tests/TIFoundationUtilsTests/Mocks/MockStorage.swift b/Tests/TIFoundationUtilsTests/Mocks/MockStorage.swift new file mode 100644 index 00000000..65005256 --- /dev/null +++ b/Tests/TIFoundationUtilsTests/Mocks/MockStorage.swift @@ -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(forKey key: StorageKey) -> Bool { + storage[key.rawValue] != nil + } + + func getValue(forKey key: StorageKey) -> Value? { + storage[key.rawValue] as? Value + } + + func store(value: Value, forKey key: StorageKey) { + storage[key.rawValue] = value + } + + func deleteValue(forKey key: StorageKey) { + storage.removeValue(forKey: key.rawValue) + } +} + +// MARK: - CodableKeyValueStorage + +extension MockStorage: CodableKeyValueStorage { + + public func codableObject(forKey key: StorageKey, + decoder: CodableKeyValueDecoder) -> Result { + + guard let storedData = getData(forKey: key) else { + return .failure(.valueNotFound) + } + + return decoder.decodeDecodable(from: storedData, for: key) + } + + public func set(encodableObject: Value, + forKey key: StorageKey, + encoder: CodableKeyValueEncoder) -> Result { + + encoder.encodeEncodable(value: encodableObject, for: key) + .map { + setData(data: $0, forKey: key) + } + } + + public func removeCodableValue(forKey key: StorageKey) -> Result { + removeValue(forKey: key) + } + + public func hasCodableValue(forKey key: StorageKey) -> Result { + guard getData(forKey: key) != nil else { + return .success(false) + } + + return .success(true) + } + + public func removeValue(forKey key: StorageKey) -> Result { + guard getData(forKey: key) != nil else { + return .failure(.valueNotFound) + } + + return .success(deleteValue(forKey: key)) + } + + // MARK: - Private methods + + private func getData(forKey key: StorageKey) -> Data? { + storage[key.rawValue] as? Data + } + + private func setData(data: Data, forKey key: StorageKey) { + storage[key.rawValue] = data + } +} diff --git a/Tests/TIFoundationUtilsTests/Mocks/Profile.swift b/Tests/TIFoundationUtilsTests/Mocks/Profile.swift new file mode 100644 index 00000000..57e5d40f --- /dev/null +++ b/Tests/TIFoundationUtilsTests/Mocks/Profile.swift @@ -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 +} diff --git a/Tests/TIFoundationUtilsTests/Mocks/StringSingleValueMockStorage.swift b/Tests/TIFoundationUtilsTests/Mocks/StringSingleValueMockStorage.swift new file mode 100644 index 00000000..8251f1fd --- /dev/null +++ b/Tests/TIFoundationUtilsTests/Mocks/StringSingleValueMockStorage.swift @@ -0,0 +1,45 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import TIFoundationUtils + +final class StringSingleValueMockStorage: BaseSingleValueMockStorage { + + init(storage: MockStorage, storageKey: StorageKey) { + 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) + } +} diff --git a/docs/tikeychainutils/keychaincodablebackingstore.md b/docs/tikeychainutils/keychaincodablebackingstore.md index 2c9e545f..5088e43b 100644 --- a/docs/tikeychainutils/keychaincodablebackingstore.md +++ b/docs/tikeychainutils/keychaincodablebackingstore.md @@ -22,11 +22,17 @@ extension Keychain { } } +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, sourceStorage: .keychain, targetStorage: .groupKeychain) + @KeychainMigratingCodableBackingStore(key: .knownPins, storageContainer: .defaultContainer) var knownPins = [String: Set]() } ``` -- 2.40.1 From 25c0d04d111870f0ad0bfc4e59b126aacc8d7fbb Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Thu, 6 Jul 2023 12:34:48 +0300 Subject: [PATCH 04/12] feat: update tests, migration fixes, factory method for migration storage added --- Package.swift | 4 +- ...eyVlaueStorage+MigratingBackingStore.swift | 22 +- .../BaseSingleValueStorage.swift | 35 +-- .../BaseMigratingSingleValueStorage.swift | 32 ++- ...BaseCodableMigratingStorageContainer.swift | 45 +++- .../DefaultsMigratingStorageContainer.swift | 4 +- TIFoundationUtils/PlaygroundPodfile | 1 + TIFoundationUtils/TIFoundationLogger.swift | 30 +++ TIFoundationUtils/TIFoundationUtils.podspec | 1 + .../KeychainMigratingStorageContainer.swift | 4 +- .../Contents.swift | 6 +- TITableKitUtils/TITableKitUtils.podspec | 9 +- .../MigratingBackingStoreTests.swift | 191 +++++++++++--- .../MigratingSingleValueStorageTests.swift | 245 ++++++++++-------- .../Mocks/BaseSingleValueMockStorage.swift | 6 +- .../Mocks/MockMigratingStorageContainer.swift | 4 +- .../Mocks/MockStorageLogger.swift | 46 ++++ .../Mocks/StringSingleValueMockStorage.swift | 2 +- docs/tikeychainutils/singlevaluestorage.md | 9 +- 19 files changed, 495 insertions(+), 201 deletions(-) create mode 100644 TIFoundationUtils/TIFoundationLogger.swift create mode 100644 Tests/TIFoundationUtilsTests/Mocks/MockStorageLogger.swift diff --git a/Package.swift b/Package.swift index 4f3fe068..fbaee8c0 100644 --- a/Package.swift +++ b/Package.swift @@ -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")]), @@ -148,7 +148,7 @@ let package = Package( path: "Tests/TITextProcessingTests"), .testTarget( name: "TIFoundationUtilsTests", - dependencies: ["TIFoundationUtils", "TISwiftUtils"], + dependencies: ["TIFoundationUtils", "TISwiftUtils", "TILogging"], path: "Tests/TIFoundationUtilsTests") ] ) diff --git a/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyVlaueStorage+MigratingBackingStore.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyVlaueStorage+MigratingBackingStore.swift index 7620f8cf..fc095f2f 100644 --- a/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyVlaueStorage+MigratingBackingStore.swift +++ b/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyVlaueStorage+MigratingBackingStore.swift @@ -32,7 +32,14 @@ public extension MigratingBackingStore where MigratingStorages: CodableKeyValueS where StoreContent == Value? { let getClosure: GetClosure = { container in - try? container.codableObject(forKey: key, decoder: decoder).get() + // If source storage has value and target doesn't we can't migrate it from storate container + try? container.codableObject(forKey: key, decoder: decoder) + .flatMap { + let _ = container.set(encodableObject: $0, forKey: key, encoder: encoder) + + return .success($0) + } + .get() } let setClosure: SetClosure = { container, value in @@ -50,7 +57,18 @@ public extension MigratingBackingStore where MigratingStorages: CodableKeyValueS where StoreContent == Value? { let getClosure: GetClosure = { container in - container.codableObject(forKey: key, defaultValue: wrappedValue, decoder: decoder) + // If source storage has value and target doesn't we can't migrate it from storate container + try? container.codableObject(forKey: key, decoder: decoder) + .flatMap { + let _ = container.set(encodableObject: $0, forKey: key, encoder: encoder) + + return .success($0) + } + .flatMapError { _ in + container.set(encodableObject: wrappedValue, forKey: key, encoder: encoder) + .flatMap { _ in .success(wrappedValue) } + } + .get() } let setClosure: SetClosure = { container, value in diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/BaseSingleValueStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/BaseSingleValueStorage.swift index 4603f93e..73a2119d 100644 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/BaseSingleValueStorage.swift +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/BaseSingleValueStorage.swift @@ -33,8 +33,6 @@ open class BaseSingleValueStorage: SingleValueStorage { public let getValueClosure: GetValueClosure public let storeValueClosure: StoreValueClosure - public var migrationStorage: StorageType? - public init(storage: StorageType, storageKey: StorageKey, hasValueClosure: @escaping HasValueClosure, @@ -53,43 +51,18 @@ open class BaseSingleValueStorage: SingleValueStorage { // MARK: - SingleValueStorage open func hasStoredValue() -> Bool { - if let migrationStorage, (try? hasValueClosure(migrationStorage, storageKey).get()) == true { - let _ = deleteValueClosure(storage, storageKey) - return true - } - - return (try? hasValueClosure(storage, storageKey).get()) ?? false + (try? hasValueClosure(storage, storageKey).get()) ?? false } open func store(value: ValueType) -> Result { - let targetStorate = migrationStorage ?? storage - - return storeValueClosure(targetStorate, value, storageKey) + storeValueClosure(storage, value, storageKey) } open func getValue() -> Result { - guard let migrationStorage else { - return getValueClosure(storage, storageKey) - } - - if let value = try? getValueClosure(migrationStorage, storageKey).get() { - return .success(value) - } - - let result = getValueClosure(storage, storageKey) - - if case let .success(value) = result, case .success = store(value: value) { - let _ = deleteValueClosure(storage, storageKey) - } - - return result + getValueClosure(storage, storageKey) } open func deleteValue() -> Result { - if let migrationStorage { - return deleteValueClosure(migrationStorage, storageKey) - } - - return deleteValueClosure(storage, storageKey) + deleteValueClosure(storage, storageKey) } } diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueStorage.swift index a1a45a32..f658f27c 100644 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueStorage.swift +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueStorage.swift @@ -20,6 +20,8 @@ // THE SOFTWARE. // +import TILogging + open class BaseMigratingSingleValueStorage: SingleValueStorage where SourceStorage.ErrorType == TargetStorage.ErrorType, @@ -30,12 +32,15 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType, public let sourceStorage: SourceStorage public let targetStorage: TargetStorage + public let errorLogger: ErrorLogger public init(sourceStorage: SourceStorage, - targetStorage: TargetStorage) { + targetStorage: TargetStorage, + errorLogger: ErrorLogger = TIFoundationLogger(category: "BaseMigratingSingleValueStorage")) { self.sourceStorage = sourceStorage self.targetStorage = targetStorage + self.errorLogger = errorLogger } // MARK: - SingleValueStorage @@ -46,6 +51,11 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType, open func store(value: ValueType) -> Result { let _ = sourceStorage.deleteValue() + .flatMapError { + errorLogger.log(error: $0, file: #file, line: #line) + + return .failure($0) + } return targetStorage.store(value: value) } @@ -54,6 +64,11 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType, targetStorage.getValue() .flatMap { let _ = sourceStorage.deleteValue() + .flatMapError { + errorLogger.log(error: $0, file: #file, line: #line) + + return .failure($0) + } return .success($0) } @@ -79,5 +94,20 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType, return .success($0) } + .flatMapError { + errorLogger.log(error: $0, file: #file, line: #line) + + return sourceStorage.deleteValue() + } + } +} + +public extension SingleValueStorage where ErrorType == StorageError { + + func migrating(to targetStorage: S) -> BaseMigratingSingleValueStorage + where ErrorType == S.ErrorType, + ValueType == S.ValueType { + + BaseMigratingSingleValueStorage(sourceStorage: self, targetStorage: targetStorage) } } diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift index 766f5d47..4986607e 100644 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift @@ -21,18 +21,39 @@ // import TISwiftUtils +import TILogging open class BaseCodableMigratingStorageContainer: MigratingStorageContainer, CodableKeyValueStorage where SourceStorage: CodableKeyValueStorage, TargetStorage: CodableKeyValueStorage { + public enum CodableTypeError: Error { + case unableToConvertToEncodable + } + + public let errorLogger: ErrorLogger + + public init(sourceStorage: SourceStorage, + targetStorage: TargetStorage, + errorLogger: ErrorLogger = TIFoundationLogger(category: "BaseCodableMigratingStorageContainer")) { + + self.errorLogger = errorLogger + + super.init(sourceStorage: sourceStorage, targetStorage: targetStorage) + } + open func codableObject(forKey key: StorageKey, decoder: CodableKeyValueDecoder) -> Result { targetStorage.codableObject(forKey: key, decoder: decoder) .flatMap { - removeSourceValue(forKey: key) + let _ = removeSourceValue(forKey: key) + .flatMapError { + errorLogger.log(error: $0, file: #file, line: #line) + + return .failure($0) + } return .success($0) } @@ -45,19 +66,26 @@ where SourceStorage: CodableKeyValueStorage, TargetStorage: CodableKeyValueStora forKey key: StorageKey, encoder: CodableKeyValueEncoder) -> Result { - removeSourceValue(forKey: key) + let _ = removeSourceValue(forKey: key) + .flatMapError { + errorLogger.log(error: $0, file: #file, line: #line) + + return .failure($0) + } return targetStorage.set(encodableObject: encodableObject, forKey: key, encoder: encoder) } open func removeCodableValue(forKey key: StorageKey) -> Result { targetStorage.removeCodableValue(forKey: key) - .flatMap { - if let value = try? sourceStorage.hasCodableValue(forKey: key).get(), value { - return sourceStorage.removeCodableValue(forKey: key) - } + .flatMap { value in + sourceStorage.removeCodableValue(forKey: key) + .flatMapError { _ in .success(value) } + } + .flatMapError { + errorLogger.log(error: $0, file: #file, line: #line) - return .success($0) + return removeSourceValue(forKey: key) } } @@ -70,7 +98,8 @@ where SourceStorage: CodableKeyValueStorage, TargetStorage: CodableKeyValueStora // MARK: - Open methods - open func removeSourceValue(forKey key: StorageKey) { + open func removeSourceValue(forKey key: StorageKey) -> Result { // override in subclasses + .failure(.valueNotFound) } } diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/DefaultsMigratingStorageContainer.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/DefaultsMigratingStorageContainer.swift index 97da6482..9a5e02f3 100644 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/DefaultsMigratingStorageContainer.swift +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/DefaultsMigratingStorageContainer.swift @@ -24,7 +24,7 @@ import Foundation public final class DefaultsMigratingStorageContainer: BaseCodableMigratingStorageContainer { - public override func removeSourceValue(forKey key: StorageKey) { - let _ = sourceStorage.removeValue(forKey: key) + public override func removeSourceValue(forKey key: StorageKey) -> Result { + sourceStorage.removeValue(forKey: key) } } diff --git a/TIFoundationUtils/PlaygroundPodfile b/TIFoundationUtils/PlaygroundPodfile index 95c2fd95..8a8cb08a 100644 --- a/TIFoundationUtils/PlaygroundPodfile +++ b/TIFoundationUtils/PlaygroundPodfile @@ -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 diff --git a/TIFoundationUtils/TIFoundationLogger.swift b/TIFoundationUtils/TIFoundationLogger.swift new file mode 100644 index 00000000..dbfeb1e2 --- /dev/null +++ b/TIFoundationUtils/TIFoundationLogger.swift @@ -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)) + } +} diff --git a/TIFoundationUtils/TIFoundationUtils.podspec b/TIFoundationUtils/TIFoundationUtils.podspec index a8f783b2..9e0f609b 100644 --- a/TIFoundationUtils/TIFoundationUtils.podspec +++ b/TIFoundationUtils/TIFoundationUtils.podspec @@ -20,5 +20,6 @@ Pod::Spec.new do |s| end s.dependency 'TISwiftUtils', s.version.to_s + s.dependency 'TILogging', s.version.to_s s.framework = 'Foundation' end diff --git a/TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/Container/KeychainMigratingStorageContainer.swift b/TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/Container/KeychainMigratingStorageContainer.swift index 0a8ff79e..f10fc43e 100644 --- a/TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/Container/KeychainMigratingStorageContainer.swift +++ b/TIKeychainUtils/Sources/SingleValueKeychainStorage/MigratableStorage/Container/KeychainMigratingStorageContainer.swift @@ -25,7 +25,7 @@ import KeychainAccess public final class KeychainMigratingStorageContainer: BaseCodableMigratingStorageContainer { - public override func removeSourceValue(forKey key: StorageKey) { - let _ = sourceStorage.removeValue(forKey: key) + public override func removeSourceValue(forKey key: StorageKey) -> Result { + sourceStorage.removeValue(forKey: key) } } diff --git a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift index 845945ed..2648322f 100644 --- a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift +++ b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift @@ -127,10 +127,6 @@ case let .failure(storageError): let groupKeychain = Keychain(service: "app.group.identifier") let targetApiTokenKeychainStorage = StringValueKeychainStorage(keychain: groupKeychain, storageKey: .apiToken) -let migratingApiTokenKeychainStorage = BaseMigratingSingleValueStorage(sourceStorage: apiTokenKeychainStorage, - targetStorage: targetApiTokenKeychainStorage) +let migratingApiTokenKeychainStorage = apiTokenKeychainStorage.migrating(to: targetApiTokenKeychainStorage) let token = migratingApiTokenKeychainStorage.getValue() - -//: Если нет необходимости в создании target storage, можно воспользоваться переменной `migrationStorage`, которая есть у каждого хранилища, наследованного от `BaseSingleValueStorage` -apiTokenKeychainStorage.migrationStorage = groupKeychain diff --git a/TITableKitUtils/TITableKitUtils.podspec b/TITableKitUtils/TITableKitUtils.podspec index db3631a9..ba01a447 100644 --- a/TITableKitUtils/TITableKitUtils.podspec +++ b/TITableKitUtils/TITableKitUtils.podspec @@ -10,7 +10,14 @@ Pod::Spec.new do |s| s.ios.deployment_target = '11.0' s.swift_versions = ['5.7'] - s.source_files = s.name + '/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.exclude_files = s.name + '/*.app' + end s.dependency 'TIUIElements', s.version.to_s s.dependency 'TISwiftUtils', s.version.to_s diff --git a/Tests/TIFoundationUtilsTests/MigratingBackingStoreTests.swift b/Tests/TIFoundationUtilsTests/MigratingBackingStoreTests.swift index 7621ab11..bf368892 100644 --- a/Tests/TIFoundationUtilsTests/MigratingBackingStoreTests.swift +++ b/Tests/TIFoundationUtilsTests/MigratingBackingStoreTests.swift @@ -33,61 +33,192 @@ final class MigratingBackingStoreTests: XCTestCase { override func setUp() { profile = nil + let _ = MockMigratingStorageContainer.defaultContainer.sourceStorage.removeValue(forKey: .profile) + let _ = MockMigratingStorageContainer.defaultContainer.targetStorage.removeValue(forKey: .profile) } - // MARK: - Tests + // MARK: - Read Tests - func testMigrationOnGetValue() throws { - let oldProfile = Profile(name: "some", age: 0) + // Precondition - source: ✅ target: ✅ + // PostCondition - source: ❌ target: ✅ + func testGetValue() throws { + let oldProfile = Profile(name: "old", age: 0) + let newProfile = Profile(name: "new", age: 0) let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage + let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage let _ = sourceStorage.set(encodableObject: oldProfile, forKey: .profile, encoder: encoder) + let _ = targetStorage.set(encodableObject: newProfile, forKey: .profile, encoder: encoder) - XCTAssertEqual(profile, oldProfile) + XCTAssertEqual(profile, newProfile) + XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get()) + XCTAssertNoThrow(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get()) } - func testGetValueFromTarget() throws { - let newProfile = Profile(name: "any", age: 1) + // Precondition - source: ❌ target: ✅ + // PostCondition - source: ❌ target: ✅ + func testGetValueWithNoSource() throws { + let newProfile = Profile(name: "new", age: 0) + let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage let _ = targetStorage.set(encodableObject: newProfile, forKey: .profile, encoder: encoder) XCTAssertEqual(profile, newProfile) - XCTAssertEqual(try? targetStorage.codableObject(forKey: .profile, decoder: decoder).get(), newProfile) + XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get()) + XCTAssertNoThrow(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get()) } - func testGetValue() throws { - let newProfile = Profile(name: "any", age: 1) - let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage - - profile = newProfile - - XCTAssertEqual(profile, newProfile) - XCTAssertEqual(try? targetStorage.codableObject(forKey: .profile, decoder: decoder).get(), newProfile) - } - - func testMigrationOnStoreValue() throws { - let oldProfile = Profile(name: "some", age: 0) - let newProfile = Profile(name: "any", age: 1) + // Precondition - source: ✅ target: ❌ + // PostCondition - source: ❌ target: ✅ + func testGetValueWithNoTarget() throws { + let oldProfile = Profile(name: "old", age: 0) let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage let _ = sourceStorage.set(encodableObject: oldProfile, forKey: .profile, encoder: encoder) - profile = newProfile - XCTAssertEqual(profile, newProfile) - XCTAssertEqual(try? targetStorage.codableObject(forKey: .profile, decoder: decoder).get(), newProfile) + XCTAssertEqual(profile, oldProfile) + XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get()) + XCTAssertNoThrow(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get()) } - func testStoreValueFromTarget() throws { - let oldProfile = Profile(name: "some", age: 0) - let newProfile = Profile(name: "any", age: 1) + // Precondition - source: ❌ target: ❌ + // PostCondition - source: ❌ target: ❌ + func testGetValueWithNoValues() throws { + let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage - let _ = targetStorage.set(encodableObject: oldProfile, forKey: .profile, encoder: encoder) - profile = newProfile + XCTAssertEqual(profile, nil) + XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get()) + XCTAssertThrowsError(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get()) + } - XCTAssertEqual(profile, newProfile) - XCTAssertEqual(try? targetStorage.codableObject(forKey: .profile, decoder: decoder).get(), newProfile) + // 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 = MockMigratingStorageContainer.defaultContainer.sourceStorage + let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage + + 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 sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage + let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage + + 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 = MockMigratingStorageContainer.defaultContainer.sourceStorage + let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage + + 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) + let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage + let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage + + 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 = MockMigratingStorageContainer.defaultContainer.sourceStorage + let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage + + 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 sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage + let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage + + 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 = MockMigratingStorageContainer.defaultContainer.sourceStorage + let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage + + 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 { + let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage + let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage + + profile = nil + + XCTAssertEqual(profile, nil) + XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get()) + XCTAssertThrowsError(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get()) } } diff --git a/Tests/TIFoundationUtilsTests/MigratingSingleValueStorageTests.swift b/Tests/TIFoundationUtilsTests/MigratingSingleValueStorageTests.swift index 5b00dcba..3f3ceab5 100644 --- a/Tests/TIFoundationUtilsTests/MigratingSingleValueStorageTests.swift +++ b/Tests/TIFoundationUtilsTests/MigratingSingleValueStorageTests.swift @@ -32,136 +32,171 @@ final class MigratingSingleValueStorageTests: XCTestCase { storageKey: .refreshToken) public lazy var refreshToken = BaseMigratingSingleValueStorage(sourceStorage: sourceRefreshTokenStorage, - targetStorage: targetRefreshTokenStorage) + targetStorage: targetRefreshTokenStorage, + errorLogger: MockStorageLogger.defaultLogger) override func setUp() { let _ = sourceRefreshTokenStorage.deleteValue() let _ = targetRefreshTokenStorage.deleteValue() + let _ = MockStorageLogger.defaultLogger.getError() } - // MARK: - Tests - - func testMigrationOnGetValue() throws { - let oldToken = "old-token" - - let _ = sourceRefreshTokenStorage.store(value: oldToken) - - XCTAssertEqual(try? refreshToken.getValue().get(), oldToken) - XCTAssertEqual(try? targetRefreshTokenStorage.getValue().get(), oldToken) - XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) - } - - func testGetValueFromTarget() throws { - let token = "new-token" - - let _ = targetRefreshTokenStorage.store(value: token) - - XCTAssertEqual(try? refreshToken.getValue().get(), token) - XCTAssertEqual(try? targetRefreshTokenStorage.getValue().get(), token) - XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) - } + // MARK: - Read Tests + // Precondition - source: ✅ target: ✅ + // PostCondition - source: ❌ target: ✅ func testGetValue() throws { - let token = "token" - - let _ = refreshToken.store(value: token) - - XCTAssertEqual(try? refreshToken.getValue().get(), token) - XCTAssertEqual(try? targetRefreshTokenStorage.getValue().get(), token) - XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) - } - - func testMigrationOnHasValue() throws { - let oldToken = "old-token" - - let _ = sourceRefreshTokenStorage.store(value: oldToken) - - XCTAssertTrue(refreshToken.hasStoredValue()) - XCTAssertFalse(targetRefreshTokenStorage.hasStoredValue()) - XCTAssertTrue(sourceRefreshTokenStorage.hasStoredValue()) - } - - func testHasValueFromTarget() throws { - let token = "new-token" - - let _ = targetRefreshTokenStorage.store(value: token) - - XCTAssertTrue(refreshToken.hasStoredValue()) - XCTAssertTrue(targetRefreshTokenStorage.hasStoredValue()) - XCTAssertFalse(sourceRefreshTokenStorage.hasStoredValue()) - } - - func testHasValue() throws { - let token = "token" - - let _ = refreshToken.store(value: token) - - XCTAssertTrue(refreshToken.hasStoredValue()) - XCTAssertTrue(targetRefreshTokenStorage.hasStoredValue()) - XCTAssertFalse(sourceRefreshTokenStorage.hasStoredValue()) - } - - func testMigrationOnDeleteValue() throws { - let oldToken = "old-token" - - let _ = sourceRefreshTokenStorage.store(value: oldToken) - - XCTAssertNoThrow(try refreshToken.deleteValue().get()) - XCTAssertThrowsError(try targetRefreshTokenStorage.getValue().get()) - XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) - } - - func testDeleteValueFromTarget() throws { - let token = "new-token" - - let _ = targetRefreshTokenStorage.store(value: token) - - XCTAssertNoThrow(try refreshToken.deleteValue().get()) - XCTAssertThrowsError(try targetRefreshTokenStorage.getValue().get()) - XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) - } - - func testDeleteValue() throws { - let token = "token" - - let _ = refreshToken.store(value: token) - - XCTAssertNoThrow(try refreshToken.deleteValue().get()) - XCTAssertThrowsError(try targetRefreshTokenStorage.getValue().get()) - XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) - } - - func testMigrationOnStoreValue() throws { let oldToken = "old-token" let newToken = "new-token" let _ = sourceRefreshTokenStorage.store(value: oldToken) - let _ = refreshToken.store(value: newToken) + let _ = targetRefreshTokenStorage.store(value: newToken) + XCTAssertEqual(try refreshToken.getValue().get(), newToken) + XCTAssertNil(MockStorageLogger.defaultLogger.getError()) XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) - XCTAssertEqual(try? refreshToken.getValue().get(), newToken) - XCTAssertEqual(try? targetRefreshTokenStorage.getValue().get(), newToken) + XCTAssertNoThrow(try targetRefreshTokenStorage.getValue().get()) } - func testStoreValueFromTarget() throws { - let oldToken = "old-token" + // Precondition - source: ❌ target: ✅ + // PostCondition - source: ❌ target: ✅ + func testGetValueWithNoSource() throws { let newToken = "new-token" - let _ = targetRefreshTokenStorage.store(value: oldToken) - let _ = refreshToken.store(value: newToken) + let _ = targetRefreshTokenStorage.store(value: newToken) + XCTAssertEqual(try refreshToken.getValue().get(), newToken) + XCTAssertNotNil(MockStorageLogger.defaultLogger.getError()) XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) - XCTAssertEqual(try? refreshToken.getValue().get(), newToken) - XCTAssertEqual(try? targetRefreshTokenStorage.getValue().get(), newToken) + 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 token = "token" + let oldToken = "old-token" + let newToken = "new-token" + let currentToken = "token" - let _ = refreshToken.store(value: token) + let _ = sourceRefreshTokenStorage.store(value: oldToken) + let _ = targetRefreshTokenStorage.store(value: newToken) - XCTAssertEqual(try? refreshToken.getValue().get(), token) + XCTAssertNoThrow(try refreshToken.store(value: currentToken).get()) + XCTAssertNil(MockStorageLogger.defaultLogger.getError()) XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) - XCTAssertEqual(try? targetRefreshTokenStorage.getValue().get(), token) + 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()) + XCTAssertNotNil(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()) + XCTAssertNotNil(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) + + XCTAssertNoThrow(try refreshToken.deleteValue().get()) + XCTAssertNotNil(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()) + XCTAssertNotNil(MockStorageLogger.defaultLogger.getError()) + XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) + XCTAssertThrowsError(try targetRefreshTokenStorage.getValue().get()) } } diff --git a/Tests/TIFoundationUtilsTests/Mocks/BaseSingleValueMockStorage.swift b/Tests/TIFoundationUtilsTests/Mocks/BaseSingleValueMockStorage.swift index 9bd84c5a..cbb15236 100644 --- a/Tests/TIFoundationUtilsTests/Mocks/BaseSingleValueMockStorage.swift +++ b/Tests/TIFoundationUtilsTests/Mocks/BaseSingleValueMockStorage.swift @@ -33,7 +33,11 @@ class BaseSingleValueMockStorage: BaseSingleValueStorage(forKey key: StorageKey) { - let _ = sourceStorage.removeValue(forKey: key) + override func removeSourceValue(forKey key: StorageKey) -> Result { + sourceStorage.removeValue(forKey: key) } } diff --git a/Tests/TIFoundationUtilsTests/Mocks/MockStorageLogger.swift b/Tests/TIFoundationUtilsTests/Mocks/MockStorageLogger.swift new file mode 100644 index 00000000..bffc12c0 --- /dev/null +++ b/Tests/TIFoundationUtilsTests/Mocks/MockStorageLogger.swift @@ -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 + } +} diff --git a/Tests/TIFoundationUtilsTests/Mocks/StringSingleValueMockStorage.swift b/Tests/TIFoundationUtilsTests/Mocks/StringSingleValueMockStorage.swift index 8251f1fd..cbfcda33 100644 --- a/Tests/TIFoundationUtilsTests/Mocks/StringSingleValueMockStorage.swift +++ b/Tests/TIFoundationUtilsTests/Mocks/StringSingleValueMockStorage.swift @@ -34,7 +34,7 @@ final class StringSingleValueMockStorage: BaseSingleValueMockStorage { } let storeValueClosure: StoreValueClosure = { storage, value, storageKey in - .success(storage.store(value: value, forKey: storageKey)) + .success(storage.store(value: value, forKey: storageKey)) } super.init(storage: storage, diff --git a/docs/tikeychainutils/singlevaluestorage.md b/docs/tikeychainutils/singlevaluestorage.md index 9a8dcc87..2a5aed06 100644 --- a/docs/tikeychainutils/singlevaluestorage.md +++ b/docs/tikeychainutils/singlevaluestorage.md @@ -125,14 +125,7 @@ case let .failure(storageError): let groupKeychain = Keychain(service: "app.group.identifier") let targetApiTokenKeychainStorage = StringValueKeychainStorage(keychain: groupKeychain, storageKey: .apiToken) -let migratingApiTokenKeychainStorage = BaseMigratingSingleValueStorage(sourceStorage: apiTokenKeychainStorage, - targetStorage: targetApiTokenKeychainStorage) +let migratingApiTokenKeychainStorage = apiTokenKeychainStorage.migrating(to: targetApiTokenKeychainStorage) let token = migratingApiTokenKeychainStorage.getValue() ``` - -Если нет необходимости в создании target storage, можно воспользоваться переменной `migrationStorage`, которая есть у каждого хранилища, наследованного от `BaseSingleValueStorage` - -```swift -apiTokenKeychainStorage.migrationStorage = groupKeychain -``` -- 2.40.1 From c55b8f73a9c514766d15c4479ef6dadf75f33abc Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Thu, 6 Jul 2023 18:42:18 +0300 Subject: [PATCH 05/12] fix: code review notes --- .../BaseMigratingSingleValueStorage.swift | 30 +++++++--------- ...BaseCodableMigratingStorageContainer.swift | 34 ++++++++----------- .../MigratingBackingStoreTests.swift | 2 -- .../MigratingSingleValueStorageTests.swift | 10 +++--- .../MockMigratingCodableBackingStore.swift | 2 +- 5 files changed, 34 insertions(+), 44 deletions(-) diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueStorage.swift index f658f27c..4661d882 100644 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueStorage.swift +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueStorage.swift @@ -50,12 +50,9 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType, } open func store(value: ValueType) -> Result { - let _ = sourceStorage.deleteValue() - .flatMapError { - errorLogger.log(error: $0, file: #file, line: #line) - - return .failure($0) - } + if case let .failure(error) = sourceStorage.deleteValue(), !error.isValueNotFound { + errorLogger.log(error: error, file: #file, line: #line) + } return targetStorage.store(value: value) } @@ -63,12 +60,9 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType, open func getValue() -> Result { targetStorage.getValue() .flatMap { - let _ = sourceStorage.deleteValue() - .flatMapError { - errorLogger.log(error: $0, file: #file, line: #line) - - return .failure($0) - } + if case let .failure(error) = sourceStorage.deleteValue(), !error.isValueNotFound { + errorLogger.log(error: error, file: #file, line: #line) + } return .success($0) } @@ -87,6 +81,13 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType, open func deleteValue() -> Result { targetStorage.deleteValue() + .flatMapError { + if !$0.isValueNotFound { + errorLogger.log(error: $0, file: #file, line: #line) + } + + return sourceStorage.deleteValue() + } .flatMap { if sourceStorage.hasStoredValue() { return sourceStorage.deleteValue() @@ -94,11 +95,6 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType, return .success($0) } - .flatMapError { - errorLogger.log(error: $0, file: #file, line: #line) - - return sourceStorage.deleteValue() - } } } diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift index 4986607e..741ed211 100644 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift @@ -28,10 +28,6 @@ open class BaseCodableMigratingStorageContainer, encoder: CodableKeyValueEncoder) -> Result { - let _ = removeSourceValue(forKey: key) - .flatMapError { - errorLogger.log(error: $0, file: #file, line: #line) - - return .failure($0) - } + if case let .failure(error) = removeSourceValue(forKey: key), !error.isValueNotFound { + errorLogger.log(error: error, file: #file, line: #line) + } return targetStorage.set(encodableObject: encodableObject, forKey: key, encoder: encoder) } @@ -83,7 +77,9 @@ where SourceStorage: CodableKeyValueStorage, TargetStorage: CodableKeyValueStora .flatMapError { _ in .success(value) } } .flatMapError { - errorLogger.log(error: $0, file: #file, line: #line) + if !$0.isValueNotFound { + errorLogger.log(error: $0, file: #file, line: #line) + } return removeSourceValue(forKey: key) } diff --git a/Tests/TIFoundationUtilsTests/MigratingBackingStoreTests.swift b/Tests/TIFoundationUtilsTests/MigratingBackingStoreTests.swift index bf368892..baea7de3 100644 --- a/Tests/TIFoundationUtilsTests/MigratingBackingStoreTests.swift +++ b/Tests/TIFoundationUtilsTests/MigratingBackingStoreTests.swift @@ -33,8 +33,6 @@ final class MigratingBackingStoreTests: XCTestCase { override func setUp() { profile = nil - let _ = MockMigratingStorageContainer.defaultContainer.sourceStorage.removeValue(forKey: .profile) - let _ = MockMigratingStorageContainer.defaultContainer.targetStorage.removeValue(forKey: .profile) } // MARK: - Read Tests diff --git a/Tests/TIFoundationUtilsTests/MigratingSingleValueStorageTests.swift b/Tests/TIFoundationUtilsTests/MigratingSingleValueStorageTests.swift index 3f3ceab5..69a6a280 100644 --- a/Tests/TIFoundationUtilsTests/MigratingSingleValueStorageTests.swift +++ b/Tests/TIFoundationUtilsTests/MigratingSingleValueStorageTests.swift @@ -66,7 +66,7 @@ final class MigratingSingleValueStorageTests: XCTestCase { let _ = targetRefreshTokenStorage.store(value: newToken) XCTAssertEqual(try refreshToken.getValue().get(), newToken) - XCTAssertNotNil(MockStorageLogger.defaultLogger.getError()) + XCTAssertNil(MockStorageLogger.defaultLogger.getError()) XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) XCTAssertNoThrow(try targetRefreshTokenStorage.getValue().get()) } @@ -120,7 +120,7 @@ final class MigratingSingleValueStorageTests: XCTestCase { let _ = targetRefreshTokenStorage.store(value: newToken) XCTAssertNoThrow(try refreshToken.store(value: currentToken).get()) - XCTAssertNotNil(MockStorageLogger.defaultLogger.getError()) + XCTAssertNil(MockStorageLogger.defaultLogger.getError()) XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) XCTAssertEqual(try targetRefreshTokenStorage.getValue().get(), currentToken) } @@ -145,7 +145,7 @@ final class MigratingSingleValueStorageTests: XCTestCase { let currentToken = "token" XCTAssertNoThrow(try refreshToken.store(value: currentToken).get()) - XCTAssertNotNil(MockStorageLogger.defaultLogger.getError()) + XCTAssertNil(MockStorageLogger.defaultLogger.getError()) XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) XCTAssertEqual(try targetRefreshTokenStorage.getValue().get(), currentToken) } @@ -186,7 +186,7 @@ final class MigratingSingleValueStorageTests: XCTestCase { let _ = sourceRefreshTokenStorage.store(value: oldToken) XCTAssertNoThrow(try refreshToken.deleteValue().get()) - XCTAssertNotNil(MockStorageLogger.defaultLogger.getError()) + XCTAssertNil(MockStorageLogger.defaultLogger.getError()) XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) XCTAssertThrowsError(try targetRefreshTokenStorage.getValue().get()) } @@ -195,7 +195,7 @@ final class MigratingSingleValueStorageTests: XCTestCase { // PostCondition - source: ❌ target: ❌ func testDeleteValueWithNoValues() throws { XCTAssertThrowsError(try refreshToken.deleteValue().get()) - XCTAssertNotNil(MockStorageLogger.defaultLogger.getError()) + XCTAssertNil(MockStorageLogger.defaultLogger.getError()) XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) XCTAssertThrowsError(try targetRefreshTokenStorage.getValue().get()) } diff --git a/Tests/TIFoundationUtilsTests/Mocks/MockMigratingCodableBackingStore.swift b/Tests/TIFoundationUtilsTests/Mocks/MockMigratingCodableBackingStore.swift index d2bf74aa..f336e1be 100644 --- a/Tests/TIFoundationUtilsTests/Mocks/MockMigratingCodableBackingStore.swift +++ b/Tests/TIFoundationUtilsTests/Mocks/MockMigratingCodableBackingStore.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Touch Instinct +// 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 -- 2.40.1 From c63105313148ced6edc469d9c74b5a1b6ee8c18b Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Fri, 7 Jul 2023 09:01:17 +0300 Subject: [PATCH 06/12] fix: code review notes --- ...eyVlaueStorage+MigratingBackingStore.swift | 8 +++- .../AnySingleValueStorage.swift | 2 +- ...wift => MigratingSingleValueStorage.swift} | 48 +++++++++++-------- .../DefaultFingerprintsProvider.swift | 2 +- .../MigratingBackingStoreTests.swift | 30 ++---------- .../MigratingSingleValueStorageTests.swift | 4 +- 6 files changed, 42 insertions(+), 52 deletions(-) rename TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/{BaseMigratingSingleValueStorage.swift => MigratingSingleValueStorage.swift} (71%) diff --git a/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyVlaueStorage+MigratingBackingStore.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyVlaueStorage+MigratingBackingStore.swift index fc095f2f..1786f9de 100644 --- a/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyVlaueStorage+MigratingBackingStore.swift +++ b/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyVlaueStorage+MigratingBackingStore.swift @@ -32,7 +32,9 @@ public extension MigratingBackingStore where MigratingStorages: CodableKeyValueS where StoreContent == Value? { let getClosure: GetClosure = { container in - // If source storage has value and target doesn't we can't migrate it from storate container + // 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) @@ -57,7 +59,9 @@ public extension MigratingBackingStore where MigratingStorages: CodableKeyValueS where StoreContent == Value? { let getClosure: GetClosure = { container in - // If source storage has value and target doesn't we can't migrate it from storate container + // 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) diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/AnySingleValueStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/AnySingleValueStorage.swift index 81a2a197..15516991 100644 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/AnySingleValueStorage.swift +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/AnySingleValueStorage.swift @@ -58,7 +58,7 @@ public struct AnySingleValueStorage: SingleValueSto } public extension SingleValueStorage { - func eraseToAnySingleValueStorate() -> AnySingleValueStorage { + func eraseToAnySingleValueStorage() -> AnySingleValueStorage { AnySingleValueStorage(storage: self) } } diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/MigratingSingleValueStorage.swift similarity index 71% rename from TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueStorage.swift rename to TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/MigratingSingleValueStorage.swift index 4661d882..09b80ca8 100644 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/BaseMigratingSingleValueStorage.swift +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/MigratingSingleValueStorage.swift @@ -22,8 +22,8 @@ import TILogging -open class BaseMigratingSingleValueStorage: SingleValueStorage +public final class MigratingSingleValueStorage: SingleValueStorage where SourceStorage.ErrorType == TargetStorage.ErrorType, SourceStorage.ErrorType == StorageError, SourceStorage.ValueType == TargetStorage.ValueType { @@ -45,11 +45,11 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType, // MARK: - SingleValueStorage - open func hasStoredValue() -> Bool { + public func hasStoredValue() -> Bool { targetStorage.hasStoredValue() || sourceStorage.hasStoredValue() } - open func store(value: ValueType) -> Result { + public func store(value: ValueType) -> Result { if case let .failure(error) = sourceStorage.deleteValue(), !error.isValueNotFound { errorLogger.log(error: error, file: #file, line: #line) } @@ -57,7 +57,7 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType, return targetStorage.store(value: value) } - open func getValue() -> Result { + public func getValue() -> Result { targetStorage.getValue() .flatMap { if case let .failure(error) = sourceStorage.deleteValue(), !error.isValueNotFound { @@ -79,31 +79,41 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType, } } - open func deleteValue() -> Result { - targetStorage.deleteValue() - .flatMapError { - if !$0.isValueNotFound { - errorLogger.log(error: $0, file: #file, line: #line) - } + public func deleteValue() -> Result { + switch (targetStorage.deleteValue(), sourceStorage.deleteValue()) { + case (.success, .success): + return .success(()) - return sourceStorage.deleteValue() + case let (.success, .failure(error)): + if error.isValueNotFound { + return .success(()) } - .flatMap { - if sourceStorage.hasStoredValue() { - return sourceStorage.deleteValue() - } - return .success($0) + errorLogger.log(error: error, file: #file, line: #line) + + return .failure(error) + + case let (.failure(error), .success): + if error.isValueNotFound { + return .success(()) } + + errorLogger.log(error: error, file: #file, line: #line) + + return .failure(error) + + case let (.failure, .failure(error)): + return .failure(error) + } } } public extension SingleValueStorage where ErrorType == StorageError { - func migrating(to targetStorage: S) -> BaseMigratingSingleValueStorage + func migrating(to targetStorage: S) -> MigratingSingleValueStorage where ErrorType == S.ErrorType, ValueType == S.ValueType { - BaseMigratingSingleValueStorage(sourceStorage: self, targetStorage: targetStorage) + MigratingSingleValueStorage(sourceStorage: self, targetStorage: targetStorage) } } diff --git a/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/DefaultFingerprintsProvider.swift b/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/DefaultFingerprintsProvider.swift index cf21bda6..a37b133c 100644 --- a/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/DefaultFingerprintsProvider.swift +++ b/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/DefaultFingerprintsProvider.swift @@ -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 diff --git a/Tests/TIFoundationUtilsTests/MigratingBackingStoreTests.swift b/Tests/TIFoundationUtilsTests/MigratingBackingStoreTests.swift index baea7de3..305faa86 100644 --- a/Tests/TIFoundationUtilsTests/MigratingBackingStoreTests.swift +++ b/Tests/TIFoundationUtilsTests/MigratingBackingStoreTests.swift @@ -21,10 +21,12 @@ // import XCTest -@testable import TIFoundationUtils +import TIFoundationUtils final class MigratingBackingStoreTests: XCTestCase { + private let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage + private let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage private let encoder = JSONKeyValueEncoder() private let decoder = JSONKeyValueDecoder() @@ -42,8 +44,6 @@ final class MigratingBackingStoreTests: XCTestCase { func testGetValue() throws { let oldProfile = Profile(name: "old", age: 0) let newProfile = Profile(name: "new", age: 0) - let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage - let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage let _ = sourceStorage.set(encodableObject: oldProfile, forKey: .profile, encoder: encoder) let _ = targetStorage.set(encodableObject: newProfile, forKey: .profile, encoder: encoder) @@ -57,8 +57,6 @@ final class MigratingBackingStoreTests: XCTestCase { // PostCondition - source: ❌ target: ✅ func testGetValueWithNoSource() throws { let newProfile = Profile(name: "new", age: 0) - let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage - let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage let _ = targetStorage.set(encodableObject: newProfile, forKey: .profile, encoder: encoder) @@ -71,8 +69,6 @@ final class MigratingBackingStoreTests: XCTestCase { // PostCondition - source: ❌ target: ✅ func testGetValueWithNoTarget() throws { let oldProfile = Profile(name: "old", age: 0) - let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage - let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage let _ = sourceStorage.set(encodableObject: oldProfile, forKey: .profile, encoder: encoder) @@ -84,9 +80,6 @@ final class MigratingBackingStoreTests: XCTestCase { // Precondition - source: ❌ target: ❌ // PostCondition - source: ❌ target: ❌ func testGetValueWithNoValues() throws { - let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage - let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage - XCTAssertEqual(profile, nil) XCTAssertThrowsError(try sourceStorage.codableObject(forKey: .profile, decoder: decoder).get()) XCTAssertThrowsError(try targetStorage.codableObject(forKey: .profile, decoder: decoder).get()) @@ -100,8 +93,6 @@ final class MigratingBackingStoreTests: XCTestCase { let oldProfile = Profile(name: "old", age: 0) let newProfile = Profile(name: "new", age: 0) let currentProfile = Profile(name: "name", age: 0) - let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage - let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage let _ = sourceStorage.set(encodableObject: oldProfile, forKey: .profile, encoder: encoder) let _ = targetStorage.set(encodableObject: newProfile, forKey: .profile, encoder: encoder) @@ -117,8 +108,6 @@ final class MigratingBackingStoreTests: XCTestCase { func testStoreValueWithNoSource() throws { let newProfile = Profile(name: "new", age: 0) let currentProfile = Profile(name: "name", age: 0) - let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage - let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage let _ = targetStorage.set(encodableObject: newProfile, forKey: .profile, encoder: encoder) profile = currentProfile @@ -133,8 +122,6 @@ final class MigratingBackingStoreTests: XCTestCase { func testStoreValueWithNoTarget() throws { let oldProfile = Profile(name: "old", age: 0) let currentProfile = Profile(name: "name", age: 0) - let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage - let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage let _ = sourceStorage.set(encodableObject: oldProfile, forKey: .profile, encoder: encoder) profile = currentProfile @@ -148,8 +135,6 @@ final class MigratingBackingStoreTests: XCTestCase { // PostCondition - source: ❌ target: ✅ func testStoreValueWithNoValues() throws { let currentProfile = Profile(name: "name", age: 0) - let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage - let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage profile = currentProfile @@ -165,8 +150,6 @@ final class MigratingBackingStoreTests: XCTestCase { func testDeleteValue() throws { let oldProfile = Profile(name: "old", age: 0) let newProfile = Profile(name: "new", age: 0) - let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage - let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage let _ = sourceStorage.set(encodableObject: oldProfile, forKey: .profile, encoder: encoder) let _ = targetStorage.set(encodableObject: newProfile, forKey: .profile, encoder: encoder) @@ -181,8 +164,6 @@ final class MigratingBackingStoreTests: XCTestCase { // PostCondition - source: ❌ target: ❌ func testDeleteValueWithNoSource() throws { let newProfile = Profile(name: "new", age: 0) - let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage - let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage let _ = targetStorage.set(encodableObject: newProfile, forKey: .profile, encoder: encoder) profile = nil @@ -196,8 +177,6 @@ final class MigratingBackingStoreTests: XCTestCase { // PostCondition - source: ❌ target: ❌ func testDeleteValueWithNoTarget() throws { let oldProfile = Profile(name: "old", age: 0) - let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage - let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage let _ = sourceStorage.set(encodableObject: oldProfile, forKey: .profile, encoder: encoder) profile = nil @@ -210,9 +189,6 @@ final class MigratingBackingStoreTests: XCTestCase { // Precondition - source: ❌ target: ❌ // PostCondition - source: ❌ target: ❌ func testDeleteValueWithNoValues() throws { - let sourceStorage = MockMigratingStorageContainer.defaultContainer.sourceStorage - let targetStorage = MockMigratingStorageContainer.defaultContainer.targetStorage - profile = nil XCTAssertEqual(profile, nil) diff --git a/Tests/TIFoundationUtilsTests/MigratingSingleValueStorageTests.swift b/Tests/TIFoundationUtilsTests/MigratingSingleValueStorageTests.swift index 69a6a280..70532748 100644 --- a/Tests/TIFoundationUtilsTests/MigratingSingleValueStorageTests.swift +++ b/Tests/TIFoundationUtilsTests/MigratingSingleValueStorageTests.swift @@ -21,7 +21,7 @@ // import XCTest -@testable import TIFoundationUtils +import TIFoundationUtils final class MigratingSingleValueStorageTests: XCTestCase { @@ -31,7 +31,7 @@ final class MigratingSingleValueStorageTests: XCTestCase { private let targetRefreshTokenStorage = StringSingleValueMockStorage(storage: .defaultGroupStorage, storageKey: .refreshToken) - public lazy var refreshToken = BaseMigratingSingleValueStorage(sourceStorage: sourceRefreshTokenStorage, + public lazy var refreshToken = MigratingSingleValueStorage(sourceStorage: sourceRefreshTokenStorage, targetStorage: targetRefreshTokenStorage, errorLogger: MockStorageLogger.defaultLogger) -- 2.40.1 From a79ff67a384799129dbdb1af2228498eb001569d Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Fri, 7 Jul 2023 14:00:43 +0300 Subject: [PATCH 07/12] fix: logic of deletion --- ...eyVlaueStorage+MigratingBackingStore.swift | 52 ++++++++------- ...BaseCodableMigratingStorageContainer.swift | 65 ++++++++++++------- .../MigratingSingleValueStorage.swift | 42 ++++++------ .../Contents.swift | 2 +- .../MigratingSingleValueStorageTests.swift | 2 +- docs/tikeychainutils/singlevaluestorage.md | 2 +- 6 files changed, 97 insertions(+), 68 deletions(-) diff --git a/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyVlaueStorage+MigratingBackingStore.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyVlaueStorage+MigratingBackingStore.swift index 1786f9de..4d21b8c2 100644 --- a/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyVlaueStorage+MigratingBackingStore.swift +++ b/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyVlaueStorage+MigratingBackingStore.swift @@ -32,16 +32,7 @@ public extension MigratingBackingStore where MigratingStorages: CodableKeyValueS where StoreContent == Value? { let getClosure: GetClosure = { container in - // 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) - } - .get() + Self.getValue(from: container, forKey: key, decoder: decoder, encoder: encoder) } let setClosure: SetClosure = { container, value in @@ -59,20 +50,7 @@ public extension MigratingBackingStore where MigratingStorages: CodableKeyValueS where StoreContent == Value? { let getClosure: GetClosure = { container in - // 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) - } - .flatMapError { _ in - container.set(encodableObject: wrappedValue, forKey: key, encoder: encoder) - .flatMap { _ in .success(wrappedValue) } - } - .get() + Self.getValue(from: container, forKey: key, wrappedValue: wrappedValue, decoder: decoder, encoder: encoder) } let setClosure: SetClosure = { container, value in @@ -81,4 +59,30 @@ public extension MigratingBackingStore where MigratingStorages: CodableKeyValueS self.init(storageContainer: storageContainer, getClosure: getClosure, setClosure: setClosure) } + + static func getValue(from container: MigratingStorages, + forKey key: StorageKey, + 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) + } + .flatMapError { + guard let wrappedValue else { + return .failure($0) + } + + return container.set(encodableObject: wrappedValue, forKey: key, encoder: encoder) + .flatMap { _ in .success(wrappedValue) } + } + .get() + } } diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift index 741ed211..d8e1b446 100644 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift @@ -44,16 +44,14 @@ where SourceStorage: CodableKeyValueStorage, TargetStorage: CodableKeyValueStora targetStorage.codableObject(forKey: key, decoder: decoder) .flatMap { - if case let .failure(error) = removeSourceValue(forKey: key), !error.isValueNotFound { - errorLogger.log(error: error, file: #file, line: #line) + if case let .failure(error) = removeSourceValue(forKey: key) { + logErrorIfNeeded(error, file: #file, line: #line) } return .success($0) } .flatMapError { - if !$0.isValueNotFound { - errorLogger.log(error: $0, file: #file, line: #line) - } + logErrorIfNeeded($0, file: #file, line: #line) return sourceStorage.codableObject(forKey: key, decoder: decoder) } @@ -63,33 +61,46 @@ where SourceStorage: CodableKeyValueStorage, TargetStorage: CodableKeyValueStora forKey key: StorageKey, encoder: CodableKeyValueEncoder) -> Result { - if case let .failure(error) = removeSourceValue(forKey: key), !error.isValueNotFound { - errorLogger.log(error: error, file: #file, line: #line) + if case let .failure(error) = removeSourceValue(forKey: key) { + logErrorIfNeeded(error, file: #file, line: #line) } return targetStorage.set(encodableObject: encodableObject, forKey: key, encoder: encoder) } open func removeCodableValue(forKey key: StorageKey) -> Result { - targetStorage.removeCodableValue(forKey: key) - .flatMap { value in - sourceStorage.removeCodableValue(forKey: key) - .flatMapError { _ in .success(value) } - } - .flatMapError { - if !$0.isValueNotFound { - errorLogger.log(error: $0, file: #file, line: #line) - } + switch (targetStorage.removeCodableValue(forKey: key), sourceStorage.removeCodableValue(forKey: key)) { + case (.success, .success): + return .success(()) - return removeSourceValue(forKey: key) - } + 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) + } } open func hasCodableValue(forKey key: StorageKey) -> Result { - targetStorage.hasCodableValue(forKey: key) - .flatMapError { _ in - sourceStorage.hasCodableValue(forKey: key) - } + 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 @@ -98,4 +109,14 @@ where SourceStorage: CodableKeyValueStorage, TargetStorage: CodableKeyValueStora // 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) + } } diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/MigratingSingleValueStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/MigratingSingleValueStorage.swift index 09b80ca8..89b25f72 100644 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/MigratingSingleValueStorage.swift +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/MigratingSingleValueStorage.swift @@ -36,7 +36,7 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType, public init(sourceStorage: SourceStorage, targetStorage: TargetStorage, - errorLogger: ErrorLogger = TIFoundationLogger(category: "BaseMigratingSingleValueStorage")) { + errorLogger: ErrorLogger = TIFoundationLogger(category: "MigratingSingleValueStorage")) { self.sourceStorage = sourceStorage self.targetStorage = targetStorage @@ -50,8 +50,8 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType, } public func store(value: ValueType) -> Result { - if case let .failure(error) = sourceStorage.deleteValue(), !error.isValueNotFound { - errorLogger.log(error: error, file: #file, line: #line) + if case let .failure(error) = sourceStorage.deleteValue() { + logErrorIfNeeded(error, file: #file, line: #line) } return targetStorage.store(value: value) @@ -60,8 +60,8 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType, public func getValue() -> Result { targetStorage.getValue() .flatMap { - if case let .failure(error) = sourceStorage.deleteValue(), !error.isValueNotFound { - errorLogger.log(error: error, file: #file, line: #line) + if case let .failure(error) = sourceStorage.deleteValue() { + logErrorIfNeeded(error, file: #file, line: #line) } return .success($0) @@ -85,29 +85,33 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType, return .success(()) case let (.success, .failure(error)): - if error.isValueNotFound { - return .success(()) - } + logErrorIfNeeded(error, file: #file, line: #line) - errorLogger.log(error: error, file: #file, line: #line) - - return .failure(error) + return .success(()) case let (.failure(error), .success): - if error.isValueNotFound { - return .success(()) - } - - errorLogger.log(error: error, file: #file, line: #line) - return .failure(error) - case let (.failure, .failure(error)): - 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 + } + + errorLogger.log(error: error, file: file, line: line) + } } +// MARK: - Factory method + public extension SingleValueStorage where ErrorType == StorageError { func migrating(to targetStorage: S) -> MigratingSingleValueStorage diff --git a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift index 2648322f..af651031 100644 --- a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift +++ b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift @@ -118,7 +118,7 @@ case let .failure(storageError): /*: ### MigratingStorage - При необходимости мигрировать с одного keychain на другой можно воспользоваться классом `BaseMigratingSingleValueStorage`. При создании "мигрирующего" хранилища необходимо будет указать: + При необходимости мигрировать с одного keychain на другой можно воспользоваться классом `MigratingSingleValueStorage`. При создании "мигрирующего" хранилища необходимо будет указать: - source storage: хранилище с которого мигрируем - target storage: хранилище на которое мигрируем diff --git a/Tests/TIFoundationUtilsTests/MigratingSingleValueStorageTests.swift b/Tests/TIFoundationUtilsTests/MigratingSingleValueStorageTests.swift index 70532748..445e65f3 100644 --- a/Tests/TIFoundationUtilsTests/MigratingSingleValueStorageTests.swift +++ b/Tests/TIFoundationUtilsTests/MigratingSingleValueStorageTests.swift @@ -185,7 +185,7 @@ final class MigratingSingleValueStorageTests: XCTestCase { let _ = sourceRefreshTokenStorage.store(value: oldToken) - XCTAssertNoThrow(try refreshToken.deleteValue().get()) + XCTAssertThrowsError(try refreshToken.deleteValue().get()) XCTAssertNil(MockStorageLogger.defaultLogger.getError()) XCTAssertThrowsError(try sourceRefreshTokenStorage.getValue().get()) XCTAssertThrowsError(try targetRefreshTokenStorage.getValue().get()) diff --git a/docs/tikeychainutils/singlevaluestorage.md b/docs/tikeychainutils/singlevaluestorage.md index 2a5aed06..07d0518f 100644 --- a/docs/tikeychainutils/singlevaluestorage.md +++ b/docs/tikeychainutils/singlevaluestorage.md @@ -116,7 +116,7 @@ case let .failure(storageError): ### MigratingStorage - При необходимости мигрировать с одного keychain на другой можно воспользоваться классом `BaseMigratingSingleValueStorage`. При создании "мигрирующего" хранилища необходимо будет указать: + При необходимости мигрировать с одного keychain на другой можно воспользоваться классом `MigratingSingleValueStorage`. При создании "мигрирующего" хранилища необходимо будет указать: - source storage: хранилище с которого мигрируем - target storage: хранилище на которое мигрируем -- 2.40.1 From 45c060403fb3eae3839c1af546b323815e5013ba Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Fri, 7 Jul 2023 15:17:54 +0300 Subject: [PATCH 08/12] fix: logic of getting value from storage --- ...BaseCodableMigratingStorageContainer.swift | 11 +++++--- .../MigratingSingleValueStorage.swift | 28 +++++++++++-------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift index d8e1b446..3ef52349 100644 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift @@ -61,11 +61,14 @@ where SourceStorage: CodableKeyValueStorage, TargetStorage: CodableKeyValueStora forKey key: StorageKey, encoder: CodableKeyValueEncoder) -> Result { - if case let .failure(error) = removeSourceValue(forKey: key) { - logErrorIfNeeded(error, file: #file, line: #line) - } + targetStorage.set(encodableObject: encodableObject, forKey: key, encoder: encoder) + .flatMap { + if case let .failure(error) = removeSourceValue(forKey: key) { + logErrorIfNeeded(error, file: #file, line: #line) + } - return targetStorage.set(encodableObject: encodableObject, forKey: key, encoder: encoder) + return .success(()) + } } open func removeCodableValue(forKey key: StorageKey) -> Result { diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/MigratingSingleValueStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/MigratingSingleValueStorage.swift index 89b25f72..11728a8a 100644 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/MigratingSingleValueStorage.swift +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/MigratingSingleValueStorage.swift @@ -50,11 +50,14 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType, } public func store(value: ValueType) -> Result { - if case let .failure(error) = sourceStorage.deleteValue() { - logErrorIfNeeded(error, file: #file, line: #line) - } + targetStorage.store(value: value) + .flatMap { + if case let .failure(error) = sourceStorage.deleteValue() { + logErrorIfNeeded(error, file: #file, line: #line) + } - return targetStorage.store(value: value) + return .success(()) + } } public func getValue() -> Result { @@ -69,12 +72,8 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType, .flatMapError { _ in sourceStorage.getValue() .flatMap { value in - Result { - try store(value: value).get() - - return value - } - .mapError { .unableToExtractData(underlyingError: $0) } + store(value: value) + .map { _ in value } } } } @@ -110,7 +109,7 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType, } } -// MARK: - Factory method +// MARK: - Factory methods public extension SingleValueStorage where ErrorType == StorageError { @@ -120,4 +119,11 @@ public extension SingleValueStorage where ErrorType == StorageError { MigratingSingleValueStorage(sourceStorage: self, targetStorage: targetStorage) } + + func migrating(from sourceStorage: S) -> MigratingSingleValueStorage + where ErrorType == S.ErrorType, + ValueType == S.ValueType { + + MigratingSingleValueStorage(sourceStorage: sourceStorage, targetStorage: self) + } } -- 2.40.1 From da527644a7ca882ab0870de2ecb93a679da409e3 Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Fri, 7 Jul 2023 15:39:11 +0300 Subject: [PATCH 09/12] fix: move tifoundationlogger to it's own folder --- .../{ => TIFoundationLogger/Sources}/TIFoundationLogger.swift | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename TIFoundationUtils/{ => TIFoundationLogger/Sources}/TIFoundationLogger.swift (100%) diff --git a/TIFoundationUtils/TIFoundationLogger.swift b/TIFoundationUtils/TIFoundationLogger/Sources/TIFoundationLogger.swift similarity index 100% rename from TIFoundationUtils/TIFoundationLogger.swift rename to TIFoundationUtils/TIFoundationLogger/Sources/TIFoundationLogger.swift -- 2.40.1 From 85b206bf18936350070c091c4811e5ce9b35f507 Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Fri, 7 Jul 2023 15:39:11 +0300 Subject: [PATCH 10/12] fix: move tifoundationlogger to it's own folder --- ...BaseCodableMigratingStorageContainer.swift | 2 +- .../MigratingSingleValueStorage.swift | 2 +- .../Logger/Sources/TIFoundationLogger.swift | 30 +++++++++++++++++++ TIFoundationUtils/TIFoundationUtils.podspec | 2 +- 4 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 TIFoundationUtils/Logger/Sources/TIFoundationLogger.swift diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift index 3ef52349..37f0383c 100644 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift @@ -32,7 +32,7 @@ where SourceStorage: CodableKeyValueStorage, TargetStorage: CodableKeyValueStora public init(sourceStorage: SourceStorage, targetStorage: TargetStorage, - errorLogger: ErrorLogger = TIFoundationLogger(category: "BaseCodableMigratingStorageContainer")) { + errorLogger: ErrorLogger) { self.errorLogger = errorLogger diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/MigratingSingleValueStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/MigratingSingleValueStorage.swift index 11728a8a..38674f0b 100644 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/MigratingSingleValueStorage.swift +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/MigratingSingleValueStorage.swift @@ -36,7 +36,7 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType, public init(sourceStorage: SourceStorage, targetStorage: TargetStorage, - errorLogger: ErrorLogger = TIFoundationLogger(category: "MigratingSingleValueStorage")) { + errorLogger: ErrorLogger) { self.sourceStorage = sourceStorage self.targetStorage = targetStorage diff --git a/TIFoundationUtils/Logger/Sources/TIFoundationLogger.swift b/TIFoundationUtils/Logger/Sources/TIFoundationLogger.swift new file mode 100644 index 00000000..dbfeb1e2 --- /dev/null +++ b/TIFoundationUtils/Logger/Sources/TIFoundationLogger.swift @@ -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)) + } +} diff --git a/TIFoundationUtils/TIFoundationUtils.podspec b/TIFoundationUtils/TIFoundationUtils.podspec index 9e0f609b..fb0aa6ef 100644 --- a/TIFoundationUtils/TIFoundationUtils.podspec +++ b/TIFoundationUtils/TIFoundationUtils.podspec @@ -19,7 +19,7 @@ Pod::Spec.new do |s| s.exclude_files = s.name + '/*.app', '**/NefPlaygroundSupport.swift' end - s.dependency 'TISwiftUtils', s.version.to_s s.dependency 'TILogging', s.version.to_s + s.dependency 'TISwiftUtils', s.version.to_s s.framework = 'Foundation' end -- 2.40.1 From 6084dd5fec78913d47170624519a1daaf12ed0be Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Fri, 7 Jul 2023 15:39:11 +0300 Subject: [PATCH 11/12] fix: move tifoundationlogger to it's own folder --- .../Sources/TIFoundationLogger.swift | 30 ------------------- 1 file changed, 30 deletions(-) delete mode 100644 TIFoundationUtils/TIFoundationLogger/Sources/TIFoundationLogger.swift diff --git a/TIFoundationUtils/TIFoundationLogger/Sources/TIFoundationLogger.swift b/TIFoundationUtils/TIFoundationLogger/Sources/TIFoundationLogger.swift deleted file mode 100644 index dbfeb1e2..00000000 --- a/TIFoundationUtils/TIFoundationLogger/Sources/TIFoundationLogger.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// 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)) - } -} -- 2.40.1 From 6b7be340f570df724f80c1cac3fb43de86dc7677 Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Sun, 9 Jul 2023 22:15:36 +0300 Subject: [PATCH 12/12] fix: added default logger parameter --- .../Container/BaseCodableMigratingStorageContainer.swift | 2 +- .../MigratableStorage/MigratingSingleValueStorage.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift index 37f0383c..3ef52349 100644 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/Container/BaseCodableMigratingStorageContainer.swift @@ -32,7 +32,7 @@ where SourceStorage: CodableKeyValueStorage, TargetStorage: CodableKeyValueStora public init(sourceStorage: SourceStorage, targetStorage: TargetStorage, - errorLogger: ErrorLogger) { + errorLogger: ErrorLogger = TIFoundationLogger(category: "BaseCodableMigratingStorageContainer")) { self.errorLogger = errorLogger diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/MigratingSingleValueStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/MigratingSingleValueStorage.swift index 38674f0b..11728a8a 100644 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/MigratingSingleValueStorage.swift +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/Implementations/MigratableStorage/MigratingSingleValueStorage.swift @@ -36,7 +36,7 @@ where SourceStorage.ErrorType == TargetStorage.ErrorType, public init(sourceStorage: SourceStorage, targetStorage: TargetStorage, - errorLogger: ErrorLogger) { + errorLogger: ErrorLogger = TIFoundationLogger(category: "MigratingSingleValueStorage")) { self.sourceStorage = sourceStorage self.targetStorage = targetStorage -- 2.40.1